├── .php-cs-fixer.php ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── lets_encrypt.php ├── database └── migrations │ ├── add_lets_encrypt_certificates_subject_alternative_names.php.stub │ └── create_lets_encrypt_certificates_table.php.stub └── src ├── AcmePhp ├── Core │ ├── AcmeClient.php │ ├── AcmeClientInterface.php │ ├── AcmeClientV2Interface.php │ ├── Exception │ │ ├── AcmeCoreClientException.php │ │ ├── AcmeCoreException.php │ │ ├── AcmeCoreServerException.php │ │ ├── AcmeDnsResolutionException.php │ │ ├── Protocol │ │ │ ├── CertificateRequestFailedException.php │ │ │ ├── CertificateRequestTimedOutException.php │ │ │ ├── CertificateRevocationException.php │ │ │ ├── ChallengeFailedException.php │ │ │ ├── ChallengeNotSupportedException.php │ │ │ ├── ChallengeTimedOutException.php │ │ │ ├── ExpectedJsonException.php │ │ │ └── ProtocolException.php │ │ └── Server │ │ │ ├── BadCsrServerException.php │ │ │ ├── BadNonceServerException.php │ │ │ ├── CaaServerException.php │ │ │ ├── ConnectionServerException.php │ │ │ ├── DnsServerException.php │ │ │ ├── IncorrectResponseServerException.php │ │ │ ├── InternalServerException.php │ │ │ ├── InvalidContactServerException.php │ │ │ ├── InvalidEmailServerException.php │ │ │ ├── MalformedServerException.php │ │ │ ├── OrderNotReadyServerException.php │ │ │ ├── RateLimitedServerException.php │ │ │ ├── RejectedIdentifierServerException.php │ │ │ ├── TlsServerException.php │ │ │ ├── UnauthorizedServerException.php │ │ │ ├── UnknownHostServerException.php │ │ │ ├── UnsupportedContactServerException.php │ │ │ ├── UnsupportedIdentifierServerException.php │ │ │ └── UserActionRequiredServerException.php │ ├── Http │ │ ├── Base64SafeEncoder.php │ │ ├── SecureHttpClient.php │ │ ├── SecureHttpClientFactory.php │ │ └── ServerErrorHandler.php │ ├── Protocol │ │ ├── AuthorizationChallenge.php │ │ ├── CertificateOrder.php │ │ ├── ResourcesDirectory.php │ │ └── RevocationReason.php │ └── Util │ │ └── JsonDecoder.php └── Ssl │ ├── Certificate.php │ ├── CertificateRequest.php │ ├── CertificateResponse.php │ ├── DistinguishedName.php │ ├── Exception │ ├── AcmeSslException.php │ ├── CSRSigningException.php │ ├── CertificateFormatException.php │ ├── CertificateParsingException.php │ ├── DataSigningException.php │ ├── KeyFormatException.php │ ├── KeyGenerationException.php │ ├── KeyPairGenerationException.php │ ├── KeyParsingException.php │ ├── ParsingException.php │ └── SigningException.php │ ├── Generator │ ├── ChainPrivateKeyGenerator.php │ ├── DhKey │ │ ├── DhKeyGenerator.php │ │ └── DhKeyOption.php │ ├── DsaKey │ │ ├── DsaKeyGenerator.php │ │ └── DsaKeyOption.php │ ├── EcKey │ │ ├── EcKeyGenerator.php │ │ └── EcKeyOption.php │ ├── KeyOption.php │ ├── KeyPairGenerator.php │ ├── OpensslPrivateKeyGeneratorTrait.php │ ├── PrivateKeyGeneratorInterface.php │ └── RsaKey │ │ ├── RsaKeyGenerator.php │ │ └── RsaKeyOption.php │ ├── Key.php │ ├── KeyPair.php │ ├── ParsedCertificate.php │ ├── ParsedKey.php │ ├── Parser │ ├── CertificateParser.php │ └── KeyParser.php │ ├── PrivateKey.php │ ├── PublicKey.php │ └── Signer │ ├── CertificateRequestSigner.php │ └── DataSigner.php ├── Builders └── LetsEncryptCertificateBuilder.php ├── Collections └── LetsEncryptCertificateCollection.php ├── Commands └── LetsEncryptGenerateCommand.php ├── Contracts └── PathGenerator.php ├── Encoders └── PemEncoder.php ├── Events ├── ChallengeAuthorizationFailed.php ├── CleanUpChallengeFailed.php ├── RegisterAccountFailed.php ├── RenewExpiringCertificatesFailed.php ├── RequestAuthorizationFailed.php ├── RequestCertificateFailed.php └── StoreCertificateFailed.php ├── Exceptions ├── DomainAlreadyExists.php ├── FailedToMoveChallengeException.php ├── FailedToStoreCertificate.php ├── InvalidDomainException.php ├── InvalidKeyPairConfiguration.php └── InvalidPathGenerator.php ├── Facades └── LetsEncrypt.php ├── Interfaces └── LetsEncryptCertificateFailed.php ├── Jobs ├── ChallengeAuthorization.php ├── CleanUpChallenge.php ├── RegisterAccount.php ├── RenewExpiringCertificates.php ├── RequestAuthorization.php ├── RequestCertificate.php └── StoreCertificate.php ├── LetsEncrypt.php ├── LetsEncryptServiceProvider.php ├── Models └── LetsEncryptCertificate.php ├── PendingCertificate.php ├── Support ├── DefaultPathGenerator.php └── PathGeneratorFactory.php └── Traits └── Retryable.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | notPath('bootstrap/*') 5 | ->notPath('storage/*') 6 | ->notPath('storage/*') 7 | ->notPath('resources/view/mail/*') 8 | ->in([ 9 | __DIR__ . '/src', 10 | __DIR__ . '/tests', 11 | ]) 12 | ->name('*.php') 13 | ->notName('*.blade.php') 14 | ->ignoreDotFiles(true) 15 | ->ignoreVCS(true); 16 | 17 | $config = new PhpCsFixer\Config(); 18 | return $config 19 | ->setRules([ 20 | '@PSR2' => true, 21 | 'array_syntax' => ['syntax' => 'short'], 22 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 23 | 'no_unused_imports' => true, 24 | 'not_operator_with_successor_space' => true, 25 | 'trailing_comma_in_multiline' => true, 26 | 'phpdoc_scalar' => true, 27 | 'unary_operator_spaces' => true, 28 | 'binary_operator_spaces' => true, 29 | 'blank_line_before_statement' => [ 30 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 31 | ], 32 | 'phpdoc_single_line_var_spacing' => true, 33 | 'phpdoc_var_without_name' => true, 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ] 38 | ]) 39 | ->setFinder($finder); 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `daanra/laravel-lets-encrypt` will be documented in this file 4 | 5 | ## V0.1.0 6 | 7 | - Initial release 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Daan Raatjes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Let's Encrypt Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/daanra/laravel-lets-encrypt.svg?style=flat-square)](https://packagist.org/packages/daanra/laravel-lets-encrypt) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/daanra/laravel-lets-encrypt/run-tests?label=tests)](https://github.com/daanra/laravel-lets-encrypt/actions?query=workflow%3Arun-tests+branch%3Amaster) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/daanra/laravel-lets-encrypt.svg?style=flat-square)](https://packagist.org/packages/daanra/laravel-lets-encrypt) 6 | 7 | A Laravel package for easily generating and renewing SSL certificates using Let's Encrypt. This package is especially useful if 8 | you have a Laravel application that manages the SSL certificates of many domains. This package is **not** recommended if 9 | you just need to generate a single SSL certificate for your application. 10 | 11 | This package is essentially a Laravel-friendly wrapper around [Acme PHP](https://github.com/acmephp/acmephp). 12 | 13 | ## Installation 14 | 15 | You can install the package via composer: 16 | 17 | ```bash 18 | composer require daanra/laravel-lets-encrypt 19 | ``` 20 | 21 | If you're having installation problems with conflicting dependencies caused by Guzzle then you might want to run: 22 | ```bash 23 | composer require daanra/laravel-lets-encrypt guzzlehttp/guzzle:^6.0 -w 24 | ``` 25 | 26 | Publish the configuration file and the migration: 27 | 28 | ```bash 29 | php artisan vendor:publish --provider="Daanra\LaravelLetsEncrypt\LetsEncryptServiceProvider" --tag="lets-encrypt" 30 | ``` 31 | 32 | Run the migration: 33 | ```bash 34 | php artisan migrate 35 | ``` 36 | 37 | **Note:** 38 | 39 | You somehow have to return a stored challenge whenever it it retrieved from the `/.well-known/acme-challenge` endpoint. You could do this by configuring NGINX/Apache appropriately or by registering a route: 40 | ```php 41 | Route::get('/.well-known/acme-challenge/{token}', function (string $token) { 42 | return \Illuminate\Support\Facades\Storage::get('public/.well-known/acme-challenge/' . $token); 43 | }) 44 | ``` 45 | 46 | Sometimes the `/.well-known/` prefix is disabled by default in the NGINX/Apache config (see [#4](https://github.com/Daanra/laravel-lets-encrypt/issues/4)). Make sure it is forwarded to your Laravel application if you want Laravel to return the challenge. 47 | 48 | 49 | ## Usage 50 | 51 | Creating a new SSL certificate for a specific domain is easy: 52 | ```php 53 | // Puts several jobs on the queue to handle the communication with the lets-encrypt server 54 | [$certificate, $pendingDispatch] = \Daanra\LaravelLetsEncrypt\Facades\LetsEncrypt::create('mydomain.com'); 55 | 56 | // You could, for example, chain some jobs to enable a new virtual host 57 | // in Apache and send a notification once the website is available 58 | [$certificate, $pendingDispatch] = \Daanra\LaravelLetsEncrypt\Facades\LetsEncrypt::create('mydomain.com', [ 59 | new CreateNewApacheVirtualHost('mydomain.com'), 60 | new ReloadApache(), 61 | new NotifyUserOfNewCertificate(request()->user()), 62 | ]); 63 | 64 | // You can also do it synchronously: 65 | \Daanra\LaravelLetsEncrypt\Facades\LetsEncrypt::createNow('mydomain.com'); 66 | ``` 67 | 68 | Alternative syntax available from v0.3.0: 69 | 70 | ```php 71 | LetsEncrypt::certificate('mydomain.com') 72 | ->chain([ 73 | new SomeJob() 74 | ]) 75 | ->delay(5) 76 | ->retryAfter(4) 77 | ->setTries(4) 78 | ->setRetryList([1, 5, 10]) 79 | ->create(); // or ->renew() 80 | ``` 81 | 82 | Where you can specify values for all jobs: 83 | 84 | - tries (The number of times the job may be attempted) 85 | - retryAfter (The number of seconds to wait before retrying the job) 86 | - retryList (The list of seconds to wait before retrying the job) 87 | - chain (Chain some jobs after the certificate has successfully been obtained) 88 | - delay (Set the desired delay for the job) 89 | 90 | You could also achieve the same by using an artisan command: 91 | ```bash 92 | php artisan lets-encrypt:create -d mydomain.com 93 | ``` 94 | 95 | Certificates are stored in the database. You can query them like so: 96 | ```php 97 | // All certificates 98 | \Daanra\LaravelLetsEncrypt\Models\LetsEncryptCertificate::all(); 99 | // All expired certificates 100 | \Daanra\LaravelLetsEncrypt\Models\LetsEncryptCertificate::query()->expired()->get(); 101 | // All currently valid certificates 102 | \Daanra\LaravelLetsEncrypt\Models\LetsEncryptCertificate::query()->valid()->get(); 103 | // All certificates that should be renewed (because they're more than 60 days old) 104 | \Daanra\LaravelLetsEncrypt\Models\LetsEncryptCertificate::query()->requiresRenewal()->get(); 105 | 106 | // Find certificate by domain 107 | $certificate = LetsEncryptCertificate::where('domain', 'mydomain.com')->first(); 108 | // If you no longer need it, you can soft delete 109 | $certificate->delete(); 110 | // Or use a hard delete 111 | $certificate->forceDelete(); 112 | ``` 113 | 114 | 115 | ## Subject Alternative Names 116 | 117 | It's also possible to specify Subject Alternative Names as below (requires >= 0.5.0): 118 | 119 | ```php 120 | LetsEncrypt::certificate('mydomain.com') 121 | ->setSubjectAlternativeNames(['mydomain2.com']) 122 | ->create(); 123 | ``` 124 | 125 | ## Failure events 126 | 127 | If one of the jobs fails, one of the following events will be dispatched: 128 | ```php 129 | Daanra\LaravelLetsEncrypt\Events\CleanUpChallengeFailed 130 | Daanra\LaravelLetsEncrypt\Events\ChallengeAuthorizationFailed 131 | Daanra\LaravelLetsEncrypt\Events\RegisterAccountFailed 132 | Daanra\LaravelLetsEncrypt\Events\RequestAuthorizationFailed 133 | Daanra\LaravelLetsEncrypt\Events\RequestCertificateFailed 134 | Daanra\LaravelLetsEncrypt\Events\StoreCertificateFailed 135 | Daanra\LaravelLetsEncrypt\Events\RenewExpiringCertificatesFailed 136 | ``` 137 | 138 | Every event implements the `Daanra\LaravelLetsEncrypt\Interfaces\LetsEncryptCertificateFailed` interface so you can listen for that as well. 139 | 140 | ## Automatically renewing certificates 141 | 142 | Certificates are valid for 90 days. Before those 90 days are over, you will want to renew them. To do so, you 143 | could add the following to your `App\Console\Kernel`: 144 | ```php 145 | protected function schedule(Schedule $schedule) 146 | { 147 | $schedule->job(new \Daanra\LaravelLetsEncrypt\Jobs\RenewExpiringCertificates)->daily(); 148 | } 149 | ``` 150 | 151 | This will automatically renew every certificate that is older than 60 days, ensuring that they never expire. 152 | 153 | ## Configuration 154 | 155 | By default this package will use Let's Encrypt's staging server to issue certificates. You should set: 156 | ```bash 157 | LETS_ENCRYPT_API_URL=https://acme-v02.api.letsencrypt.org/directory 158 | ``` 159 | in the `.env` file of your production server. 160 | 161 | 162 | By default, this package will attempt to validate a certificate using [a HTTP-01 challenge](https://letsencrypt.org/docs/challenge-types/). 163 | For this reason, a file will be temporarily stored in your application's storage directory under the path 164 | `app/public/.well-known/acme-challenge/`. You can customise this behavior by setting a custom 165 | `PathGenerator` class in your config under `path_generator`. Note that Let's Encrypt expects the following path: 166 | ```bash 167 | /.well-known/acme-challenge/ 168 | ``` 169 | to return the contents of the file located at `$pathGenerator->getPath($token)`. 170 | 171 | 172 | ## Testing 173 | 174 | ``` bash 175 | composer test 176 | ``` 177 | 178 | ## Changelog 179 | 180 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 181 | 182 | ## Contributing 183 | 184 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 185 | 186 | ## Security 187 | 188 | If you discover any security related issues, please email opensource@daanraatjes.dev instead of using the issue tracker. If you have a question, please open an issue instead of sending an email. 189 | 190 | ## Credits 191 | 192 | - [Daan Raatjes](https://github.com/Daanra) 193 | - [All Contributors](../../contributors) 194 | 195 | ## License 196 | 197 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 198 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daanra/laravel-lets-encrypt", 3 | "description": "A Laravel package to easily generate SSL certificates using Let's Encrypt", 4 | "keywords": [ 5 | "daanra", 6 | "lets", 7 | "encrypt", 8 | "ssl", 9 | "certificate", 10 | "laravel" 11 | ], 12 | "homepage": "https://github.com/daanra/laravel-lets-encrypt", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Daan Raatjes", 17 | "email": "daanraatjes+dev@gmail.com" 18 | } 19 | ], 20 | "require": { 21 | "php": "^7.2|^8.0", 22 | "ext-openssl": "*", 23 | "guzzlehttp/guzzle": "^7.4", 24 | "illuminate/console": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 25 | "illuminate/filesystem": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 26 | "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0" 27 | }, 28 | "require-dev": { 29 | "friendsofphp/php-cs-fixer": "^3.0", 30 | "orchestra/testbench": "^7.0|^8.0|^9.0", 31 | "phpunit/phpunit": "^10.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Daanra\\LaravelLetsEncrypt\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Daanra\\LaravelLetsEncrypt\\Tests\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "vendor/bin/phpunit", 45 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 46 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 47 | }, 48 | "config": { 49 | "sort-packages": true 50 | }, 51 | "extra": { 52 | "laravel": { 53 | "providers": [ 54 | "Daanra\\LaravelLetsEncrypt\\LetsEncryptServiceProvider" 55 | ], 56 | "aliases": { 57 | "LetsEncrypt": "Daanra\\LaravelLetsEncrypt\\Facades\\LetsEncrypt" 58 | } 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /config/lets_encrypt.php: -------------------------------------------------------------------------------- 1 | env('LETS_ENCRYPT_API_URL', 'https://acme-staging-v02.api.letsencrypt.org/directory'), 9 | 10 | // The path to the public ssl key used for connecting with the let's encrypt API. 11 | // A fresh key will be generated if it does not exist yet. 12 | 'public_key_path' => env('LETS_ENCRYPT_PUBLIC_KEY_PATH', storage_path('app/letsencrypt/keys/account.pub.pem')), 13 | 14 | // The path to the private ssl key used for connecting with the let's encrypt API. 15 | // A fresh key will be generated if it does not exist yet. 16 | 'private_key_path' => env('LETS_ENCRYPT_PRIVATE_KEY_PATH', storage_path('app/letsencrypt/keys/account.pem')), 17 | 18 | // Universal email address, every certificate will be issued using this email address by default. 19 | // Only useful if you want to receive emails about expiring certificates 20 | 'universal_email_address' => env('LETS_ENCRYPT_UNIVERSAL_EMAIL_ADDRESS', null), 21 | 22 | 'path_generator' => DefaultPathGenerator::class, 23 | 24 | // The disk to store the certificates on. Use null for the default disk. 25 | 'certificate_disk' => null, 26 | 27 | // The disk to store store challenges on. Use null for the default disk. 28 | 'challenge_disk' => null, 29 | ]; 30 | -------------------------------------------------------------------------------- /database/migrations/add_lets_encrypt_certificates_subject_alternative_names.php.stub: -------------------------------------------------------------------------------- 1 | json('subject_alternative_names')->default(new Expression('(JSON_ARRAY())'))->after('domain'); 14 | }); 15 | } 16 | 17 | public function down() 18 | { 19 | Schema::table('lets_encrypt_certificates', function (Blueprint $table) { 20 | $table->dropColumn('subject_alternative_names'); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/migrations/create_lets_encrypt_certificates_table.php.stub: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->timestamps(); 14 | $table->softDeletes(); 15 | $table->string('domain'); 16 | $table->timestamp('last_renewed_at')->nullable(); 17 | $table->boolean('created')->default(false); 18 | $table->string('fullchain_path')->nullable(); 19 | $table->string('chain_path')->nullable(); 20 | $table->string('cert_path')->nullable(); 21 | $table->string('privkey_path')->nullable(); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('lets_encrypt_certificates'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/AcmeClientInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreClientException; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol\CertificateRequestFailedException; 17 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol\CertificateRequestTimedOutException; 18 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol\CertificateRevocationException; 19 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol\ChallengeFailedException; 20 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException; 21 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol\ChallengeTimedOutException; 22 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Http\SecureHttpClient; 23 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Protocol\AuthorizationChallenge; 24 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Protocol\RevocationReason; 25 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Certificate; 26 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\CertificateRequest; 27 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\CertificateResponse; 28 | 29 | /** 30 | * ACME protocol client interface. 31 | * 32 | * @author Titouan Galopin 33 | */ 34 | interface AcmeClientInterface 35 | { 36 | /** 37 | * Register the local account KeyPair in the Certificate Authority. 38 | * 39 | * @param string|null $agreement an optionnal URI referring to a subscriber agreement or terms of service 40 | * @param string|null $email an optionnal e-mail to associate with the account 41 | * 42 | * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code 43 | * (the exception will be more specific if detail is provided) 44 | * @throws AcmeCoreClientException when an error occured during response parsing 45 | * 46 | * @return array the Certificate Authority response decoded from JSON into an array 47 | */ 48 | public function registerAccount($agreement = null, $email = null); 49 | 50 | /** 51 | * Request authorization challenge data for a given domain. 52 | * 53 | * An AuthorizationChallenge is an association between a URI, a token and a payload. 54 | * The Certificate Authority will create this challenge data and you will then have 55 | * to expose the payload for the verification (see challengeAuthorization). 56 | * 57 | * @param string $domain the domain to challenge 58 | * 59 | * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code 60 | * (the exception will be more specific if detail is provided) 61 | * @throws AcmeCoreClientException when an error occured during response parsing 62 | * @throws ChallengeNotSupportedException when the HTTP challenge is not supported by the server 63 | * 64 | * @return AuthorizationChallenge[] the list of challenges data returned by the Certificate Authority 65 | */ 66 | public function requestAuthorization($domain); 67 | 68 | /** 69 | * Ask the Certificate Authority to challenge a given authorization. 70 | * 71 | * This check will generally consists of requesting over HTTP the domain 72 | * at a specific URL. This URL should return the raw payload generated 73 | * by requestAuthorization. 74 | * 75 | * WARNING : This method SHOULD NOT BE USED in a web action. It will 76 | * wait for the Certificate Authority to validate the challenge and this 77 | * operation could be long. 78 | * 79 | * @param AuthorizationChallenge $challenge the challenge data to check 80 | * @param int $timeout the timeout period 81 | * 82 | * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code 83 | * (the exception will be more specific if detail is provided) 84 | * @throws AcmeCoreClientException when an error occured during response parsing 85 | * @throws ChallengeTimedOutException when the challenge timed out 86 | * @throws ChallengeFailedException when the challenge failed 87 | * 88 | * @return array the validate challenge response 89 | */ 90 | public function challengeAuthorization(AuthorizationChallenge $challenge, $timeout = 180); 91 | 92 | /** 93 | * Request a certificate for the given domain. 94 | * 95 | * This method should be called only if a previous authorization challenge has 96 | * been successful for the asked domain. 97 | * 98 | * WARNING : This method SHOULD NOT BE USED in a web action. It will 99 | * wait for the Certificate Authority to validate the certificate and 100 | * this operation could be long. 101 | * 102 | * @param string $domain the domain to request a certificate for 103 | * @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate) 104 | * @param int $timeout the timeout period 105 | * 106 | * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code 107 | * (the exception will be more specific if detail is provided) 108 | * @throws AcmeCoreClientException when an error occured during response parsing 109 | * @throws CertificateRequestFailedException when the certificate request failed 110 | * @throws CertificateRequestTimedOutException when the certificate request timed out 111 | * 112 | * @return CertificateResponse the certificate data to save it somewhere you want 113 | */ 114 | public function requestCertificate($domain, CertificateRequest $csr, $timeout = 180); 115 | 116 | /** 117 | * @throws CertificateRevocationException 118 | */ 119 | public function revokeCertificate(Certificate $certificate, RevocationReason $revocationReason = null); 120 | 121 | /** 122 | * Get the HTTP client. 123 | * 124 | * @return SecureHttpClient 125 | */ 126 | public function getHttpClient(); 127 | } 128 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/AcmeClientV2Interface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreClientException; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol\CertificateRequestFailedException; 17 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol\CertificateRequestTimedOutException; 18 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException; 19 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Protocol\AuthorizationChallenge; 20 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Protocol\CertificateOrder; 21 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\CertificateRequest; 22 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\CertificateResponse; 23 | 24 | /** 25 | * ACME protocol client interface. 26 | * 27 | * @author Titouan Galopin 28 | */ 29 | interface AcmeClientV2Interface extends AcmeClientInterface 30 | { 31 | /** 32 | * Request authorization challenge data for a list of domains. 33 | * 34 | * An AuthorizationChallenge is an association between a URI, a token and a payload. 35 | * The Certificate Authority will create this challenge data and you will then have 36 | * to expose the payload for the verification (see challengeAuthorization). 37 | * 38 | * @param string[] $domains the domains to challenge 39 | * 40 | * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code 41 | * (the exception will be more specific if detail is provided) 42 | * @throws AcmeCoreClientException when an error occured during response parsing 43 | * @throws ChallengeNotSupportedException when the HTTP challenge is not supported by the server 44 | * 45 | * @return CertificateOrder the Order returned by the Certificate Authority 46 | */ 47 | public function requestOrder(array $domains); 48 | 49 | /** 50 | * Request a certificate for the given domain. 51 | * 52 | * This method should be called only if a previous authorization challenge has 53 | * been successful for the asked domain. 54 | * 55 | * WARNING : This method SHOULD NOT BE USED in a web action. It will 56 | * wait for the Certificate Authority to validate the certificate and 57 | * this operation could be long. 58 | * 59 | * @param CertificateOrder $order the Order returned by the Certificate Authority 60 | * @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate) 61 | * @param int $timeout the timeout period 62 | * 63 | * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code 64 | * (the exception will be more specific if detail is provided) 65 | * @throws AcmeCoreClientException when an error occured during response parsing 66 | * @throws CertificateRequestFailedException when the certificate request failed 67 | * @throws CertificateRequestTimedOutException when the certificate request timed out 68 | * 69 | * @return CertificateResponse the certificate data to save it somewhere you want 70 | */ 71 | public function finalizeOrder(CertificateOrder $order, CertificateRequest $csr, $timeout = 180); 72 | 73 | /** 74 | * Request the current status of an authorization challenge. 75 | * 76 | * @param AuthorizationChallenge $challenge The challenge to request 77 | * 78 | * @return AuthorizationChallenge A new instance of the challenge 79 | */ 80 | public function reloadAuthorization(AuthorizationChallenge $challenge); 81 | } 82 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/AcmeCoreClientException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception; 13 | 14 | /** 15 | * Error reported by the client. 16 | * 17 | * @author Titouan Galopin 18 | */ 19 | class AcmeCoreClientException extends AcmeCoreException 20 | { 21 | public function __construct($message, \Exception $previous = null) 22 | { 23 | parent::__construct($message, 0, $previous); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/AcmeCoreException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class AcmeCoreException extends \RuntimeException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/AcmeCoreServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception; 13 | 14 | use Psr\Http\Message\RequestInterface; 15 | 16 | /** 17 | * Error reported by the server. 18 | * 19 | * @author Titouan Galopin 20 | */ 21 | class AcmeCoreServerException extends AcmeCoreException 22 | { 23 | public function __construct(RequestInterface $request, $message, \Exception $previous = null) 24 | { 25 | parent::__construct($message, $previous ? $previous->getCode() : 0, $previous); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/AcmeDnsResolutionException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | */ 17 | class AcmeDnsResolutionException extends AcmeCoreException 18 | { 19 | public function __construct($message, \Exception $previous = null) 20 | { 21 | parent::__construct(null === $message ? 'An exception was thrown during resolution of DNS' : $message, 0, $previous); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Protocol/CertificateRequestFailedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class CertificateRequestFailedException extends ProtocolException 18 | { 19 | public function __construct($response) 20 | { 21 | parent::__construct(sprintf('Certificate request failed (response: %s)', $response)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Protocol/CertificateRequestTimedOutException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class CertificateRequestTimedOutException extends ProtocolException 18 | { 19 | public function __construct($response) 20 | { 21 | parent::__construct(sprintf('Certificate request timed out (response: %s)', $response)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Protocol/CertificateRevocationException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreClientException; 15 | 16 | class CertificateRevocationException extends AcmeCoreClientException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Protocol/ChallengeFailedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class ChallengeFailedException extends ProtocolException 18 | { 19 | private $response; 20 | 21 | public function __construct($response, \Exception $previous = null) 22 | { 23 | parent::__construct( 24 | sprintf('Challenge failed (response: %s).', json_encode($response)), 25 | $previous 26 | ); 27 | 28 | $this->response = $response; 29 | } 30 | 31 | /** 32 | * @return array 33 | */ 34 | public function getResponse() 35 | { 36 | return $this->response; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Protocol/ChallengeNotSupportedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class ChallengeNotSupportedException extends ProtocolException 18 | { 19 | public function __construct(\Exception $previous = null) 20 | { 21 | parent::__construct('This ACME server does not expose supported challenge.', $previous); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Protocol/ChallengeTimedOutException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class ChallengeTimedOutException extends ProtocolException 18 | { 19 | private $response; 20 | 21 | public function __construct($response, \Exception $previous = null) 22 | { 23 | parent::__construct( 24 | sprintf('Challenge timed out (response: %s).', json_encode($response)), 25 | $previous 26 | ); 27 | 28 | $this->response = $response; 29 | } 30 | 31 | /** 32 | * @return array 33 | */ 34 | public function getResponse() 35 | { 36 | return $this->response; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Protocol/ExpectedJsonException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class ExpectedJsonException extends ProtocolException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Protocol/ProtocolException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Protocol; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreClientException; 15 | 16 | /** 17 | * Error because the protocol was not respected. 18 | * 19 | * @author Titouan Galopin 20 | */ 21 | class ProtocolException extends AcmeCoreClientException 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/BadCsrServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class BadCsrServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[badCSR] The CSR is unacceptable (e.g., due to a short key): '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/BadNonceServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class BadNonceServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[badNonce] The client sent an unacceptable anti-replay nonce: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/CaaServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Alex Plekhanov 19 | */ 20 | class CaaServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[caa] Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/ConnectionServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class ConnectionServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[connection] The server could not connect to the client for DV: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/DnsServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Alex Plekhanov 19 | */ 20 | class DnsServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[dns] There was a problem with a DNS query during identifier validation: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/IncorrectResponseServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Alex Plekhanov 19 | */ 20 | class IncorrectResponseServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | "[incorrectResponse] Response received didn’t match the challenge's requirements: ".$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/InternalServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class InternalServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[serverInternal] The server experienced an internal error: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/InvalidContactServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Alex Plekhanov 19 | */ 20 | class InvalidContactServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[invalidContact] A contact URL for an account was invalid: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/InvalidEmailServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class InvalidEmailServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[invalidEmail] This email is unacceptable (e.g., it is invalid): '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/MalformedServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class MalformedServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[malformed] The request message was malformed: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/OrderNotReadyServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | class OrderNotReadyServerException extends AcmeCoreServerException 18 | { 19 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 20 | { 21 | parent::__construct( 22 | $request, 23 | '[orderNotReady] Order could not be finalized: '.$detail, 24 | $previous 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/RateLimitedServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class RateLimitedServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[rateLimited] This client reached the rate limit of the server: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/RejectedIdentifierServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Alex Plekhanov 19 | */ 20 | class RejectedIdentifierServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[rejectedIdentifier] The server will not issue certificates for the identifier: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/TlsServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class TlsServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[tls] The server experienced a TLS error during DV: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/UnauthorizedServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class UnauthorizedServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[unauthorized] The client lacks sufficient authorization: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/UnknownHostServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Titouan Galopin 19 | */ 20 | class UnknownHostServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[unknownHost] The server could not resolve a domain name: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/UnsupportedContactServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Alex Plekhanov 19 | */ 20 | class UnsupportedContactServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[unsupportedContact] A contact URL for an account used an unsupported protocol scheme: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/UnsupportedIdentifierServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Alex Plekhanov 19 | */ 20 | class UnsupportedIdentifierServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[unsupportedIdentifier] An identifier is of an unsupported type: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Exception/Server/UserActionRequiredServerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Psr\Http\Message\RequestInterface; 16 | 17 | /** 18 | * @author Alex Plekhanov 19 | */ 20 | class UserActionRequiredServerException extends AcmeCoreServerException 21 | { 22 | public function __construct(RequestInterface $request, $detail, \Exception $previous = null) 23 | { 24 | parent::__construct( 25 | $request, 26 | '[userActionRequired] Visit the “instance” URL and take actions specified there: '.$detail, 27 | $previous 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Http/Base64SafeEncoder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Http; 13 | 14 | /** 15 | * Encode and decode safely in base64. 16 | * 17 | * @author Titouan Galopin 18 | */ 19 | class Base64SafeEncoder 20 | { 21 | /** 22 | * @param string $input 23 | * 24 | * @return string 25 | */ 26 | public function encode($input) 27 | { 28 | return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); 29 | } 30 | 31 | /** 32 | * @param string $input 33 | * 34 | * @return string 35 | */ 36 | public function decode($input) 37 | { 38 | $remainder = \strlen($input) % 4; 39 | 40 | if ($remainder) { 41 | $padlen = 4 - $remainder; 42 | $input .= str_repeat('=', $padlen); 43 | } 44 | 45 | return base64_decode(strtr($input, '-_', '+/')); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Http/SecureHttpClientFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Http; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\KeyPair; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Parser\KeyParser; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Signer\DataSigner; 17 | use GuzzleHttp\ClientInterface; 18 | 19 | /** 20 | * Guzzle HTTP client wrapper to send requests signed with the account KeyPair. 21 | * 22 | * @author Titouan Galopin 23 | */ 24 | class SecureHttpClientFactory 25 | { 26 | /** 27 | * @var ClientInterface 28 | */ 29 | private $httpClient; 30 | 31 | /** 32 | * @var Base64SafeEncoder 33 | */ 34 | private $base64Encoder; 35 | 36 | /** 37 | * @var KeyParser 38 | */ 39 | private $keyParser; 40 | 41 | /** 42 | * @var DataSigner 43 | */ 44 | private $dataSigner; 45 | 46 | /** 47 | * @var ServerErrorHandler 48 | */ 49 | private $errorHandler; 50 | 51 | public function __construct( 52 | ClientInterface $httpClient, 53 | Base64SafeEncoder $base64Encoder, 54 | KeyParser $keyParser, 55 | DataSigner $dataSigner, 56 | ServerErrorHandler $errorHandler 57 | ) { 58 | $this->httpClient = $httpClient; 59 | $this->base64Encoder = $base64Encoder; 60 | $this->keyParser = $keyParser; 61 | $this->dataSigner = $dataSigner; 62 | $this->errorHandler = $errorHandler; 63 | } 64 | 65 | /** 66 | * Create a SecureHttpClient using a given account KeyPair. 67 | * 68 | * @return SecureHttpClient 69 | */ 70 | public function createSecureHttpClient(KeyPair $accountKeyPair) 71 | { 72 | return new SecureHttpClient( 73 | $accountKeyPair, 74 | $this->httpClient, 75 | $this->base64Encoder, 76 | $this->keyParser, 77 | $this->dataSigner, 78 | $this->errorHandler 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Http/ServerErrorHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Http; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreServerException; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\BadCsrServerException; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\BadNonceServerException; 17 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\CaaServerException; 18 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\ConnectionServerException; 19 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\DnsServerException; 20 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\IncorrectResponseServerException; 21 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\InternalServerException; 22 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\InvalidContactServerException; 23 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\InvalidEmailServerException; 24 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\MalformedServerException; 25 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\OrderNotReadyServerException; 26 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\RateLimitedServerException; 27 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\RejectedIdentifierServerException; 28 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\TlsServerException; 29 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\UnauthorizedServerException; 30 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\UnknownHostServerException; 31 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\UnsupportedContactServerException; 32 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\UnsupportedIdentifierServerException; 33 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\Server\UserActionRequiredServerException; 34 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Util\JsonDecoder; 35 | use GuzzleHttp\Exception\RequestException; 36 | use GuzzleHttp\Psr7\Utils; 37 | use Psr\Http\Message\RequestInterface; 38 | use Psr\Http\Message\ResponseInterface; 39 | 40 | /** 41 | * Create appropriate exception for given server response. 42 | * 43 | * @author Titouan Galopin 44 | */ 45 | class ServerErrorHandler 46 | { 47 | private static $exceptions = [ 48 | 'badCSR' => BadCsrServerException::class, 49 | 'badNonce' => BadNonceServerException::class, 50 | 'caa' => CaaServerException::class, 51 | 'connection' => ConnectionServerException::class, 52 | 'dns' => DnsServerException::class, 53 | 'incorrectResponse' => IncorrectResponseServerException::class, 54 | 'invalidContact' => InvalidContactServerException::class, 55 | 'invalidEmail' => InvalidEmailServerException::class, 56 | 'malformed' => MalformedServerException::class, 57 | 'orderNotReady' => OrderNotReadyServerException::class, 58 | 'rateLimited' => RateLimitedServerException::class, 59 | 'rejectedIdentifier' => RejectedIdentifierServerException::class, 60 | 'serverInternal' => InternalServerException::class, 61 | 'tls' => TlsServerException::class, 62 | 'unauthorized' => UnauthorizedServerException::class, 63 | 'unknownHost' => UnknownHostServerException::class, 64 | 'unsupportedContact' => UnsupportedContactServerException::class, 65 | 'unsupportedIdentifier' => UnsupportedIdentifierServerException::class, 66 | 'userActionRequired' => UserActionRequiredServerException::class, 67 | ]; 68 | 69 | /** 70 | * Get a response summary (useful for exceptions). 71 | * Use Guzzle method if available (Guzzle 6.1.1+). 72 | * 73 | * @return string 74 | */ 75 | public static function getResponseBodySummary(ResponseInterface $response) 76 | { 77 | // Rewind the stream if possible to allow re-reading for the summary. 78 | if ($response->getBody()->isSeekable()) { 79 | $response->getBody()->rewind(); 80 | } 81 | 82 | if (method_exists(RequestException::class, 'getResponseBodySummary')) { 83 | return RequestException::getResponseBodySummary($response); 84 | } 85 | 86 | $body = Utils::copyToString($response->getBody()); 87 | 88 | if (\strlen($body) > 120) { 89 | return substr($body, 0, 120).' (truncated...)'; 90 | } 91 | 92 | return $body; 93 | } 94 | 95 | /** 96 | * @return AcmeCoreServerException 97 | */ 98 | public function createAcmeExceptionForResponse( 99 | RequestInterface $request, 100 | ResponseInterface $response, 101 | \Exception $previous = null 102 | ) { 103 | $body = Utils::copyToString($response->getBody()); 104 | 105 | try { 106 | $data = JsonDecoder::decode($body, true); 107 | } catch (\InvalidArgumentException $e) { 108 | $data = null; 109 | } 110 | 111 | if (! $data || ! isset($data['type'], $data['detail'])) { 112 | // Not JSON: not an ACME error response 113 | return $this->createDefaultExceptionForResponse($request, $response, $previous); 114 | } 115 | 116 | $type = preg_replace('/^urn:(ietf:params:)?acme:error:/i', '', $data['type']); 117 | 118 | if (! isset(self::$exceptions[$type])) { 119 | // Unknown type: not an ACME error response 120 | return $this->createDefaultExceptionForResponse($request, $response, $previous); 121 | } 122 | 123 | $exceptionClass = self::$exceptions[$type]; 124 | 125 | return new $exceptionClass( 126 | $request, 127 | sprintf('%s (on request "%s %s")', $data['detail'], $request->getMethod(), $request->getUri()), 128 | $previous 129 | ); 130 | } 131 | 132 | /** 133 | * @return AcmeCoreServerException 134 | */ 135 | private function createDefaultExceptionForResponse( 136 | RequestInterface $request, 137 | ResponseInterface $response, 138 | \Exception $previous = null 139 | ) { 140 | return new AcmeCoreServerException( 141 | $request, 142 | sprintf( 143 | 'A non-ACME %s HTTP error occured on request "%s %s" (response body: "%s")', 144 | $response->getStatusCode(), 145 | $request->getMethod(), 146 | $request->getUri(), 147 | self::getResponseBodySummary($response) 148 | ), 149 | $previous 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Protocol/AuthorizationChallenge.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Protocol; 13 | 14 | use Webmozart\Assert\Assert; 15 | 16 | /** 17 | * Represent a ACME challenge. 18 | * 19 | * @author Titouan Galopin 20 | */ 21 | class AuthorizationChallenge 22 | { 23 | /** 24 | * @var string 25 | */ 26 | private $domain; 27 | 28 | /** 29 | * @var string 30 | */ 31 | private $status; 32 | 33 | /** 34 | * @var string 35 | */ 36 | private $type; 37 | 38 | /** 39 | * @var string 40 | */ 41 | private $url; 42 | 43 | /** 44 | * @var string 45 | */ 46 | private $token; 47 | 48 | /** 49 | * @var string 50 | */ 51 | private $payload; 52 | 53 | /** 54 | * @param string $domain 55 | * @param string $status 56 | * @param string $type 57 | * @param string $url 58 | * @param string $token 59 | * @param string $payload 60 | */ 61 | public function __construct($domain, $status, $type, $url, $token, $payload) 62 | { 63 | Assert::stringNotEmpty($domain, 'Challenge::$domain expected a non-empty string. Got: %s'); 64 | Assert::stringNotEmpty($status, 'Challenge::$status expected a non-empty string. Got: %s'); 65 | Assert::stringNotEmpty($type, 'Challenge::$type expected a non-empty string. Got: %s'); 66 | Assert::stringNotEmpty($url, 'Challenge::$url expected a non-empty string. Got: %s'); 67 | Assert::stringNotEmpty($token, 'Challenge::$token expected a non-empty string. Got: %s'); 68 | Assert::stringNotEmpty($payload, 'Challenge::$payload expected a non-empty string. Got: %s'); 69 | 70 | $this->domain = $domain; 71 | $this->status = $status; 72 | $this->type = $type; 73 | $this->url = $url; 74 | $this->token = $token; 75 | $this->payload = $payload; 76 | } 77 | 78 | /** 79 | * @return array 80 | */ 81 | public function toArray() 82 | { 83 | return [ 84 | 'domain' => $this->getDomain(), 85 | 'status' => $this->getStatus(), 86 | 'type' => $this->getType(), 87 | 'url' => $this->getUrl(), 88 | 'token' => $this->getToken(), 89 | 'payload' => $this->getPayload(), 90 | ]; 91 | } 92 | 93 | /** 94 | * @return AuthorizationChallenge 95 | */ 96 | public static function fromArray(array $data) 97 | { 98 | return new self( 99 | $data['domain'], 100 | $data['status'], 101 | $data['type'], 102 | $data['url'], 103 | $data['token'], 104 | $data['payload'] 105 | ); 106 | } 107 | 108 | /** 109 | * @return string 110 | */ 111 | public function getDomain() 112 | { 113 | return $this->domain; 114 | } 115 | 116 | /** 117 | * @return string 118 | */ 119 | public function getStatus() 120 | { 121 | return $this->status; 122 | } 123 | 124 | /** 125 | * @return bool 126 | */ 127 | public function isValid() 128 | { 129 | return 'valid' === $this->status; 130 | } 131 | 132 | /** 133 | * @return bool 134 | */ 135 | public function isPending() 136 | { 137 | return 'pending' === $this->status || 'processing' === $this->status; 138 | } 139 | 140 | /** 141 | * @return string 142 | */ 143 | public function getType() 144 | { 145 | return $this->type; 146 | } 147 | 148 | /** 149 | * @return string 150 | */ 151 | public function getUrl() 152 | { 153 | return $this->url; 154 | } 155 | 156 | /** 157 | * @return string 158 | */ 159 | public function getToken() 160 | { 161 | return $this->token; 162 | } 163 | 164 | /** 165 | * @return string 166 | */ 167 | public function getPayload() 168 | { 169 | return $this->payload; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Protocol/CertificateOrder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Protocol; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Core\Exception\AcmeCoreClientException; 15 | use Webmozart\Assert\Assert; 16 | 17 | /** 18 | * Represent an ACME order. 19 | * 20 | * @author Jérémy Derussé 21 | */ 22 | class CertificateOrder 23 | { 24 | /** 25 | * @var AuthorizationChallenge[][] 26 | */ 27 | private $authorizationsChallenges; 28 | 29 | /** 30 | * @var string 31 | */ 32 | private $orderEndpoint; 33 | 34 | /** 35 | * @param string $domain 36 | * @param string $type 37 | * @param string $url 38 | * @param string $token 39 | * @param string $payload 40 | * @param string $order 41 | */ 42 | public function __construct($authorizationsChallenges, $orderEndpoint = null) 43 | { 44 | Assert::isArray($authorizationsChallenges, 'Challenge::$authorizationsChallenges expected an array. Got: %s'); 45 | Assert::nullOrString($orderEndpoint, 'Challenge::$orderEndpoint expected a string or null. Got: %s'); 46 | 47 | foreach ($authorizationsChallenges as &$authorizationChallenges) { 48 | foreach ($authorizationChallenges as &$authorizationChallenge) { 49 | if (\is_array($authorizationChallenge)) { 50 | $authorizationChallenge = AuthorizationChallenge::fromArray($authorizationChallenge); 51 | } 52 | } 53 | } 54 | 55 | $this->authorizationsChallenges = $authorizationsChallenges; 56 | $this->orderEndpoint = $orderEndpoint; 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function toArray() 63 | { 64 | return [ 65 | 'authorizationsChallenges' => $this->getAuthorizationsChallenges(), 66 | 'orderEndpoint' => $this->getOrderEndpoint(), 67 | ]; 68 | } 69 | 70 | /** 71 | * @return AuthorizationChallenge 72 | */ 73 | public static function fromArray(array $data) 74 | { 75 | return new self( 76 | $data['authorizationsChallenges'], 77 | $data['orderEndpoint'] 78 | ); 79 | } 80 | 81 | /** 82 | * @return AuthorizationChallenge[][] 83 | */ 84 | public function getAuthorizationsChallenges() 85 | { 86 | return $this->authorizationsChallenges; 87 | } 88 | 89 | /** 90 | * @param string $domain 91 | * 92 | * @return AuthorizationChallenge[] 93 | */ 94 | public function getAuthorizationChallenges($domain) 95 | { 96 | if (! isset($this->authorizationsChallenges[$domain])) { 97 | throw new AcmeCoreClientException('The order does not contains any authorization challenge for the domain '.$domain); 98 | } 99 | 100 | return $this->authorizationsChallenges[$domain]; 101 | } 102 | 103 | /** 104 | * @return string 105 | */ 106 | public function getOrderEndpoint() 107 | { 108 | return $this->orderEndpoint; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Protocol/ResourcesDirectory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Protocol; 13 | 14 | use Webmozart\Assert\Assert; 15 | 16 | /** 17 | * Represent a ACME resources directory. 18 | * 19 | * @author Titouan Galopin 20 | */ 21 | class ResourcesDirectory 22 | { 23 | const NEW_ACCOUNT = 'newAccount'; 24 | const NEW_ORDER = 'newOrder'; 25 | const NEW_NONCE = 'newNonce'; 26 | const REVOKE_CERT = 'revokeCert'; 27 | 28 | /** 29 | * @var array 30 | */ 31 | private $serverResources; 32 | 33 | public function __construct(array $serverResources) 34 | { 35 | $this->serverResources = $serverResources; 36 | } 37 | 38 | /** 39 | * @return string[] 40 | */ 41 | public static function getResourcesNames() 42 | { 43 | return [ 44 | self::NEW_ACCOUNT, 45 | self::NEW_ORDER, 46 | self::NEW_NONCE, 47 | self::REVOKE_CERT, 48 | ]; 49 | } 50 | 51 | /** 52 | * Find a resource URL. 53 | * 54 | * @param string $resource 55 | * 56 | * @return string 57 | */ 58 | public function getResourceUrl($resource) 59 | { 60 | Assert::oneOf( 61 | $resource, 62 | self::getResourcesNames(), 63 | 'Resource type "%s" is not supported by the ACME server (supported: %2$s)' 64 | ); 65 | 66 | return isset($this->serverResources[$resource]) ? $this->serverResources[$resource] : null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Protocol/RevocationReason.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Protocol; 13 | 14 | use Webmozart\Assert\Assert; 15 | 16 | /** 17 | * @url https://github.com/certbot/certbot/blob/c326c021082dede7c3b2bd411cec3aec6dff0ac5/certbot/constants.py#L124 18 | */ 19 | class RevocationReason 20 | { 21 | const DEFAULT_REASON = self::REASON_UNSPECIFIED; 22 | const REASON_UNSPECIFIED = 0; 23 | const REASON_KEY_COMPROMISE = 1; 24 | const REASON_AFFILLIATION_CHANGED = 3; 25 | const REASON_SUPERCEDED = 4; 26 | const REASON_CESSATION_OF_OPERATION = 5; 27 | 28 | /** 29 | * @var int|null 30 | */ 31 | private $reasonType = null; 32 | 33 | /** 34 | * @param int $reasonType 35 | * 36 | * @throws \InvalidArgumentException 37 | */ 38 | public function __construct($reasonType) 39 | { 40 | $reasonType = (int) $reasonType; 41 | 42 | Assert::oneOf($reasonType, self::getReasons(), 'Revocation reason type "%s" is not supported by the ACME server (supported: %2$s)'); 43 | 44 | $this->reasonType = $reasonType; 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function getReasonType() 51 | { 52 | return $this->reasonType; 53 | } 54 | 55 | /** 56 | * @return static 57 | */ 58 | public static function createDefaultReason() 59 | { 60 | return new static(self::DEFAULT_REASON); 61 | } 62 | 63 | /** 64 | * @return array 65 | */ 66 | public static function getFormattedReasons() 67 | { 68 | $formatted = []; 69 | 70 | foreach (self::getReasonLabelMap() as $reason => $label) { 71 | $formatted[] = $reason.' - '.$label; 72 | } 73 | 74 | return $formatted; 75 | } 76 | 77 | /** 78 | * @return array 79 | */ 80 | private static function getReasonLabelMap() 81 | { 82 | return [ 83 | self::REASON_UNSPECIFIED => 'unspecified', 84 | self::REASON_KEY_COMPROMISE => 'key compromise', 85 | self::REASON_AFFILLIATION_CHANGED => 'affiliation changed', 86 | self::REASON_SUPERCEDED => 'superceded', 87 | self::REASON_CESSATION_OF_OPERATION => 'cessation of operation', 88 | ]; 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | public static function getReasons() 95 | { 96 | return [ 97 | self::REASON_UNSPECIFIED, 98 | self::REASON_KEY_COMPROMISE, 99 | self::REASON_AFFILLIATION_CHANGED, 100 | self::REASON_SUPERCEDED, 101 | self::REASON_CESSATION_OF_OPERATION, 102 | ]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/AcmePhp/Core/Util/JsonDecoder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Core\Util; 13 | 14 | /** 15 | * Guzzle HTTP client wrapper to send requests signed with the account KeyPair. 16 | * 17 | * @author Titouan Galopin 18 | * 19 | * @internal 20 | */ 21 | class JsonDecoder 22 | { 23 | /** 24 | * Wrapper for json_decode that throws when an error occurs. 25 | * Extracted from Guzzle for BC. 26 | * 27 | * @param string $json JSON data to parse 28 | * @param bool $assoc when true, returned objects will be converted 29 | * into associative arrays 30 | * @param int $depth user specified recursion depth 31 | * @param int $options bitmask of JSON decode options 32 | * 33 | * @throws \InvalidArgumentException if the JSON cannot be decoded 34 | * 35 | * @return mixed 36 | * 37 | * @see http://www.php.net/manual/en/function.json-decode.php 38 | */ 39 | public static function decode($json, $assoc = false, $depth = 512, $options = 0) 40 | { 41 | $data = json_decode($json, $assoc, $depth, $options); 42 | 43 | if (JSON_ERROR_NONE !== json_last_error()) { 44 | throw new \InvalidArgumentException('json_decode error: '.json_last_error_msg()); 45 | } 46 | 47 | return $data; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Certificate.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\CertificateFormatException; 15 | use Webmozart\Assert\Assert; 16 | 17 | /** 18 | * Represent a Certificate. 19 | * 20 | * @author Jérémy Derussé 21 | */ 22 | class Certificate 23 | { 24 | /** @var string */ 25 | private $certificatePEM; 26 | 27 | /** @var Certificate */ 28 | private $issuerCertificate; 29 | 30 | /** 31 | * @param string $certificatePEM 32 | * @param Certificate|null $issuerCertificate 33 | */ 34 | public function __construct($certificatePEM, self $issuerCertificate = null) 35 | { 36 | Assert::stringNotEmpty($certificatePEM, __CLASS__.'::$certificatePEM should not be an empty string. Got %s'); 37 | 38 | $this->certificatePEM = $certificatePEM; 39 | $this->issuerCertificate = $issuerCertificate; 40 | } 41 | 42 | /** 43 | * @return Certificate[] 44 | */ 45 | public function getIssuerChain() 46 | { 47 | $chain = []; 48 | $issuerCertificate = $this->getIssuerCertificate(); 49 | 50 | while (null !== $issuerCertificate) { 51 | $chain[] = $issuerCertificate; 52 | $issuerCertificate = $issuerCertificate->getIssuerCertificate(); 53 | } 54 | 55 | return $chain; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getPEM() 62 | { 63 | return $this->certificatePEM; 64 | } 65 | 66 | /** 67 | * @return Certificate|null 68 | */ 69 | public function getIssuerCertificate() 70 | { 71 | return $this->issuerCertificate; 72 | } 73 | 74 | /** 75 | * @return resource 76 | */ 77 | public function getPublicKeyResource() 78 | { 79 | if (! $resource = openssl_pkey_get_public($this->certificatePEM)) { 80 | throw new CertificateFormatException(sprintf('Failed to convert certificate into public key resource: %s', openssl_error_string())); 81 | } 82 | 83 | return $resource; 84 | } 85 | 86 | /** 87 | * @return PublicKey 88 | */ 89 | public function getPublicKey() 90 | { 91 | return new PublicKey(openssl_pkey_get_details($this->getPublicKeyResource())['key']); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/CertificateRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | /** 15 | * Contains data required to request a certificate. 16 | * 17 | * @author Jérémy Derussé 18 | */ 19 | class CertificateRequest 20 | { 21 | /** @var DistinguishedName */ 22 | private $distinguishedName; 23 | 24 | /** @var KeyPair */ 25 | private $keyPair; 26 | 27 | public function __construct(DistinguishedName $distinguishedName, KeyPair $keyPair) 28 | { 29 | $this->distinguishedName = $distinguishedName; 30 | $this->keyPair = $keyPair; 31 | } 32 | 33 | /** 34 | * @return DistinguishedName 35 | */ 36 | public function getDistinguishedName() 37 | { 38 | return $this->distinguishedName; 39 | } 40 | 41 | /** 42 | * @return KeyPair 43 | */ 44 | public function getKeyPair() 45 | { 46 | return $this->keyPair; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/CertificateResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | /** 15 | * Represent the response to a certificate request. 16 | * 17 | * @author Jérémy Derussé 18 | */ 19 | class CertificateResponse 20 | { 21 | /** @var CertificateRequest */ 22 | private $certificateRequest; 23 | 24 | /** @var Certificate */ 25 | private $certificate; 26 | 27 | public function __construct( 28 | CertificateRequest $certificateRequest, 29 | Certificate $certificate 30 | ) { 31 | $this->certificateRequest = $certificateRequest; 32 | $this->certificate = $certificate; 33 | } 34 | 35 | /** 36 | * @return CertificateRequest 37 | */ 38 | public function getCertificateRequest() 39 | { 40 | return $this->certificateRequest; 41 | } 42 | 43 | /** 44 | * @return Certificate 45 | */ 46 | public function getCertificate() 47 | { 48 | return $this->certificate; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/DistinguishedName.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | use Webmozart\Assert\Assert; 15 | 16 | /** 17 | * Represent a Distinguished Name. 18 | * 19 | * @author Jérémy Derussé 20 | */ 21 | class DistinguishedName 22 | { 23 | /** @var string */ 24 | private $commonName; 25 | 26 | /** @var string */ 27 | private $countryName; 28 | 29 | /** @var string */ 30 | private $stateOrProvinceName; 31 | 32 | /** @var string */ 33 | private $localityName; 34 | 35 | /** @var string */ 36 | private $organizationName; 37 | 38 | /** @var string */ 39 | private $organizationalUnitName; 40 | 41 | /** @var string */ 42 | private $emailAddress; 43 | 44 | /** @var array */ 45 | private $subjectAlternativeNames; 46 | 47 | /** 48 | * @param string $commonName 49 | * @param string $countryName 50 | * @param string $stateOrProvinceName 51 | * @param string $localityName 52 | * @param string $organizationName 53 | * @param string $organizationalUnitName 54 | * @param string $emailAddress 55 | */ 56 | public function __construct( 57 | $commonName, 58 | $countryName = null, 59 | $stateOrProvinceName = null, 60 | $localityName = null, 61 | $organizationName = null, 62 | $organizationalUnitName = null, 63 | $emailAddress = null, 64 | array $subjectAlternativeNames = [] 65 | ) { 66 | Assert::stringNotEmpty($commonName, __CLASS__.'::$commonName expected a non empty string. Got: %s'); 67 | Assert::nullOrStringNotEmpty($countryName, __CLASS__.'::$countryName expected a string. Got: %s'); 68 | Assert::nullOrStringNotEmpty($stateOrProvinceName, __CLASS__.'::$stateOrProvinceName expected a string. Got: %s'); 69 | Assert::nullOrStringNotEmpty($localityName, __CLASS__.'::$localityName expected a string. Got: %s'); 70 | Assert::nullOrStringNotEmpty($organizationName, __CLASS__.'::$organizationName expected a string. Got: %s'); 71 | Assert::nullOrStringNotEmpty($organizationalUnitName, __CLASS__.'::$organizationalUnitName expected a string. Got: %s'); 72 | Assert::nullOrStringNotEmpty($emailAddress, __CLASS__.'::$emailAddress expected a string. Got: %s'); 73 | Assert::allStringNotEmpty( 74 | $subjectAlternativeNames, 75 | __CLASS__.'::$subjectAlternativeNames expected an array of non empty string. Got: %s' 76 | ); 77 | 78 | $this->commonName = $commonName; 79 | $this->countryName = $countryName; 80 | $this->stateOrProvinceName = $stateOrProvinceName; 81 | $this->localityName = $localityName; 82 | $this->organizationName = $organizationName; 83 | $this->organizationalUnitName = $organizationalUnitName; 84 | $this->emailAddress = $emailAddress; 85 | $this->subjectAlternativeNames = array_diff(array_unique($subjectAlternativeNames), [$commonName]); 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function getCommonName() 92 | { 93 | return $this->commonName; 94 | } 95 | 96 | /** 97 | * @return string 98 | */ 99 | public function getCountryName() 100 | { 101 | return $this->countryName; 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function getStateOrProvinceName() 108 | { 109 | return $this->stateOrProvinceName; 110 | } 111 | 112 | /** 113 | * @return string 114 | */ 115 | public function getLocalityName() 116 | { 117 | return $this->localityName; 118 | } 119 | 120 | /** 121 | * @return string 122 | */ 123 | public function getOrganizationName() 124 | { 125 | return $this->organizationName; 126 | } 127 | 128 | /** 129 | * @return string 130 | */ 131 | public function getOrganizationalUnitName() 132 | { 133 | return $this->organizationalUnitName; 134 | } 135 | 136 | /** 137 | * @return string 138 | */ 139 | public function getEmailAddress() 140 | { 141 | return $this->emailAddress; 142 | } 143 | 144 | /** 145 | * @return array 146 | */ 147 | public function getSubjectAlternativeNames() 148 | { 149 | return $this->subjectAlternativeNames; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/AcmeSslException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | */ 17 | class AcmeSslException extends \RuntimeException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/CSRSigningException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | */ 17 | class CSRSigningException extends SigningException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/CertificateFormatException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | */ 17 | class CertificateFormatException extends ParsingException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/CertificateParsingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | */ 17 | class CertificateParsingException extends ParsingException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/DataSigningException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class DataSigningException extends SigningException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/KeyFormatException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | */ 17 | class KeyFormatException extends ParsingException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/KeyGenerationException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | */ 17 | class KeyGenerationException extends AcmeSslException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/KeyPairGenerationException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | */ 17 | class KeyPairGenerationException extends KeyGenerationException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/KeyParsingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class KeyParsingException extends ParsingException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/ParsingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class ParsingException extends AcmeSslException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Exception/SigningException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception; 13 | 14 | /** 15 | * @author Titouan Galopin 16 | */ 17 | class SigningException extends AcmeSslException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/ChainPrivateKeyGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator; 13 | 14 | /** 15 | * Generate random RSA private key using OpenSSL. 16 | * 17 | * @author Jérémy Derussé 18 | */ 19 | class ChainPrivateKeyGenerator implements PrivateKeyGeneratorInterface 20 | { 21 | /** @var PrivateKeyGeneratorInterface[] */ 22 | private $generators; 23 | 24 | /** 25 | * @param PrivateKeyGeneratorInterface[] $generators 26 | */ 27 | public function __construct($generators) 28 | { 29 | $this->generators = $generators; 30 | } 31 | 32 | public function generatePrivateKey(KeyOption $keyOption) 33 | { 34 | foreach ($this->generators as $generator) { 35 | if ($generator->supportsKeyOption($keyOption)) { 36 | return $generator->generatePrivateKey($keyOption); 37 | } 38 | } 39 | 40 | throw new \LogicException(sprintf('Unable to find a generator for a key option of type %s', \get_class($keyOption))); 41 | } 42 | 43 | public function supportsKeyOption(KeyOption $keyOption) 44 | { 45 | foreach ($this->generators as $generator) { 46 | if ($generator->supportsKeyOption($keyOption)) { 47 | return true; 48 | } 49 | } 50 | 51 | return false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/DhKey/DhKeyGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\DhKey; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\KeyOption; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface; 17 | use Webmozart\Assert\Assert; 18 | 19 | /** 20 | * Generate random DH private key using OpenSSL. 21 | * 22 | * @author Jérémy Derussé 23 | */ 24 | class DhKeyGenerator implements PrivateKeyGeneratorInterface 25 | { 26 | use OpensslPrivateKeyGeneratorTrait; 27 | 28 | /** 29 | * @param DhKeyOption|KeyOption $keyOption 30 | */ 31 | public function generatePrivateKey(KeyOption $keyOption) 32 | { 33 | Assert::isInstanceOf($keyOption, DhKeyOption::class); 34 | 35 | return $this->generatePrivateKeyFromOpensslOptions( 36 | [ 37 | 'private_key_type' => OPENSSL_KEYTYPE_DH, 38 | 'dh' => [ 39 | 'p' => $keyOption->getPrime(), 40 | 'g' => $keyOption->getGenerator(), 41 | ], 42 | ] 43 | ); 44 | } 45 | 46 | public function supportsKeyOption(KeyOption $keyOption) 47 | { 48 | return $keyOption instanceof DhKeyOption; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/DhKey/DhKeyOption.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\DhKey; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\KeyOption; 15 | 16 | class DhKeyOption implements KeyOption 17 | { 18 | /** @var string */ 19 | private $generator; 20 | /** @var string */ 21 | private $prime; 22 | 23 | /** 24 | * @param string $prime Hexadecimal representation of the prime 25 | * @param string $generator Hexadecimal representation of the generator: ie. 02 26 | * 27 | * @see https://tools.ietf.org/html/rfc3526 how to choose a prime and generator numbers 28 | */ 29 | public function __construct($prime, $generator = '02') 30 | { 31 | $this->generator = pack('H*', $generator); 32 | $this->prime = pack('H*', $prime); 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getGenerator() 39 | { 40 | return $this->generator; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getPrime() 47 | { 48 | return $this->prime; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/DsaKey/DsaKeyGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\DsaKey; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\KeyOption; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface; 17 | use Webmozart\Assert\Assert; 18 | 19 | /** 20 | * Generate random DSA private key using OpenSSL. 21 | * 22 | * @author Jérémy Derussé 23 | */ 24 | class DsaKeyGenerator implements PrivateKeyGeneratorInterface 25 | { 26 | use OpensslPrivateKeyGeneratorTrait; 27 | 28 | /** 29 | * @param DsaKeyOption|KeyOption $keyOption 30 | */ 31 | public function generatePrivateKey(KeyOption $keyOption) 32 | { 33 | Assert::isInstanceOf($keyOption, DsaKeyOption::class); 34 | 35 | return $this->generatePrivateKeyFromOpensslOptions( 36 | [ 37 | 'private_key_type' => OPENSSL_KEYTYPE_DSA, 38 | 'private_key_bits' => $keyOption->getBits(), 39 | ] 40 | ); 41 | } 42 | 43 | public function supportsKeyOption(KeyOption $keyOption) 44 | { 45 | return $keyOption instanceof DsaKeyOption; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/DsaKey/DsaKeyOption.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\DsaKey; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\KeyOption; 15 | use Webmozart\Assert\Assert; 16 | 17 | class DsaKeyOption implements KeyOption 18 | { 19 | /** @var int */ 20 | private $bits; 21 | 22 | public function __construct($bits = 2048) 23 | { 24 | Assert::integer($bits); 25 | 26 | $this->bits = $bits; 27 | } 28 | 29 | /** 30 | * @return int 31 | */ 32 | public function getBits() 33 | { 34 | return $this->bits; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/EcKey/EcKeyGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\EcKey; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\KeyOption; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface; 17 | use Webmozart\Assert\Assert; 18 | 19 | /** 20 | * Generate random EC private key using OpenSSL. 21 | * 22 | * @author Jérémy Derussé 23 | */ 24 | class EcKeyGenerator implements PrivateKeyGeneratorInterface 25 | { 26 | use OpensslPrivateKeyGeneratorTrait; 27 | 28 | /** 29 | * @param EcKeyOption|KeyOption $keyOption 30 | */ 31 | public function generatePrivateKey(KeyOption $keyOption) 32 | { 33 | if (\PHP_VERSION_ID < 70100) { 34 | throw new \LogicException('The generation of ECDSA requires a version of PHP >= 7.1'); 35 | } 36 | 37 | Assert::isInstanceOf($keyOption, EcKeyOption::class); 38 | 39 | return $this->generatePrivateKeyFromOpensslOptions( 40 | [ 41 | 'private_key_type' => OPENSSL_KEYTYPE_EC, 42 | 'curve_name' => $keyOption->getCurveName(), 43 | ] 44 | ); 45 | } 46 | 47 | public function supportsKeyOption(KeyOption $keyOption) 48 | { 49 | return $keyOption instanceof EcKeyOption; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/EcKey/EcKeyOption.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\EcKey; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\KeyOption; 15 | use Webmozart\Assert\Assert; 16 | 17 | class EcKeyOption implements KeyOption 18 | { 19 | /** @var string */ 20 | private $curveName; 21 | 22 | public function __construct($curveName = 'secp384r1') 23 | { 24 | if (\PHP_VERSION_ID < 70100) { 25 | throw new \LogicException('The generation of ECDSA requires a version of PHP >= 7.1'); 26 | } 27 | 28 | Assert::stringNotEmpty($curveName); 29 | Assert::oneOf($curveName, openssl_get_curve_names(), 'The given curve %s is not supported. Available curves are: %s'); 30 | 31 | $this->curveName = $curveName; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getCurveName() 38 | { 39 | return $this->curveName; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/KeyOption.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator; 13 | 14 | interface KeyOption 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/KeyPairGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\KeyGenerationException; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\KeyPairGenerationException; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\DhKey\DhKeyGenerator; 17 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\DsaKey\DsaKeyGenerator; 18 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\EcKey\EcKeyGenerator; 19 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\RsaKey\RsaKeyGenerator; 20 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\RsaKey\RsaKeyOption; 21 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\KeyPair; 22 | use Webmozart\Assert\Assert; 23 | 24 | /** 25 | * Generate random KeyPair using OpenSSL. 26 | * 27 | * @author Jérémy Derussé 28 | */ 29 | class KeyPairGenerator 30 | { 31 | private $generator; 32 | 33 | public function __construct(PrivateKeyGeneratorInterface $generator = null) 34 | { 35 | $this->generator = $generator ?: new ChainPrivateKeyGenerator( 36 | [ 37 | new RsaKeyGenerator(), 38 | new EcKeyGenerator(), 39 | new DhKeyGenerator(), 40 | new DsaKeyGenerator(), 41 | ] 42 | ); 43 | } 44 | 45 | /** 46 | * Generate KeyPair. 47 | * 48 | * @param KeyOption $keyOption configuration of the key to generate 49 | * 50 | * @throws KeyPairGenerationException when OpenSSL failed to generate keys 51 | * 52 | * @return KeyPair 53 | */ 54 | public function generateKeyPair($keyOption = null) 55 | { 56 | if (null === $keyOption) { 57 | $keyOption = new RsaKeyOption(); 58 | } 59 | if (\is_int($keyOption)) { 60 | @trigger_error('Passing a keySize to "generateKeyPair" is deprecated since version 1.1 and will be removed in 2.0. Pass an instance of KeyOption instead', E_USER_DEPRECATED); 61 | $keyOption = new RsaKeyOption($keyOption); 62 | } 63 | Assert::isInstanceOf($keyOption, KeyOption::class); 64 | 65 | try { 66 | $privateKey = $this->generator->generatePrivateKey($keyOption); 67 | } catch (KeyGenerationException $e) { 68 | throw new KeyPairGenerationException('Fail to generate a KeyPair with the given options', 0, $e); 69 | } 70 | 71 | return new KeyPair( 72 | $privateKey->getPublicKey(), 73 | $privateKey 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/OpensslPrivateKeyGeneratorTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\KeyGenerationException; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\KeyPairGenerationException; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\PrivateKey; 17 | 18 | trait OpensslPrivateKeyGeneratorTrait 19 | { 20 | private function generatePrivateKeyFromOpensslOptions(array $opensslOptions) 21 | { 22 | $resource = openssl_pkey_new($opensslOptions); 23 | 24 | if (! $resource) { 25 | throw new KeyGenerationException(sprintf('OpenSSL key creation failed during generation with error: %s', openssl_error_string())); 26 | } 27 | if (! openssl_pkey_export($resource, $privateKey)) { 28 | throw new KeyPairGenerationException(sprintf('OpenSSL key export failed during generation with error: %s', openssl_error_string())); 29 | } 30 | 31 | // PHP 8 automatically frees the key instance and deprecates the function 32 | if (\PHP_VERSION_ID < 80000) { 33 | openssl_free_key($resource); 34 | } 35 | 36 | return new PrivateKey($privateKey); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/PrivateKeyGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\KeyGenerationException; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\PrivateKey; 16 | 17 | /** 18 | * Generate random private key. 19 | * 20 | * @author Jérémy Derussé 21 | */ 22 | interface PrivateKeyGeneratorInterface 23 | { 24 | /** 25 | * Generate a PrivateKey. 26 | * 27 | * @param KeyOption $keyOption configuration of the key to generate 28 | * 29 | * @throws KeyGenerationException when OpenSSL failed to generate keys 30 | * 31 | * @return PrivateKey 32 | */ 33 | public function generatePrivateKey(KeyOption $keyOption); 34 | 35 | /** 36 | * Returns whether the instance is able to generator a private key from the given option. 37 | * 38 | * @param KeyOption $keyOption configuration of the key to generate 39 | * 40 | * @return bool 41 | */ 42 | public function supportsKeyOption(KeyOption $keyOption); 43 | } 44 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/RsaKey/RsaKeyGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\RsaKey; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\KeyOption; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface; 17 | use Webmozart\Assert\Assert; 18 | 19 | /** 20 | * Generate random RSA private key using OpenSSL. 21 | * 22 | * @author Jérémy Derussé 23 | */ 24 | class RsaKeyGenerator implements PrivateKeyGeneratorInterface 25 | { 26 | use OpensslPrivateKeyGeneratorTrait; 27 | 28 | /** 29 | * @param RsaKeyOption|KeyOption $keyOption 30 | */ 31 | public function generatePrivateKey(KeyOption $keyOption) 32 | { 33 | Assert::isInstanceOf($keyOption, RsaKeyOption::class); 34 | 35 | return $this->generatePrivateKeyFromOpensslOptions( 36 | [ 37 | 'private_key_type' => OPENSSL_KEYTYPE_RSA, 38 | 'private_key_bits' => $keyOption->getBits(), 39 | ] 40 | ); 41 | } 42 | 43 | public function supportsKeyOption(KeyOption $keyOption) 44 | { 45 | return $keyOption instanceof RsaKeyOption; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Generator/RsaKey/RsaKeyOption.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\RsaKey; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Generator\KeyOption; 15 | use Webmozart\Assert\Assert; 16 | 17 | class RsaKeyOption implements KeyOption 18 | { 19 | /** @var int */ 20 | private $bits; 21 | 22 | public function __construct($bits = 4096) 23 | { 24 | Assert::integer($bits); 25 | 26 | $this->bits = $bits; 27 | } 28 | 29 | /** 30 | * @return int 31 | */ 32 | public function getBits() 33 | { 34 | return $this->bits; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Key.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | use Webmozart\Assert\Assert; 15 | 16 | /** 17 | * Represent a SSL key. 18 | * 19 | * @author Jérémy Derussé 20 | */ 21 | abstract class Key 22 | { 23 | /** @var string */ 24 | protected $keyPEM; 25 | 26 | /** 27 | * @param string $keyPEM 28 | */ 29 | public function __construct($keyPEM) 30 | { 31 | Assert::stringNotEmpty($keyPEM, __CLASS__.'::$keyPEM should not be an empty string. Got %s'); 32 | 33 | $this->keyPEM = $keyPEM; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getPEM() 40 | { 41 | return $this->keyPEM; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getDER() 48 | { 49 | $lines = explode("\n", trim($this->keyPEM)); 50 | unset($lines[\count($lines) - 1]); 51 | unset($lines[0]); 52 | $result = implode('', $lines); 53 | $result = base64_decode($result); 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * @return resource 60 | */ 61 | abstract public function getResource(); 62 | } 63 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/KeyPair.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | /** 15 | * Represent a SSL key-pair (public and private). 16 | * 17 | * @author Titouan Galopin 18 | */ 19 | class KeyPair 20 | { 21 | /** @var PublicKey */ 22 | private $publicKey; 23 | 24 | /** @var PrivateKey */ 25 | private $privateKey; 26 | 27 | public function __construct(PublicKey $publicKey, PrivateKey $privateKey) 28 | { 29 | $this->publicKey = $publicKey; 30 | $this->privateKey = $privateKey; 31 | } 32 | 33 | /** 34 | * @return PublicKey 35 | */ 36 | public function getPublicKey() 37 | { 38 | return $this->publicKey; 39 | } 40 | 41 | /** 42 | * @return PrivateKey 43 | */ 44 | public function getPrivateKey() 45 | { 46 | return $this->privateKey; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/ParsedCertificate.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | use Webmozart\Assert\Assert; 15 | 16 | /** 17 | * Represent the content of a parsed certificate. 18 | * 19 | * @author Jérémy Derussé 20 | */ 21 | class ParsedCertificate 22 | { 23 | /** @var Certificate */ 24 | private $source; 25 | 26 | /** @var string */ 27 | private $subject; 28 | 29 | /** @var string */ 30 | private $issuer; 31 | 32 | /** @var bool */ 33 | private $selfSigned; 34 | 35 | /** @var \DateTime */ 36 | private $validFrom; 37 | 38 | /** @var \DateTime */ 39 | private $validTo; 40 | 41 | /** @var string */ 42 | private $serialNumber; 43 | 44 | /** @var array */ 45 | private $subjectAlternativeNames; 46 | 47 | /** 48 | * @param string $subject 49 | * @param string $issuer 50 | * @param bool $selfSigned 51 | * @param \DateTime $validFrom 52 | * @param \DateTime $validTo 53 | * @param string $serialNumber 54 | */ 55 | public function __construct( 56 | Certificate $source, 57 | $subject, 58 | $issuer = null, 59 | $selfSigned = true, 60 | \DateTime $validFrom = null, 61 | \DateTime $validTo = null, 62 | $serialNumber = null, 63 | array $subjectAlternativeNames = [] 64 | ) { 65 | Assert::stringNotEmpty($subject, __CLASS__.'::$subject expected a non empty string. Got: %s'); 66 | Assert::nullOrString($issuer, __CLASS__.'::$issuer expected a string or null. Got: %s'); 67 | Assert::nullOrBoolean($selfSigned, __CLASS__.'::$selfSigned expected a boolean or null. Got: %s'); 68 | Assert::nullOrString($serialNumber, __CLASS__.'::$serialNumber expected a string or null. Got: %s'); 69 | Assert::allStringNotEmpty( 70 | $subjectAlternativeNames, 71 | __CLASS__.'::$subjectAlternativeNames expected a array of non empty string. Got: %s' 72 | ); 73 | 74 | $this->source = $source; 75 | $this->subject = $subject; 76 | $this->issuer = $issuer; 77 | $this->selfSigned = $selfSigned; 78 | $this->validFrom = $validFrom; 79 | $this->validTo = $validTo; 80 | $this->serialNumber = $serialNumber; 81 | $this->subjectAlternativeNames = $subjectAlternativeNames; 82 | } 83 | 84 | /** 85 | * @return Certificate 86 | */ 87 | public function getSource() 88 | { 89 | return $this->source; 90 | } 91 | 92 | /** 93 | * @return string 94 | */ 95 | public function getSubject() 96 | { 97 | return $this->subject; 98 | } 99 | 100 | /** 101 | * @return string 102 | */ 103 | public function getIssuer() 104 | { 105 | return $this->issuer; 106 | } 107 | 108 | /** 109 | * @return bool 110 | */ 111 | public function isSelfSigned() 112 | { 113 | return $this->selfSigned; 114 | } 115 | 116 | /** 117 | * @return \DateTime 118 | */ 119 | public function getValidFrom() 120 | { 121 | return $this->validFrom; 122 | } 123 | 124 | /** 125 | * @return \DateTime 126 | */ 127 | public function getValidTo() 128 | { 129 | return $this->validTo; 130 | } 131 | 132 | /** 133 | * @return bool 134 | */ 135 | public function isExpired() 136 | { 137 | return $this->validTo < (new \DateTime()); 138 | } 139 | 140 | /** 141 | * @return string 142 | */ 143 | public function getSerialNumber() 144 | { 145 | return $this->serialNumber; 146 | } 147 | 148 | /** 149 | * @return array 150 | */ 151 | public function getSubjectAlternativeNames() 152 | { 153 | return $this->subjectAlternativeNames; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/ParsedKey.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | use Webmozart\Assert\Assert; 15 | 16 | /** 17 | * Represent the content of a parsed key. 18 | * 19 | * @see openssl_pkey_get_details 20 | * 21 | * @author Titouan Galopin 22 | */ 23 | class ParsedKey 24 | { 25 | /** @var Key */ 26 | private $source; 27 | 28 | /** @var string */ 29 | private $key; 30 | 31 | /** @var int */ 32 | private $bits; 33 | 34 | /** @var int */ 35 | private $type; 36 | 37 | /** @var array */ 38 | private $details; 39 | 40 | /** 41 | * @param string $key 42 | * @param int $bits 43 | * @param int $type 44 | */ 45 | public function __construct(Key $source, $key, $bits, $type, array $details = []) 46 | { 47 | Assert::stringNotEmpty($key, __CLASS__.'::$key expected a non empty string. Got: %s'); 48 | Assert::integer($bits, __CLASS__.'::$bits expected an integer. Got: %s'); 49 | Assert::oneOf( 50 | $type, 51 | [OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_DSA, OPENSSL_KEYTYPE_DH, OPENSSL_KEYTYPE_EC], 52 | __CLASS__.'::$type expected one of: %2$s. Got: %s' 53 | ); 54 | 55 | $this->source = $source; 56 | $this->key = $key; 57 | $this->bits = $bits; 58 | $this->type = $type; 59 | $this->details = $details; 60 | } 61 | 62 | /** 63 | * @return Key 64 | */ 65 | public function getSource() 66 | { 67 | return $this->source; 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getKey() 74 | { 75 | return $this->key; 76 | } 77 | 78 | /** 79 | * @return int 80 | */ 81 | public function getBits() 82 | { 83 | return $this->bits; 84 | } 85 | 86 | /** 87 | * @return int 88 | */ 89 | public function getType() 90 | { 91 | return $this->type; 92 | } 93 | 94 | /** 95 | * @return array 96 | */ 97 | public function getDetails() 98 | { 99 | return $this->details; 100 | } 101 | 102 | /** 103 | * @param string $name 104 | * 105 | * @return bool 106 | */ 107 | public function hasDetail($name) 108 | { 109 | return isset($this->details[$name]); 110 | } 111 | 112 | /** 113 | * @param string $name 114 | * 115 | * @return mixed 116 | */ 117 | public function getDetail($name) 118 | { 119 | Assert::oneOf($name, array_keys($this->details), 'ParsedKey::getDetail() expected one of: %2$s. Got: %s'); 120 | 121 | return $this->details[$name]; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Parser/CertificateParser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Parser; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Certificate; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\CertificateParsingException; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\ParsedCertificate; 17 | 18 | /** 19 | * Parse certificate to extract metadata. 20 | * 21 | * @author Jérémy Derussé 22 | */ 23 | class CertificateParser 24 | { 25 | /** 26 | * Parse the certificate. 27 | * 28 | * @return ParsedCertificate 29 | */ 30 | public function parse(Certificate $certificate) 31 | { 32 | $rawData = openssl_x509_parse($certificate->getPEM()); 33 | 34 | if (! \is_array($rawData)) { 35 | throw new CertificateParsingException(sprintf('Fail to parse certificate with error: %s', openssl_error_string())); 36 | } 37 | 38 | if (! isset($rawData['subject']['CN'])) { 39 | throw new CertificateParsingException('Missing expected key "subject.cn" in certificate'); 40 | } 41 | if (! isset($rawData['serialNumber'])) { 42 | throw new CertificateParsingException('Missing expected key "serialNumber" in certificate'); 43 | } 44 | if (! isset($rawData['validFrom_time_t'])) { 45 | throw new CertificateParsingException('Missing expected key "validFrom_time_t" in certificate'); 46 | } 47 | if (! isset($rawData['validTo_time_t'])) { 48 | throw new CertificateParsingException('Missing expected key "validTo_time_t" in certificate'); 49 | } 50 | 51 | $subjectAlternativeName = []; 52 | 53 | if (isset($rawData['extensions']['subjectAltName'])) { 54 | $subjectAlternativeName = array_map( 55 | function ($item) { 56 | return explode(':', trim($item), 2)[1]; 57 | }, 58 | array_filter( 59 | explode( 60 | ',', 61 | $rawData['extensions']['subjectAltName'] 62 | ), 63 | function ($item) { 64 | return false !== strpos($item, ':'); 65 | } 66 | ) 67 | ); 68 | } 69 | 70 | return new ParsedCertificate( 71 | $certificate, 72 | $rawData['subject']['CN'], 73 | isset($rawData['issuer']['CN']) ? $rawData['issuer']['CN'] : null, 74 | $rawData['subject'] === $rawData['issuer'], 75 | new \DateTime('@'.$rawData['validFrom_time_t']), 76 | new \DateTime('@'.$rawData['validTo_time_t']), 77 | $rawData['serialNumber'], 78 | $subjectAlternativeName 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Parser/KeyParser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Parser; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\KeyFormatException; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\KeyParsingException; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Key; 17 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\ParsedKey; 18 | 19 | /** 20 | * Parse keys to extract metadata. 21 | * 22 | * @author Titouan Galopin 23 | */ 24 | class KeyParser 25 | { 26 | /** 27 | * Parse the key. 28 | * 29 | * @return ParsedKey 30 | */ 31 | public function parse(Key $key) 32 | { 33 | try { 34 | $resource = $key->getResource(); 35 | } catch (KeyFormatException $e) { 36 | throw new KeyParsingException('Fail to load resource for key', 0, $e); 37 | } 38 | 39 | $rawData = openssl_pkey_get_details($resource); 40 | 41 | // PHP 8 automatically frees the key instance and deprecates the function 42 | if (\PHP_VERSION_ID < 80000) { 43 | openssl_free_key($resource); 44 | } 45 | 46 | if (! \is_array($rawData)) { 47 | throw new KeyParsingException(sprintf('Fail to parse key with error: %s', openssl_error_string())); 48 | } 49 | 50 | foreach (['type', 'key', 'bits'] as $requiredKey) { 51 | if (! isset($rawData[$requiredKey])) { 52 | throw new KeyParsingException(sprintf('Missing expected key "%s" in OpenSSL key', $requiredKey)); 53 | } 54 | } 55 | 56 | $details = []; 57 | 58 | if (OPENSSL_KEYTYPE_RSA === $rawData['type']) { 59 | $details = $rawData['rsa']; 60 | } elseif (OPENSSL_KEYTYPE_DSA === $rawData['type']) { 61 | $details = $rawData['dsa']; 62 | } elseif (OPENSSL_KEYTYPE_DH === $rawData['type']) { 63 | $details = $rawData['dh']; 64 | } elseif (OPENSSL_KEYTYPE_EC === $rawData['type']) { 65 | $details = $rawData['ec']; 66 | } 67 | 68 | return new ParsedKey($key, $rawData['key'], $rawData['bits'], $rawData['type'], $details); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/PrivateKey.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\KeyFormatException; 15 | use Webmozart\Assert\Assert; 16 | 17 | /** 18 | * Represent a SSL Private key. 19 | * 20 | * @author Jérémy Derussé 21 | */ 22 | class PrivateKey extends Key 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getResource() 28 | { 29 | if (! $resource = openssl_pkey_get_private($this->keyPEM)) { 30 | throw new KeyFormatException(sprintf('Failed to convert key into resource: %s', openssl_error_string())); 31 | } 32 | 33 | return $resource; 34 | } 35 | 36 | /** 37 | * @return PublicKey 38 | */ 39 | public function getPublicKey() 40 | { 41 | $resource = $this->getResource(); 42 | if (! $details = openssl_pkey_get_details($resource)) { 43 | throw new KeyFormatException(sprintf('Failed to extract public key: %s', openssl_error_string())); 44 | } 45 | 46 | // PHP 8 automatically frees the key instance and deprecates the function 47 | if (\PHP_VERSION_ID < 80000) { 48 | openssl_free_key($resource); 49 | } 50 | 51 | return new PublicKey($details['key']); 52 | } 53 | 54 | /** 55 | * @param $keyDER 56 | * 57 | * @return PrivateKey 58 | */ 59 | public static function fromDER($keyDER) 60 | { 61 | Assert::stringNotEmpty($keyDER, __METHOD__.'::$keyDER should be a non-empty string. Got %s'); 62 | 63 | $der = base64_encode($keyDER); 64 | $lines = str_split($der, 65); 65 | array_unshift($lines, '-----BEGIN PRIVATE KEY-----'); 66 | $lines[] = '-----END PRIVATE KEY-----'; 67 | $lines[] = ''; 68 | 69 | return new self(implode("\n", $lines)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/PublicKey.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\KeyFormatException; 15 | use Webmozart\Assert\Assert; 16 | 17 | /** 18 | * Represent a SSL Public key. 19 | * 20 | * @author Jérémy Derussé 21 | */ 22 | class PublicKey extends Key 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getResource() 28 | { 29 | if (! $resource = openssl_pkey_get_public($this->keyPEM)) { 30 | throw new KeyFormatException(sprintf('Failed to convert key into resource: %s', openssl_error_string())); 31 | } 32 | 33 | return $resource; 34 | } 35 | 36 | /** 37 | * @param $keyDER 38 | * 39 | * @return PublicKey 40 | */ 41 | public static function fromDER($keyDER) 42 | { 43 | Assert::stringNotEmpty($keyDER, __METHOD__.'::$keyDER should be a non-empty string. Got %s'); 44 | 45 | $der = base64_encode($keyDER); 46 | $lines = str_split($der, 65); 47 | array_unshift($lines, '-----BEGIN PUBLIC KEY-----'); 48 | $lines[] = '-----END PUBLIC KEY-----'; 49 | $lines[] = ''; 50 | 51 | return new self(implode("\n", $lines)); 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getHPKP() 58 | { 59 | return base64_encode(hash('sha256', $this->getDER(), true)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Signer/CertificateRequestSigner.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Signer; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\CertificateRequest; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\DistinguishedName; 16 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\CSRSigningException; 17 | 18 | /** 19 | * Provide tools to sign certificate request. 20 | * 21 | * @author Jérémy Derussé 22 | */ 23 | class CertificateRequestSigner 24 | { 25 | /** 26 | * Generate a CSR from the given distinguishedName and keyPair. 27 | * 28 | * @return string 29 | */ 30 | public function signCertificateRequest(CertificateRequest $certificateRequest) 31 | { 32 | $csrObject = $this->createCsrWithSANsObject($certificateRequest); 33 | 34 | if (! $csrObject || ! openssl_csr_export($csrObject, $csrExport)) { 35 | throw new CSRSigningException(sprintf('OpenSSL CSR signing failed with error: %s', openssl_error_string())); 36 | } 37 | 38 | return $csrExport; 39 | } 40 | 41 | /** 42 | * Generate a CSR object with SANs from the given distinguishedName and keyPair. 43 | * 44 | * @return mixed 45 | */ 46 | protected function createCsrWithSANsObject(CertificateRequest $certificateRequest) 47 | { 48 | $sslConfigTemplate = <<<'EOL' 49 | [ req ] 50 | distinguished_name = req_distinguished_name 51 | req_extensions = v3_req 52 | [ req_distinguished_name ] 53 | [ v3_req ] 54 | basicConstraints = CA:FALSE 55 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 56 | subjectAltName = @req_subject_alt_name 57 | [ req_subject_alt_name ] 58 | %s 59 | EOL; 60 | $sslConfigDomains = []; 61 | 62 | $distinguishedName = $certificateRequest->getDistinguishedName(); 63 | $domains = array_merge( 64 | [$distinguishedName->getCommonName()], 65 | $distinguishedName->getSubjectAlternativeNames() 66 | ); 67 | 68 | foreach (array_values($domains) as $index => $domain) { 69 | $sslConfigDomains[] = 'DNS.'.($index + 1).' = '.$domain; 70 | } 71 | 72 | $sslConfigContent = sprintf($sslConfigTemplate, implode("\n", $sslConfigDomains)); 73 | $sslConfigFile = tempnam(sys_get_temp_dir(), 'acmephp_'); 74 | 75 | try { 76 | file_put_contents($sslConfigFile, $sslConfigContent); 77 | 78 | $resource = $certificateRequest->getKeyPair()->getPrivateKey()->getResource(); 79 | 80 | $csr = openssl_csr_new( 81 | $this->getCSRPayload($distinguishedName), 82 | $resource, 83 | [ 84 | 'digest_alg' => 'sha256', 85 | 'config' => $sslConfigFile, 86 | ] 87 | ); 88 | 89 | // PHP 8 automatically frees the key instance and deprecates the function 90 | if (\PHP_VERSION_ID < 80000) { 91 | openssl_free_key($resource); 92 | } 93 | 94 | if (! $csr) { 95 | throw new CSRSigningException(sprintf('OpenSSL CSR signing failed with error: %s', openssl_error_string())); 96 | } 97 | 98 | return $csr; 99 | } finally { 100 | unlink($sslConfigFile); 101 | } 102 | } 103 | 104 | /** 105 | * Retrieves a CSR payload from the given distinguished name. 106 | * 107 | * @return array 108 | */ 109 | private function getCSRPayload(DistinguishedName $distinguishedName) 110 | { 111 | $payload = []; 112 | if (null !== $countryName = $distinguishedName->getCountryName()) { 113 | $payload['countryName'] = $countryName; 114 | } 115 | if (null !== $stateOrProvinceName = $distinguishedName->getStateOrProvinceName()) { 116 | $payload['stateOrProvinceName'] = $stateOrProvinceName; 117 | } 118 | if (null !== $localityName = $distinguishedName->getLocalityName()) { 119 | $payload['localityName'] = $localityName; 120 | } 121 | if (null !== $OrganizationName = $distinguishedName->getOrganizationName()) { 122 | $payload['organizationName'] = $OrganizationName; 123 | } 124 | if (null !== $organizationUnitName = $distinguishedName->getOrganizationalUnitName()) { 125 | $payload['organizationalUnitName'] = $organizationUnitName; 126 | } 127 | if (null !== $commonName = $distinguishedName->getCommonName()) { 128 | $payload['commonName'] = $commonName; 129 | } 130 | if (null !== $emailAddress = $distinguishedName->getEmailAddress()) { 131 | $payload['emailAddress'] = $emailAddress; 132 | } 133 | 134 | return $payload; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/AcmePhp/Ssl/Signer/DataSigner.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Signer; 13 | 14 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\Exception\DataSigningException; 15 | use Daanra\LaravelLetsEncrypt\AcmePhp\Ssl\PrivateKey; 16 | use Webmozart\Assert\Assert; 17 | 18 | /** 19 | * Provide tools to sign data using a private key. 20 | * 21 | * @author Titouan Galopin 22 | */ 23 | class DataSigner 24 | { 25 | const FORMAT_DER = 'DER'; 26 | const FORMAT_ECDSA = 'ECDSA'; 27 | 28 | /** 29 | * Generate a signature of the given data using a private key and an algorithm. 30 | * 31 | * @param string $data Data to sign 32 | * @param PrivateKey $privateKey Key used to sign 33 | * @param int $algorithm Signature algorithm defined by constants OPENSSL_ALGO_* 34 | * @param string $format Format of the output 35 | * 36 | * @return string 37 | */ 38 | public function signData($data, PrivateKey $privateKey, $algorithm = OPENSSL_ALGO_SHA256, $format = self::FORMAT_DER) 39 | { 40 | Assert::oneOf($format, [self::FORMAT_ECDSA, self::FORMAT_DER], 'The format %s to sign request does not exists. Available format: %s'); 41 | 42 | $resource = $privateKey->getResource(); 43 | if (! openssl_sign($data, $signature, $resource, $algorithm)) { 44 | throw new DataSigningException(sprintf('OpenSSL data signing failed with error: %s', openssl_error_string())); 45 | } 46 | 47 | // PHP 8 automatically frees the key instance and deprecates the function 48 | if (\PHP_VERSION_ID < 80000) { 49 | openssl_free_key($resource); 50 | } 51 | 52 | switch ($format) { 53 | case self::FORMAT_DER: 54 | return $signature; 55 | case self::FORMAT_ECDSA: 56 | switch ($algorithm) { 57 | case OPENSSL_ALGO_SHA256: 58 | return $this->DERtoECDSA($signature, 64); 59 | case OPENSSL_ALGO_SHA384: 60 | return $this->DERtoECDSA($signature, 96); 61 | case OPENSSL_ALGO_SHA512: 62 | return $this->DERtoECDSA($signature, 132); 63 | } 64 | 65 | throw new DataSigningException('Unable to generate a ECDSA signature with the given algorithm'); 66 | default: 67 | throw new DataSigningException('The given format does exists'); 68 | } 69 | } 70 | 71 | /** 72 | * Convert a DER signature into ECDSA. 73 | * 74 | * The code is a copy/paste from another lib (web-token/jwt-core) which is not compatible with php <= 7.0 75 | * 76 | * @see https://github.com/web-token/jwt-core/blob/master/Util/ECSignature.php 77 | */ 78 | private function DERtoECDSA($der, $partLength) 79 | { 80 | $hex = unpack('H*', $der)[1]; 81 | if ('30' !== mb_substr($hex, 0, 2, '8bit')) { // SEQUENCE 82 | throw new DataSigningException('Invalid signature provided'); 83 | } 84 | if ('81' === mb_substr($hex, 2, 2, '8bit')) { // LENGTH > 128 85 | $hex = mb_substr($hex, 6, null, '8bit'); 86 | } else { 87 | $hex = mb_substr($hex, 4, null, '8bit'); 88 | } 89 | if ('02' !== mb_substr($hex, 0, 2, '8bit')) { // INTEGER 90 | throw new DataSigningException('Invalid signature provided'); 91 | } 92 | 93 | $Rl = hexdec(mb_substr($hex, 2, 2, '8bit')); 94 | $R = $this->retrievePositiveInteger(mb_substr($hex, 4, $Rl * 2, '8bit')); 95 | $R = str_pad($R, $partLength, '0', STR_PAD_LEFT); 96 | 97 | $hex = mb_substr($hex, 4 + $Rl * 2, null, '8bit'); 98 | if ('02' !== mb_substr($hex, 0, 2, '8bit')) { // INTEGER 99 | throw new DataSigningException('Invalid signature provided'); 100 | } 101 | $Sl = hexdec(mb_substr($hex, 2, 2, '8bit')); 102 | $S = $this->retrievePositiveInteger(mb_substr($hex, 4, $Sl * 2, '8bit')); 103 | $S = str_pad($S, $partLength, '0', STR_PAD_LEFT); 104 | 105 | return pack('H*', $R.$S); 106 | } 107 | 108 | /** 109 | * The code is a copy/paste from another lib (web-token/jwt-core) which is not compatible with php <= 7.0. 110 | * 111 | * @see https://github.com/web-token/jwt-core/blob/master/Util/ECSignature.php 112 | */ 113 | private function preparePositiveInteger($data) 114 | { 115 | if (mb_substr($data, 0, 2, '8bit') > '7f') { 116 | return '00'.$data; 117 | } 118 | while ('00' === mb_substr($data, 0, 2, '8bit') && mb_substr($data, 2, 2, '8bit') <= '7f') { 119 | $data = mb_substr($data, 2, null, '8bit'); 120 | } 121 | 122 | return $data; 123 | } 124 | 125 | /** 126 | * The code is a copy/paste from another lib (web-token/jwt-core) which is not compatible with php <= 7.0. 127 | * 128 | * @see https://github.com/web-token/jwt-core/blob/master/Util/ECSignature.php 129 | */ 130 | private function retrievePositiveInteger($data) 131 | { 132 | while ('00' === mb_substr($data, 0, 2, '8bit') && mb_substr($data, 2, 2, '8bit') > '7f') { 133 | $data = mb_substr($data, 2, null, '8bit'); 134 | } 135 | 136 | return $data; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Builders/LetsEncryptCertificateBuilder.php: -------------------------------------------------------------------------------- 1 | where('last_renewed_at', '<=', now()->subDays(90)); 16 | } 17 | 18 | /** 19 | * Returns all certificates that are current valid, i.e. certificates that have been issues less than 90 days 20 | * ago. 21 | * @return self 22 | */ 23 | public function valid(): self 24 | { 25 | return $this->where('last_renewed_at', '>', now()->subDays(90)); 26 | } 27 | 28 | /** 29 | * Returns all certificates that require renewal, i.e. all certificates that are older than 60 days. 30 | * @return self 31 | */ 32 | public function requiresRenewal(): self 33 | { 34 | return $this->where('last_renewed_at', '<=', now()->subDays(61)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Collections/LetsEncryptCertificateCollection.php: -------------------------------------------------------------------------------- 1 | each(function (LetsEncryptCertificate $certificate): void { 17 | $certificate->renew(); 18 | }); 19 | 20 | return $this; 21 | } 22 | 23 | /** 24 | * Renews all certificates in the collection synchronously (without placing them on the queue). 25 | * @return self 26 | */ 27 | public function renewNow(): self 28 | { 29 | $this->each(function (LetsEncryptCertificate $certificate): void { 30 | $certificate->renewNow(); 31 | }); 32 | 33 | return $this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Commands/LetsEncryptGenerateCommand.php: -------------------------------------------------------------------------------- 1 | option('domain')) { 19 | $domains = $this->ask('For which domain do you want to create an SSL certificate? [Separate multiple domains with comma\'s]'); 20 | } 21 | $domains = collect(explode(',', $domains)); 22 | 23 | $this->comment('Generating certificates for ' . count($domains) . ' domains.'); 24 | $domains->each(function (string $domain) { 25 | $this->comment($domain . ':'); 26 | 27 | rescue(function () use ($domain) { 28 | LetsEncrypt::createNow($domain); 29 | }, function (Throwable $e) use ($domain) { 30 | $this->error('Failed to generate a certificate for ' . $domain); 31 | $this->error($e->getMessage()); 32 | $this->comment(''); 33 | }, false); 34 | }); 35 | } 36 | 37 | /** 38 | * Get the console command options. 39 | * 40 | * @return array 41 | */ 42 | protected function getOptions() 43 | { 44 | return [ 45 | ['domain', 'd', InputOption::VALUE_OPTIONAL, 'Generate a certificate for the given domain name(s). Multiple domains can be separated by a comma.'], 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Contracts/PathGenerator.php: -------------------------------------------------------------------------------- 1 | getChallengePath($token)); 12 | * }); 13 | */ 14 | interface PathGenerator 15 | { 16 | // Should return the path of where the challenge should be stored. 17 | public function getChallengePath(string $token): string; 18 | 19 | // Should return the path of where the certificate should be stored. 20 | // Note that $filename is 'privkey.pem', 'fullchain.pem', 'chain.pem' or 'cert.pem' 21 | public function getCertificatePath(string $domain, string $filename): string; 22 | } 23 | -------------------------------------------------------------------------------- /src/Encoders/PemEncoder.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 25 | } 26 | 27 | public function getException(): \Throwable 28 | { 29 | return $this->exception; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Events/CleanUpChallengeFailed.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 25 | } 26 | 27 | public function getException(): \Throwable 28 | { 29 | return $this->exception; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Events/RegisterAccountFailed.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 25 | } 26 | 27 | public function getException(): \Throwable 28 | { 29 | return $this->exception; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Events/RenewExpiringCertificatesFailed.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 25 | } 26 | 27 | public function getException(): \Throwable 28 | { 29 | return $this->exception; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Events/RequestAuthorizationFailed.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 29 | $this->certificate = $certificate; 30 | } 31 | 32 | public function getException(): \Throwable 33 | { 34 | return $this->exception; 35 | } 36 | 37 | 38 | public function getCertificate(): LetsEncryptCertificate 39 | { 40 | return $this->certificate; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Events/RequestCertificateFailed.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 29 | $this->certificate = $certificate; 30 | } 31 | 32 | public function getException(): \Throwable 33 | { 34 | return $this->exception; 35 | } 36 | 37 | public function getCertificate(): LetsEncryptCertificate 38 | { 39 | return $this->certificate; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Events/StoreCertificateFailed.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 29 | $this->certificate = $certificate; 30 | } 31 | 32 | public function getException(): \Throwable 33 | { 34 | return $this->exception; 35 | } 36 | 37 | public function getCertificate(): LetsEncryptCertificate 38 | { 39 | return $this->certificate; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Exceptions/DomainAlreadyExists.php: -------------------------------------------------------------------------------- 1 | challenge = $httpChallenge; 28 | $this->tries = $tries; 29 | $this->retryAfter = $retryAfter; 30 | $this->retryList = $retryList; 31 | } 32 | 33 | /** 34 | * Tells the LetsEncrypt API that our challenge is in place. LetsEncrypt will attempt to access 35 | * the challenge on /.well-known/acme-challenges/ 36 | * If this job succeeds, we can clean up the challenge and request a certificate. 37 | * @throws \Daanra\LaravelLetsEncrypt\Exceptions\InvalidKeyPairConfiguration 38 | */ 39 | public function handle() 40 | { 41 | $client = LetsEncrypt::createClient(); 42 | $client->challengeAuthorization($this->challenge); 43 | CleanUpChallenge::dispatch($this->challenge, $this->tries, $this->retryAfter, $this->retryList); 44 | } 45 | 46 | /** 47 | * Handle a job failure. 48 | * 49 | * @return void 50 | */ 51 | public function failed(\Throwable $exception) 52 | { 53 | event(new ChallengeAuthorizationFailed($exception)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Jobs/CleanUpChallenge.php: -------------------------------------------------------------------------------- 1 | challenge = $httpChallenge; 30 | $this->tries = $tries; 31 | $this->retryAfter = $retryAfter; 32 | $this->retryList = $retryList; 33 | } 34 | 35 | /** 36 | * Cleans up the HTTP challenge by removing the file. Should be called right after the challenge was approved. 37 | * @return void 38 | */ 39 | public function handle() 40 | { 41 | $generator = PathGeneratorFactory::create(); 42 | Storage::disk(config('lets_encrypt.challenge_disk'))->delete($generator->getChallengePath($this->challenge->getToken())); 43 | } 44 | 45 | /** 46 | * Handle a job failure. 47 | * 48 | * @return void 49 | */ 50 | public function failed(\Throwable $exception) 51 | { 52 | event(new CleanUpChallengeFailed($exception)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Jobs/RegisterAccount.php: -------------------------------------------------------------------------------- 1 | email = $email; 24 | $this->tries = $tries; 25 | $this->retryAfter = $retryAfter; 26 | $this->retryList = $retryList; 27 | } 28 | 29 | public function handle() 30 | { 31 | $client = LetsEncrypt::createClient(); 32 | $client->registerAccount(null, $this->email); 33 | } 34 | 35 | /** 36 | * Handle a job failure. 37 | * 38 | * @return void 39 | */ 40 | public function failed(\Throwable $exception) 41 | { 42 | event(new RegisterAccountFailed($exception)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Jobs/RenewExpiringCertificates.php: -------------------------------------------------------------------------------- 1 | tries = $tries; 22 | $this->retryAfter = $retryAfter; 23 | $this->retryList = $retryList; 24 | } 25 | 26 | public function handle() 27 | { 28 | LetsEncryptCertificate::query() 29 | ->requiresRenewal() 30 | ->chunk(100, function (LetsEncryptCertificateCollection $certificates) { 31 | $certificates->renew(); 32 | }); 33 | } 34 | 35 | /** 36 | * Handle a job failure. 37 | * 38 | * @return void 39 | */ 40 | public function failed(\Throwable $exception) 41 | { 42 | event(new RenewExpiringCertificatesFailed($exception)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Jobs/RequestAuthorization.php: -------------------------------------------------------------------------------- 1 | sync = false; 35 | $this->certificate = $certificate; 36 | $this->tries = $tries; 37 | $this->retryAfter = $retryAfter; 38 | $this->retryList = $retryList; 39 | } 40 | 41 | /** 42 | * Out of the array of challenges we have, we want to find the HTTP challenge, because that's the 43 | * easiest one to solve in this scenario. 44 | * @param AuthorizationChallenge[] $challenges 45 | * @return AuthorizationChallenge 46 | */ 47 | protected function getHttpChallenge(array $challenges): AuthorizationChallenge 48 | { 49 | return collect($challenges)->first(function (AuthorizationChallenge $challenge): bool { 50 | return Str::startsWith($challenge->getType(), 'http'); 51 | }); 52 | } 53 | 54 | /** 55 | * Stores the HTTP-01 challenge at the appropriate place on disk. 56 | * @param AuthorizationChallenge $challenge 57 | * @throws FailedToMoveChallengeException 58 | */ 59 | protected function placeChallenge(AuthorizationChallenge $challenge): void 60 | { 61 | $path = PathGeneratorFactory::create()->getChallengePath($challenge->getToken()); 62 | $success = Storage::disk(config('lets_encrypt.challenge_disk'))->put($path, $challenge->getPayload()); 63 | 64 | if ($success === false) { 65 | throw new FailedToMoveChallengeException($path); 66 | } 67 | } 68 | 69 | public function handle() 70 | { 71 | $client = LetsEncrypt::createClient(); 72 | $challenges = $client->requestAuthorization($this->certificate->domain); 73 | $httpChallenge = $this->getHttpChallenge($challenges); 74 | $this->placeChallenge($httpChallenge); 75 | 76 | if ($this->sync) { 77 | ChallengeAuthorization::dispatchSync($httpChallenge, $this->tries, $this->retryAfter, $this->retryList); 78 | } else { 79 | ChallengeAuthorization::dispatch($httpChallenge, $this->tries, $this->retryAfter, $this->retryList); 80 | } 81 | } 82 | 83 | protected function setSync(bool $sync) 84 | { 85 | $this->sync = $sync; 86 | } 87 | 88 | public static function dispatchNow(LetsEncryptCertificate $certificate) 89 | { 90 | $job = new static($certificate); 91 | $job->setSync(true); 92 | app(Dispatcher::class)->dispatchSync($job); 93 | } 94 | 95 | /** 96 | * Handle a job failure. 97 | * 98 | * @return void 99 | */ 100 | public function failed(\Throwable $exception) 101 | { 102 | event(new RequestAuthorizationFailed($exception, $this->certificate)); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Jobs/RequestCertificate.php: -------------------------------------------------------------------------------- 1 | sync = false; 33 | $this->certificate = $certificate; 34 | $this->tries = $tries; 35 | $this->retryAfter = $retryAfter; 36 | $this->retryList = $retryList; 37 | } 38 | 39 | public function handle() 40 | { 41 | $distinguishedName = new DistinguishedName($this->certificate->domain, null, null, null, null, null, null, $this->certificate->subject_alternative_names); 42 | $csr = new CertificateRequest($distinguishedName, (new KeyPairGenerator())->generateKeyPair()); 43 | $client = LetsEncrypt::createClient(); 44 | $certificateResponse = $client->requestCertificate($this->certificate->domain, $csr); 45 | $certificate = $certificateResponse->getCertificate(); 46 | $privateKey = $csr->getKeyPair()->getPrivateKey(); 47 | 48 | if ($this->sync) { 49 | StoreCertificate::dispatchSync($this->certificate, $certificate, $privateKey, $this->tries, $this->retryAfter, $this->retryList); 50 | } else { 51 | StoreCertificate::dispatch($this->certificate, $certificate, $privateKey, $this->tries, $this->retryAfter, $this->retryList); 52 | } 53 | } 54 | 55 | protected function setSync(bool $sync) 56 | { 57 | $this->sync = $sync; 58 | } 59 | 60 | public static function dispatchNow(LetsEncryptCertificate $certificate) 61 | { 62 | $job = new static($certificate); 63 | $job->setSync(true); 64 | app(Dispatcher::class)->dispatchSync($job); 65 | } 66 | 67 | /** 68 | * Handle a job failure. 69 | * 70 | * @return void 71 | */ 72 | public function failed(\Throwable $exception) 73 | { 74 | event(new RequestCertificateFailed($exception, $this->certificate)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Jobs/StoreCertificate.php: -------------------------------------------------------------------------------- 1 | dbCertificate = $dbCertificate; 45 | $this->certificate = $certificate; 46 | $this->privateKey = $privateKey; 47 | $this->tries = $tries; 48 | $this->retryAfter = $retryAfter; 49 | $this->retryList = $retryList; 50 | } 51 | 52 | /** 53 | * Stores four files on disk: 'fullchain.pem', 'chain.pem', 'cert.pem' and 'privkey.pem' 54 | */ 55 | public function handle() 56 | { 57 | $certPem = PemEncoder::encode($this->certificate->getPEM()); 58 | $chainPem = collect($this->certificate->getIssuerChain()) 59 | ->reduce(function (string $carry, Certificate $certificate): string { 60 | return $carry . PemEncoder::encode($certificate->getPEM()); 61 | }, ''); 62 | 63 | $fullChainPem = $certPem . $chainPem; 64 | 65 | $privkeyPem = PemEncoder::encode($this->privateKey->getPEM()); 66 | 67 | $factory = PathGeneratorFactory::create(); 68 | 69 | $this->storeInPossiblyNonExistingDirectory($factory, 'cert', $certPem); 70 | $this->storeInPossiblyNonExistingDirectory($factory, 'chain', $certPem); 71 | $this->storeInPossiblyNonExistingDirectory($factory, 'fullchain', $fullChainPem); 72 | $this->storeInPossiblyNonExistingDirectory($factory, 'privkey', $privkeyPem); 73 | $this->dbCertificate->last_renewed_at = now(); 74 | $this->dbCertificate->created = true; 75 | $this->dbCertificate->save(); 76 | } 77 | 78 | /** 79 | * Creates the directory if it does not exist yet to prevent an error. 80 | * @param PathGenerator $generator 81 | * @param string $filename 82 | * @param string $contents 83 | * @throws FailedToStoreCertificate 84 | */ 85 | protected function storeInPossiblyNonExistingDirectory(PathGenerator $generator, string $filename, string $contents): void 86 | { 87 | $path = $generator->getCertificatePath($this->dbCertificate->domain, $filename . '.pem'); 88 | $directory = File::dirname($path); 89 | $fs = Storage::disk(config('lets_encrypt.certificate_disk')); 90 | if (! $fs->exists($directory)) { 91 | $fs->makeDirectory($directory); 92 | } 93 | 94 | $this->dbCertificate[$filename . '_path'] = $path; 95 | 96 | if ($fs->put($path, $contents) === false) { 97 | throw new FailedToStoreCertificate($path); 98 | } 99 | } 100 | 101 | /** 102 | * Handle a job failure. 103 | * 104 | * @return void 105 | */ 106 | public function failed(\Throwable $exception) 107 | { 108 | event(new StoreCertificateFailed($exception)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/LetsEncrypt.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 39 | self::$instance = $this; 40 | } 41 | 42 | /** 43 | * Creates a new certificate. The heavy work is pushed on the queue. 44 | * @param string $domain 45 | * @param array $chain 46 | * @return array{LetsEncryptCertificate, PendingDispatch} 47 | * @throws DomainAlreadyExists 48 | * @throws InvalidDomainException 49 | */ 50 | public function create(string $domain, array $chain = []): array 51 | { 52 | self::validateDomain($domain); 53 | self::checkDomainDoesNotExist($domain); 54 | 55 | $email = config('lets_encrypt.universal_email_address'); 56 | 57 | $certificate = LetsEncryptCertificate::create([ 58 | 'domain' => $domain, 59 | ]); 60 | 61 | return [$certificate, RegisterAccount::withChain(array_merge([ 62 | new RequestAuthorization($certificate), 63 | new RequestCertificate($certificate), 64 | ], $chain))->dispatch($email)]; 65 | } 66 | 67 | /** 68 | * Creates a certificate synchronously: it's not pushed on the queue. 69 | * This is not recommended in general, but can be useful if you're running it from the command 70 | * line or when you're trying to debug. 71 | * @param string $domain 72 | * @return LetsEncryptCertificate 73 | * @throws DomainAlreadyExists 74 | * @throws InvalidDomainException 75 | */ 76 | public function createNow(string $domain): LetsEncryptCertificate 77 | { 78 | self::validateDomain($domain); 79 | self::checkDomainDoesNotExist($domain); 80 | 81 | $email = config('lets_encrypt.universal_email_address'); 82 | 83 | $certificate = LetsEncryptCertificate::create([ 84 | 'domain' => $domain, 85 | ]); 86 | 87 | RegisterAccount::dispatchSync($email); 88 | RequestAuthorization::dispatchNow($certificate); 89 | RequestCertificate::dispatchNow($certificate); 90 | 91 | return $certificate->refresh(); 92 | } 93 | 94 | /** 95 | * Checks mainly to prevent API errors when a user passes e.g. 'https://domain.com' as a domain. This should be 96 | * 'domain.com' instead. 97 | * @param string $domain 98 | * @throws InvalidDomainException 99 | */ 100 | public function validateDomain(string $domain): void 101 | { 102 | if (Str::contains($domain, [':', '/', ','])) { 103 | throw new InvalidDomainException($domain); 104 | } 105 | } 106 | 107 | /** 108 | * @param string $domain 109 | * @throws DomainAlreadyExists 110 | */ 111 | public function checkDomainDoesNotExist(string $domain): void 112 | { 113 | if (LetsEncryptCertificate::withTrashed()->where('domain', $domain)->exists()) { 114 | throw new DomainAlreadyExists($domain); 115 | } 116 | } 117 | 118 | /** 119 | * @param string|LetsEncryptCertificate $domain 120 | * @param array $chain 121 | * @return mixed 122 | * @throws InvalidDomainException 123 | */ 124 | public function renew($domain, array $chain = []) 125 | { 126 | if (! $domain instanceof LetsEncryptCertificate) { 127 | $domain = LetsEncryptCertificate::where('domain', $domain)->first(); 128 | } 129 | 130 | $email = config('lets_encrypt.universal_email_address', null); 131 | 132 | return RegisterAccount::withChain(array_merge([ 133 | new RequestAuthorization($domain), 134 | new RequestCertificate($domain), 135 | ], $chain))->dispatch($email); 136 | } 137 | 138 | /** 139 | * @param string|LetsEncryptCertificate $domain 140 | * @return LetsEncryptCertificate 141 | * @throws InvalidDomainException 142 | */ 143 | public function renewNow($domain): LetsEncryptCertificate 144 | { 145 | if (! $domain instanceof LetsEncryptCertificate) { 146 | $domain = LetsEncryptCertificate::where('domain', $domain)->first(); 147 | } 148 | 149 | $email = config('lets_encrypt.universal_email_address', null); 150 | 151 | RegisterAccount::dispatchSync($email); 152 | RequestAuthorization::dispatchNow($domain); 153 | RequestCertificate::dispatchNow($domain); 154 | 155 | return $domain; 156 | } 157 | 158 | /** 159 | * @return AcmeClient 160 | * @throws InvalidKeyPairConfiguration 161 | */ 162 | public function createClient(): AcmeClient 163 | { 164 | $keyPair = self::getKeyPair(); 165 | $secureHttpClient = self::$instance->factory->createSecureHttpClient($keyPair); 166 | 167 | return new AcmeClient( 168 | $secureHttpClient, 169 | config('lets_encrypt.api_url', 'https://acme-staging-v02.api.letsencrypt.org/directory') 170 | ); 171 | } 172 | 173 | /** 174 | * Retrieves a key pair or creates a new one if it does not exist. 175 | * @return KeyPair 176 | * @throws InvalidKeyPairConfiguration 177 | */ 178 | protected function getKeyPair(): KeyPair 179 | { 180 | $publicKeyPath = config('lets_encrypt.public_key_path', storage_path('app/lets-encrypt/keys/account.pub.pem')); 181 | $privateKeyPath = config('lets_encrypt.private_key_path', storage_path('app/lets-encrypt/keys/account.pem')); 182 | 183 | if (! file_exists($privateKeyPath) && ! file_exists($publicKeyPath)) { 184 | $keyPairGenerator = new KeyPairGenerator(); 185 | $keyPair = $keyPairGenerator->generateKeyPair(); 186 | 187 | File::ensureDirectoryExists(File::dirname($publicKeyPath)); 188 | File::ensureDirectoryExists(File::dirname($privateKeyPath)); 189 | 190 | file_put_contents($publicKeyPath, $keyPair->getPublicKey()->getPEM()); 191 | file_put_contents($privateKeyPath, $keyPair->getPrivateKey()->getPEM()); 192 | 193 | return $keyPair; 194 | } 195 | 196 | if (! file_exists($privateKeyPath)) { 197 | throw new InvalidKeyPairConfiguration('Private key does not exist but public key does.'); 198 | } 199 | 200 | if (! file_exists($publicKeyPath)) { 201 | throw new InvalidKeyPairConfiguration('Public key does not exist but private key does.'); 202 | } 203 | 204 | $publicKey = new PublicKey(file_get_contents($publicKeyPath)); 205 | $privateKey = new PrivateKey(file_get_contents($privateKeyPath)); 206 | 207 | return new KeyPair($publicKey, $privateKey); 208 | } 209 | 210 | /** 211 | * @param string $domain 212 | * @return PendingCertificate 213 | */ 214 | public function certificate(string $domain): PendingCertificate 215 | { 216 | return new PendingCertificate($domain); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/LetsEncryptServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 19 | $this->publishes([ 20 | __DIR__.'/../config/lets_encrypt.php' => config_path('lets_encrypt.php'), 21 | ], 'lets-encrypt'); 22 | 23 | $migrationFileName = 'create_lets_encrypt_certificates_table.php'; 24 | if (! $this->migrationFileExists($migrationFileName)) { 25 | $this->publishes([ 26 | __DIR__ . "/../database/migrations/{$migrationFileName}.stub" => database_path('migrations/' . date('Y_m_d_His', time()) . '_' . $migrationFileName), 27 | ], 'lets-encrypt'); 28 | } 29 | 30 | $sanMigrationFileName = 'add_lets_encrypt_certificates_subject_alternative_names.php'; 31 | if (! $this->migrationFileExists($sanMigrationFileName)) { 32 | $this->publishes([ 33 | __DIR__ . "/../database/migrations/{$sanMigrationFileName}.stub" => database_path('migrations/' . date('Y_m_d_His', time() + 1) . '_' . $sanMigrationFileName), 34 | ], ['lets-encrypt', 'lets-encrypt-0.5']); 35 | } 36 | } 37 | 38 | $this->commands([ 39 | LetsEncryptGenerateCommand::class, 40 | ]); 41 | } 42 | 43 | public function register() 44 | { 45 | $this->mergeConfigFrom(__DIR__.'/../config/lets_encrypt.php', 'lets_encrypt'); 46 | $this->app->bind('lets-encrypt', function () { 47 | return new LetsEncrypt( 48 | new SecureHttpClientFactory( 49 | new GuzzleHttpClient(), 50 | new Base64SafeEncoder(), 51 | new KeyParser(), 52 | new DataSigner(), 53 | new ServerErrorHandler() 54 | ) 55 | ); 56 | }); 57 | } 58 | 59 | public static function migrationFileExists(string $migrationFileName): bool 60 | { 61 | $len = strlen($migrationFileName); 62 | foreach (glob(database_path("migrations/*.php")) as $filename) { 63 | if ((substr($filename, -$len) === $migrationFileName)) { 64 | return true; 65 | } 66 | } 67 | 68 | return false; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Models/LetsEncryptCertificate.php: -------------------------------------------------------------------------------- 1 | 'boolean', 46 | 'subject_alternative_names' => 'array', 47 | ]; 48 | 49 | public function newEloquentBuilder($query): LetsEncryptCertificateBuilder 50 | { 51 | return new LetsEncryptCertificateBuilder($query); 52 | } 53 | 54 | public function newCollection(array $models = []) 55 | { 56 | return new LetsEncryptCertificateCollection($models); 57 | } 58 | 59 | public function getHasExpiredAttribute(): bool 60 | { 61 | return $this->last_renewed_at && $this->last_renewed_at->diffInDays(now()) >= 90; 62 | } 63 | 64 | public function renew() 65 | { 66 | return LetsEncrypt::renew($this); 67 | } 68 | 69 | public function renewNow(): self 70 | { 71 | return LetsEncrypt::renewNow($this); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/PendingCertificate.php: -------------------------------------------------------------------------------- 1 | domain = $domain; 57 | } 58 | 59 | /** 60 | * Creates a new certificate. The heavy work is pushed on the queue. 61 | * @return LetsEncryptCertificate 62 | * @throws DomainAlreadyExists 63 | * @throws InvalidDomainException 64 | */ 65 | public function create(): LetsEncryptCertificate 66 | { 67 | LetsEncrypt::validateDomain($this->domain); 68 | LetsEncrypt::checkDomainDoesNotExist($this->domain); 69 | 70 | $email = config('lets_encrypt.universal_email_address'); 71 | 72 | $certificate = LetsEncryptCertificate::create([ 73 | 'domain' => $this->domain, 74 | 'subject_alternative_names' => $this->subjectAlternativeNames, 75 | ]); 76 | 77 | RegisterAccount::withChain(array_merge([ 78 | new RequestAuthorization( 79 | $certificate, 80 | $this->tries, 81 | $this->retryAfter, 82 | $this->retryList 83 | ), 84 | new RequestCertificate( 85 | $certificate, 86 | $this->tries, 87 | $this->retryAfter, 88 | $this->retryList 89 | ), 90 | ], $this->chain)) 91 | ->delay($this->delay) 92 | ->dispatch($email, $this->tries, $this->retryAfter, $this->retryList); 93 | 94 | return $certificate; 95 | } 96 | 97 | /** 98 | * @return LetsEncryptCertificate 99 | * @throws InvalidDomainException 100 | */ 101 | public function renew(): LetsEncryptCertificate 102 | { 103 | $certificate = LetsEncryptCertificate::where('domain', $this->domain)->first(); 104 | $email = config('lets_encrypt.universal_email_address', null); 105 | 106 | RegisterAccount::withChain(array_merge([ 107 | new RequestAuthorization( 108 | $certificate, 109 | $this->tries, 110 | $this->retryAfter, 111 | $this->retryList 112 | ), 113 | new RequestCertificate( 114 | $certificate, 115 | $this->tries, 116 | $this->retryAfter, 117 | $this->retryList 118 | ), 119 | ], $this->chain)) 120 | ->delay($this->delay) 121 | ->dispatch($email, $this->tries, $this->retryAfter, $this->retryList); 122 | 123 | return $certificate; 124 | } 125 | 126 | 127 | /** 128 | * @param array $domains 129 | * @return static 130 | */ 131 | public function setSubjectAlternativeNames(array $domains): self 132 | { 133 | $this->subjectAlternativeNames = $domains; 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * @param int $tries 140 | * @return static 141 | */ 142 | public function setTries(int $tries): self 143 | { 144 | $this->tries = $tries; 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * @param int $retryAfter 151 | * @return static 152 | */ 153 | public function retryAfter(int $retryAfter): self 154 | { 155 | $this->retryAfter = $retryAfter; 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * @param array $chain 162 | * @return static 163 | */ 164 | public function chain(array $chain): self 165 | { 166 | $this->chain = $chain; 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Calculate the number of seconds to wait before retrying the job. 173 | * 174 | * @param array $retryList 175 | * @return static 176 | */ 177 | public function setRetryList(array $retryList): self 178 | { 179 | $this->retryList = $retryList; 180 | 181 | return $this; 182 | } 183 | 184 | /** 185 | * @param \DateTimeInterface|\DateInterval|int|null $delay 186 | * @return static 187 | */ 188 | public function delay($delay): self 189 | { 190 | $this->delay = $delay; 191 | 192 | return $this; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Support/DefaultPathGenerator.php: -------------------------------------------------------------------------------- 1 | retryList)) ? $this->retryList[$this->attempts() - 1] : 0; 36 | } 37 | } 38 | --------------------------------------------------------------------------------