├── .github └── workflows │ └── run_tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── automatoes ├── __init__.py ├── acme.py ├── authorize.py ├── bin │ ├── __init__.py │ ├── automatoes-cli.py │ └── manuale-cli.py ├── cli │ ├── __init__.py │ ├── automatoes.py │ ├── commands │ │ ├── __init__.py │ │ ├── account.py │ │ ├── help.py │ │ └── order.py │ └── manuale.py ├── conf │ ├── automatoes.yml │ └── manuale.yml ├── crypto.py ├── errors.py ├── helpers.py ├── info.py ├── issue.py ├── messages.py ├── migrate.py ├── model.py ├── protocol.py ├── register.py ├── revoke.py └── upgrade.py ├── docs ├── Makefile ├── conf.py ├── features.rst └── index.rst ├── requirements ├── basic.txt ├── development.txt ├── docs.txt └── tests.txt ├── scripts ├── build.sh ├── install_cryptography.sh ├── install_pebble.sh └── pebble_service.sh ├── setup.py └── tests ├── __init__.py ├── ari_test.py ├── certs └── .keep_cert_dir ├── conf └── pebble-config.json ├── crypto_test.py ├── features ├── 00_nonce.feature ├── 01_user_management.feature ├── 03_authorize_and_issue_domains.feature ├── 04_certificate_revogation.feature ├── 05_migrate_account.feature ├── __init__.py ├── environment.py ├── sandbox │ └── account_sandbox.txt └── steps │ ├── acme_v2_steps.py │ ├── environment_steps.py │ └── migrate_steps.py ├── fixtures └── keys │ └── candango.org │ └── another │ ├── another.candango.org.chain.crt │ ├── another.candango.org.crt │ ├── another.candango.org.intermediate.crt │ └── another.candango.org.pem ├── nonce_test.py └── runtests.py /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run automatoes tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: '^1.13.1' 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Set up peeble 22 | run: | 23 | export GOPATH=~/go 24 | ./scripts/install_pebble.sh 25 | - name: Install dependencies 26 | run: | 27 | pip install -r requirements/development.txt 28 | - name: Run python unit tests 29 | run: | 30 | PYTHONPATH=$PYTHONPATH:. python tests/runtests.py 31 | - name: Run python behave tests 32 | run: | 33 | export GOPATH=~/go 34 | ./scripts/pebble_service.sh start tests/conf/pebble-config.json 35 | behave tests/features 36 | - name: Build packages 37 | run: | 38 | ./scripts/build.sh 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # PyCharm 107 | .idea 108 | 109 | # Mr Developer 110 | .mr.developer.cfg 111 | .project 112 | .pydevproject 113 | 114 | # Tests 115 | tests/features/sandbox/account.json 116 | tests/features/sandbox/user_contacts.txt 117 | tests/certs/*.pem 118 | tests/certs/localhost/ 119 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Candango Automatoes 2 | 3 | # What's new in Automatoes 0.9.7 4 | 5 | ## Oct 14, 2022 6 | 7 | We are pleased to announce the release of Automatoes 0.9.8. 8 | 9 | This release won't support Python 3.5. Use a python newer than 3.6 for now on. 10 | 11 | Here are the highlights: 12 | 13 | 14 | ## Build 15 | 16 | * Migrate from travis to github actions build #79 17 | * Depreciate python 3.5 #86 18 | 19 | ## Bugs 20 | 21 | * Registration error due to unicode to utf-8 decode error #103 22 | 23 | ## Features 24 | 25 | * Add account uri, id and status to the manuale info command #106 26 | * Make manuale register verbose feature #107 27 | 28 | 29 | 30 | # What's new in Automatoes 0.9.7 31 | 32 | ## Feb 20, 2020 33 | 34 | We are pleased to announce the release of Automatoes 0.9.7. 35 | 36 | This is a security fix that address CVE-2020-36242 updating cryptography to a 37 | patched version. 38 | 39 | We still support python 3.5 but the cryptography being installed won't be 40 | patched against CVE-2020-36242. 41 | 42 | It is recommended to upgrade your Python version as Python 3.5 is no longer 43 | maintained (end of life was September 13th, 2020) and cryptography dropped 44 | python 3.5 support. 45 | 46 | Here are the highlights: 47 | 48 | ## Security 49 | 50 | * CVE-2020-36242: Symmetrically encrypting large values can lead to integer overflow #84 51 | 52 | ## Bugs 53 | 54 | * Suppress crypto.py warning on Python 3.5 #83 55 | 56 | 57 | # What's new in Automatoes 0.9.6 58 | 59 | ## Nov 25, 2020 60 | 61 | We are pleased to announce the release of Automatoes 0.9.6. 62 | 63 | This release finally fixes the issue where an expired order will prevent the 64 | user of refreshing a certificate and adds more support to other ACME V2 clients 65 | that follow rfc8555 loosely. Tested against 66 | [Buypass GO](https://www.buypass.com/ssl/products/acme) ACME V2. 67 | 68 | Here are the highlights: 69 | 70 | ## Bugs 71 | 72 | * To renew a certificate it is necessary to delete its order file #39 73 | * Fix url handling when acme is served with paths #71 74 | 75 | ## Features 76 | 77 | * Set expiration date to the order if server won't do it #72 78 | * Show only information provided by the server with manuale info #73 79 | 80 | 81 | # What's new in Automatoes 0.9.5 82 | 83 | ## Aug 07, 2020 84 | 85 | We are pleased to announce the release of Automatoes 0.9.5. 86 | 87 | Here are the highlights: 88 | 89 | ## Bugs 90 | 91 | * Missing 'certificate' key in finalize response bug #42 92 | 93 | See cached version: https://web.archive.org/web/20200930224131/https://github.com/candango/automatoes/releases/tag/v0.9.5 94 | 95 | 96 | # What's new in Automatoes 0.9.4 97 | 98 | ## Jul 20, 2020 99 | 100 | We are pleased to announce the release of Automatoes 0.9.4. 101 | 102 | This release fixes the http authorization method and account registration 103 | broken by 0.9.3. 104 | 105 | Here are the highlights: 106 | 107 | ## Bugs 108 | 109 | * Content generated into the http challenge file is invalid #44 110 | * Cannot register account #53 111 | 112 | See cached version: https://web.archive.org/web/20200930224131/https://github.com/candango/automatoes/releases/tag/v0.9.4 113 | 114 | 115 | # What's new in Automatoes 0.9.3 116 | 117 | ## Jun 24, 2020 118 | 119 | We are pleased to announce the release of Automatoes 0.9.3. 120 | 121 | This release will detect if account is using Let's Encrypt ACME V1 uri and fix 122 | execution to run with ACME V2. 123 | A new command `manuale update` was added to fix Let's Encrypt ACME V1 uri to 124 | ACME V2 permanently. 125 | Also, we install on Pip 20.1.x and up. 126 | 127 | A gitter chat was added for faster support. Just ping us there, and we'll try 128 | to help you with your issue. 129 | 130 | Here are the highlights: 131 | 132 | ## Features 133 | 134 | * Upgrade existent account from acme v1 to acme v2 #30 135 | * Add gitter chat for faster support. feature #48 136 | 137 | ## Bugs 138 | 139 | * Handle better error codes from cli bug #41 140 | * Pip 20.1 will break installation bug #43 141 | 142 | See cached version: https://web.archive.org/web/20200930224131/https://github.com/candango/automatoes/releases/tag/v0.9.3 143 | 144 | 145 | # What's new in Automatoes 0.9.1 146 | 147 | ## Jan 29, 2020 148 | 149 | We are pleased to announce the release of Automatoes 0.9.1. 150 | 151 | This release fixes a severe bug with `manuale revoke` command and updates dependencies. 152 | 153 | Here are the highlights: 154 | 155 | ## Bugs 156 | 157 | * Revoke dies with AttributeError: 'str' object has no attribute 'public_bytes' bug severe. [#34](https://github.com/candango/automatoes/issues/34) 158 | 159 | 160 | # What's new in Automatoes 0.9.0 161 | 162 | ## Jan 21, 2020 163 | 164 | We are pleased to announce the release of Automatoes 0.9.0. 165 | 166 | Candango Automatoes as a ACME V2 replacement for ManuaLE. 167 | 168 | Here are the highlights: 169 | 170 | ## New Features 171 | 172 | * Created test environment. #3 173 | * Created mock server to test challenges. #10 174 | * Random string and uuid command line tasks. #284 175 | 176 | ## Refactory 177 | 178 | * ACME V2 account registration. [#5](https://github.com/candango/automatoes/issues/5) 179 | * ACME V2 get nonce. [#7](https://github.com/candango/automatoes/issues/7) 180 | * ACME V2 Account Info. [#16](https://github.com/candango/automatoes/issues/16) 181 | * ACME V2 Applying for Certificate Issuance. [#18](https://github.com/candango/automatoes/issues/18) 182 | * ACME V2 Certificate Revocation [#25](https://github.com/candango/automatoes/issues/25) 183 | 184 | # What's new in Automatoes 0.0.0.1 185 | 186 | ## Oct 09, 2019 187 | 188 | We are pleased to announce the release of Automatoes 0.0.0.1. 189 | 190 | Candango Automatoes initial rlease. 191 | 192 | ## Bugs 193 | 194 | * Python 3.5 depreciation notice. [#6](https://github.com/candango/automatoes/issues/6) 195 | 196 | ## Refactory 197 | 198 | * Changed license to Apache 2. [#2](https://github.com/candango/automatoes/issues/2) 199 | 200 | # Manuale (Legacy) 201 | 202 | ## 1.1.0 (January 1, 2017) 203 | 204 | * Added support for HTTP authorization. (contributed by GitHub user @mbr) 205 | 206 | * Added support for registration with an existing key. (contributed by GitHub 207 | user @haddoncd) 208 | 209 | * Using an existing CSR no longer requires the private key. (contributed by 210 | GitHub user @eroen) 211 | 212 | ## 1.0.3 (August 27, 2016) 213 | 214 | * Fixed handling of recycled authorizations: if a domain is already authorized, 215 | the server no longer allows reauthorizing it until expired. 216 | 217 | * Existing EC keys can now be used to issue certificates. (Support for 218 | generating EC keys is not yet implemented.) 219 | 220 | ## 1.0.2 (March 20, 2016) 221 | 222 | * The authorization command now outputs proper DNS record lines. 223 | 224 | ## 1.0.1 (February 9, 2016) 225 | 226 | * Private key files are now created with read permission for the owner only 227 | (`0600` mode). 228 | 229 | * The README is now converted into reStructuredText for display in PyPI. 230 | 231 | * Classified as Python 3 only in PyPI. 232 | 233 | ## 1.0.0 (February 6, 2016) 234 | 235 | Initial release. 236 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019-2025 Flavio Garcia 190 | Copyright 2016-2017 Veeti Paananen under MIT License 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include README.md 3 | include LICENSE 4 | include requirements/basic.txt 5 | include requirements/cryptography.txt 6 | include requirements/cryptography_legacy.txt 7 | include automatoes/conf/*.yml 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Candango Automat-o-es 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/automatoes.svg)](https://pypi.org/project/automatoes/) 4 | [![Number of PyPI downloads](https://img.shields.io/pypi/dm/automatoes.svg)](https://pypi.org/project/automatoes/#files) 5 | [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fcandango%2Fautomatoes%2Fbadge&style=flat)](https://actions-badge.atrox.dev/candango/automatoes/goto) 6 | 7 | 8 | Automatoes is a [Let's Encrypt](https://letsencrypt.org)/[ACME](https://github.com/ietf-wg-acme/acme/) 9 | client for advanced users and developers. It is intended to be used by anyone 10 | because we don't care if you're a robot, a processes or a person. 11 | 12 | We will keep the `manuale` command to provide manual workflow designed by the 13 | original project and to be a direct replacement from 14 | [ManuaLE](https://github.com/veeti/manuale). 15 | 16 | ## Why? 17 | 18 | Because Let's Encrypt's point is to be automatic and seamless and ManuaLE was 19 | designed to be manual. 20 | 21 | Automatoes will add automatic workflows and new features to evolve ManuaLe's 22 | legacy. The project also will keep performing maintenance tasks as bug fixes 23 | and refactory. 24 | 25 | Automatoes is an ACME V2 replacement to ManuaLE. 26 | 27 | ## Features 28 | 29 | * Simple interface with no hoops to jump through. Keys and certificate signing 30 | requests are automatically generated: no more cryptic OpenSSL one-liners. 31 | (However, you do need to know what to do with generated certificates and keys 32 | yourself!) 33 | 34 | * Support for DNS & HTTP validation. No need to figure out how to serve 35 | challenge files from a live domain. 36 | 37 | * Obviously, runs without root access. Use it from any machine you want, it 38 | doesn't care. Internet connection recommended. 39 | 40 | * Awful, undiscoverable name. 41 | 42 | * And finally, if the `openssl` binary is your spirit animal after all, you can 43 | still bring your own keys and/or CSR's. Everybody wins. 44 | 45 | ## Installation 46 | 47 | Python 3.9 or above is required. 48 | 49 | ### Using your package manager 50 | 51 | * TO DO 52 | 53 | * Package maintainers wanted: your package here? 54 | 55 | 56 | ### Using pip 57 | 58 | You can install the package from 59 | [PyPI](https://pypi.python.org/pypi/automatoes) using the `pip` tool. To do 60 | so, run `pip3 install automatoes`. 61 | 62 | If you're not using Windows or OS X pip may need to compile some of the 63 | dependencies. In this case, you need a compiler and development headers for 64 | Python, OpenSSL and libffi installed. 65 | 66 | On Debian-based distributions, these will typically be 67 | `gcc python3-dev libssl-dev libffi-dev`, and on RPM-based distributions 68 | `gcc python3-devel openssl-devel libffi-devel`. 69 | 70 | ### From the git repository 71 | 72 | git clone https://github.com/candango/automatoes ~/.automatoes 73 | cd ~/.automatoes 74 | python3 -m venv env 75 | env/bin/python setup.py install 76 | ln -s env/bin/manuale ~/.bin/ 77 | 78 | (Assuming you have a `~/.bin/` directory in your `$PATH`). 79 | 80 | ## Quick start 81 | 82 | Register an account (once): 83 | 84 | $ manuale register me@example.com 85 | 86 | Authorize one or more domains: 87 | 88 | $ manuale authorize example.com 89 | DNS verification required. Make sure these records are in place: 90 | _acme-challenge.example.com. IN TXT "(some random gibberish)" 91 | Press Enter to continue. 92 | ... 93 | 1 domain(s) authorized. Let's Encrypt! 94 | 95 | Get your certificate: 96 | 97 | $ manuale issue --output certs/ example.com 98 | ... 99 | Certificate issued. 100 | 101 | Expires: 2016-06-01 102 | SHA256: (more random gibberish) 103 | 104 | Wrote key to certs/example.com.pem 105 | Wrote certificate to certs/example.com.crt 106 | Wrote certificate with intermediate to certs/example.com.chain.crt 107 | Wrote intermediate certificate to certs/example.com.intermediate.crt 108 | 109 | Set yourself a reminder for renewal! 110 | 111 | ## Usage 112 | 113 | You need to create an account once. To do so, call `manuale register [email]`. 114 | This will create a new account key for you. Follow the registration 115 | instructions. 116 | 117 | Once that's done, you'll have your account saved in `account.json` in the 118 | current directory. You'll need this to do anything useful. Oh, and it contains 119 | your private key, so keep it safe and secure. 120 | 121 | `manuale` expects the account file to be in your working directory by default, 122 | so you'll probably want to make a specific directory to do all your certificate 123 | stuff in. Likewise, created certificates get saved in the current path by 124 | default. 125 | 126 | Next up, verify the domains you want a certificate for with 127 | `manuale authorize [domain]`. This will show you the DNS records you need to 128 | create and wait for you to do it. For example, you might do it for 129 | `example.com` and `www.example.com`. 130 | 131 | Once that's done, you can finally get down to business. 132 | Run `manuale issue example.com www.example.com` to get your certificate. 133 | It'll save the key, certificate and certificate with intermediate to the 134 | working directory. 135 | 136 | There's plenty of documentation inside each command. Run `manuale -h` for a 137 | list of commands and `manuale [command] -h` for details. 138 | 139 | ## Something different from ManuaLE? 140 | 141 | Yes and no. Mostly yes, in the background. 142 | 143 | Automatoes provides a manuale command replacement and a new automatoes command 144 | that will be added in the future. 145 | 146 | The manuale command will interface ACME V2 only as V1 is reaching 147 | [End Of Life](https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430). 148 | 149 | The account file structure from ManuaLE is maintained, no change here. 150 | 151 | For Let's Encrypt servers it is necessary to change the uri from V1 api to V2. 152 | With [#30](https://github.com/candango/automatoes/issues/30) we'll warn you 153 | about your uri being Let's Encrypt ACME V1 and run with a correct ACME V2 154 | without fixing the account.json file. 155 | 156 | To fix the account.json file permanently run `manuale upgrade` and after 157 | confirmation your account uri will be changed to the Let's Encrypt ACME V2 uri. 158 | 159 | The upgrade action will only act against an account uri from production Let's 160 | Encrypt ACME V1 otherwise nothing will be executed. 161 | 162 | ACME V2 works with an 163 | [order workflow](https://tools.ietf.org/html/rfc8555#section-7.1) that must be 164 | fulfilled. Automatoes will mimic orders in a file structure locally for better 165 | control. 166 | 167 | The manuale command will handle orders following the original project workflow 168 | with minimal changes. 169 | 170 | The automatoes command will be order based, let's talk about that when 171 | released. 172 | 173 | Here is what happens in the background(manuale replacement): 174 | 175 | > `manuale authorize domain.com other.domain.com` 176 | > 1. /acme/new-order is called and order file is stored locally at 177 | > working_directory/orders//order.json 178 | > 1. /acme/authz/challenge1 and /acme/authz/challenge2 are called and stored at 179 | > working_directory/orders/ 180 | > 1. the file name for challenges will be _challenge.json 181 | > 1. you fulfill all challenges either by dns or http, dns is default. 182 | > Just saying... you know the drill right? Same as before. 183 | > 1. manuale the Let's Encrypt! message and you can issue the certificate 184 | 185 | * If any challenge fails we delete the order file because it will be invalid 186 | in the server side. Invalid orders are considered fulfilled and not pending, 187 | we can discard them. 188 | * If you hit Ctrl+c, the order will start from the state found in the local 189 | file stored. Even challenges will be maintained, in a case when one challenge 190 | is validated and 2 are pending, if Ctrl+c was hit, we'll recognize them in a 191 | next attempt. 192 | * If you call the authorize command and there is an existent invalid order, 193 | this one will be deleted, and a new order will be created. 194 | 195 | > `manuale issue domain.com other.domain.com` 196 | 197 | > 1. /acme/order//finalize is called with the pem generated 198 | > or the one provided by you 199 | > 1. /acme/cert/ is called, and we place keys like we use to do before 200 | > 1. we're done! 201 | 202 | * If you try to issue certificates for a domain sequence and an order is pending 203 | or invalid, automatoes will ask you to run authorize before. 204 | 205 | After authorizing a domain sequence you need run issue with the same domain 206 | sequence because: 207 | 208 | 1. The order file is stored at 209 | working_directory/orders/ 210 | if we change the domain sequence a new order file will be created at 211 | working_directory/orders/ 212 | 2. The acme V2 order finalize call also requires something like this as 213 | described at 214 | [rfc8555 section-7.4](https://tools.ietf.org/html/rfc8555#section-7.4): 215 | 216 | > A request to finalize an order will result in an error if the CA is 217 | unwilling to issue a certificate corresponding to the submitted CSR. 218 | For example: 219 | > 220 | > * **If the CSR and order identifiers differ** <--- TALKING ABOUT THIS 221 | > 222 | > * If the account is not authorized for the identifiers indicated in the CSR 223 | > 224 | > * If the CSR requests extensions that the CA is not willing to include 225 | 226 | Trying to keep thing as [KISS](https://www.acronymfinder.com/KISS.html) as 227 | possible, we can complicate things later. Now we need ACME V2. 228 | 229 | To create a certificate for a domain sequence authorized by a previous order 230 | just: 231 | 232 | 1. call authorize again. Chances are that no challenge will be needed, but it 233 | depends on the ACME V2 server implementation. 234 | 1. fulfill challenge(s) if needed 235 | 1. call issue with same domain sequence authorize 236 | 1. we're done! 237 | 238 | In other words, a domain sequence defines how the order identifier is created 239 | locally. 240 | 241 | The sha256sum command from coreutils can be used if you have a bash script 242 | to monitor `manuale` execution: 243 | 244 | ``` 245 | > echo "domain.com other.domain.com" | sha256sum 246 | 83ccaf9441b1abea98837e2f4c2fc18122c0e9ee4e39dd1995387f4d5d495b69 - 247 | 248 | > echo "other.domain.com domain.com" | sha256sum 249 | d0bd2c4957537572ffb7150a7dc89e61f44f9ab603b75be481118e37ec5a6163 - 250 | ``` 251 | 252 | Storing meta files at working_directory/orders directory will let us 253 | automate things better. Don't delete those files let Automatoes handle them for 254 | you. 255 | 256 | Here are more some features we can explore with this local file structure in 257 | the future: 258 | 259 | - control and advise about limits, as Acme V2 enforce limits for opened orders 260 | per account 261 | - list orders and status (for pending orders) 262 | - create partial authorizations (that will be on automatoes command not in 263 | manuale) 264 | - SDK? 265 | - Can you imagine more? Create a feature request for us. 266 | 267 | Also, the manuale command can be called with a verbose parameter(-v) right now 268 | providing more output. 269 | 270 | ## See also 271 | 272 | * [Best practices for server configuration](https://wiki.mozilla.org/Security/Server_Side_TLS) 273 | * [Configuration generator for common servers](https://mozilla.github.io/server-side-tls/ssl-config-generator/) 274 | * [Test your server](https://www.ssllabs.com/ssltest/) 275 | * [Other clients](https://community.letsencrypt.org/t/list-of-client-implementations/2103) 276 | 277 | ## Support 278 | 279 | For direct support create a 280 | [new discussion](https://github.com/candango/automatoes/discussions/new?category=help) 281 | or a 282 | [new ticket](https://github.com/candango/automatoes/issues/new) 283 | we'll love to see how to help you. 284 | 285 | Automatoes is one of 286 | [Candango Open Source Group](http://www.candango.org/projects/) 287 | initiatives. Available under the 288 | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). 289 | 290 | This website and all documentation are licensed under 291 | [Creative Commons 3.0](http://creativecommons.org/licenses/by/3.0/). 292 | 293 | Copyright © 2019-2025 Flavio Garcia 294 | Copyright © 2016-2017 Veeti Paananen under MIT License 295 | -------------------------------------------------------------------------------- /automatoes/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2019-2025 Flavio Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | __author__ = "Flavio Garcia " 19 | 20 | __version__ = (0, 9, 13) 21 | 22 | __licence__ = "Apache License V2.0" 23 | 24 | 25 | def get_version(): 26 | if isinstance(__version__[-1], str): 27 | return '.'.join(map(str, __version__[:-1])) + __version__[-1] 28 | return ".".join(map(str, __version__)) 29 | 30 | 31 | def get_author(): 32 | return __author__.split(" <")[0] 33 | 34 | 35 | def get_author_email(): 36 | return __author__.split(" <")[1][:-1] 37 | -------------------------------------------------------------------------------- /automatoes/authorize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2020 Flavio Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | The domain authorization command. 20 | """ 21 | 22 | from . import get_version 23 | from .acme import AcmeV2 24 | from .crypto import generate_jwk_thumbprint 25 | from .errors import AutomatoesError 26 | from .model import Order 27 | 28 | from cartola import fs, sysexits 29 | import hashlib 30 | import os 31 | import sys 32 | 33 | 34 | def create_order(acme, domains, method, order_file): 35 | order = acme.new_order(domains, method) 36 | update_order(order, order_file) 37 | return order 38 | 39 | 40 | def update_order(order, order_file): 41 | fs.write(order_file, order.serialize().decode()) 42 | 43 | 44 | def clean_http_challenges(files): 45 | # Clean up created files 46 | for path in files: 47 | try: 48 | os.remove(path) 49 | except: 50 | print("Couldn't delete http challenge file {}".format(path)) 51 | 52 | 53 | def clean_challenge_file(challenge_file): 54 | try: 55 | os.remove(challenge_file) 56 | except: 57 | print("Couldn't delete challenge file {}".format(challenge_file)) 58 | 59 | 60 | def authorize(server, paths, account, domains, method, verbose=False): 61 | print("Candango Automatoes {}. Manuale replacement.\n\n".format( 62 | get_version())) 63 | 64 | current_path = paths['current'] 65 | orders_path = paths['orders'] 66 | domains_hash = hashlib.sha256( 67 | "_".join(domains).encode('ascii')).hexdigest() 68 | order_path = os.path.join(orders_path, domains_hash) 69 | order_file = os.path.join(order_path, "order.json".format(domains_hash)) 70 | 71 | if not os.path.exists(orders_path): 72 | if verbose: 73 | print("Orders path not found creating it at {}." 74 | "".format(orders_path)) 75 | os.mkdir(orders_path) 76 | os.chmod(orders_path, 0o770) 77 | else: 78 | if verbose: 79 | print("Orders path found at {}.".format(orders_path)) 80 | 81 | if not os.path.exists(order_path): 82 | if verbose: 83 | print("Current order {} path not found creating it at orders " 84 | "path.\n".format(domains_hash)) 85 | os.mkdir(order_path) 86 | os.chmod(order_path, 0o770) 87 | else: 88 | if verbose: 89 | print("Current order {} path found at orders path.\n".format( 90 | domains_hash)) 91 | 92 | method = method 93 | acme = AcmeV2(server, account) 94 | 95 | try: 96 | print("Authorizing {}.\n".format(", ".join(domains))) 97 | # Creating orders for domains if not existent 98 | if not os.path.exists(order_file): 99 | if verbose: 100 | print(" Order file not found creating it.") 101 | order = create_order(acme, domains, method, order_file) 102 | else: 103 | if verbose: 104 | print(" Found order file. Querying ACME server for current " 105 | "status.") 106 | order = Order.deserialize(fs.read(order_file)) 107 | try: 108 | server_order = acme.query_order(order) 109 | order.contents = server_order.contents 110 | except: 111 | print(" WARNING: Old order. Setting it as expired.\n") 112 | order.contents['status'] = "expired" 113 | update_order(order, order_file) 114 | 115 | if not order.expired and not order.invalid: 116 | if order.contents['status'] == 'valid': 117 | print(" Order is valid and expires at {}. Please run " 118 | "the issue " 119 | "command.\n".format(order.contents['expires'])) 120 | print(" {} domain(s) authorized. Let's Encrypt!".format( 121 | len(domains))) 122 | sys.exit(sysexits.EX_OK) 123 | else: 124 | if verbose: 125 | print(" Order still pending and expires " 126 | "at {}.\n".format(order.contents['expires'])) 127 | else: 128 | if order.invalid: 129 | print(" WARNING: Invalid order, renewing it.\n Just " 130 | "continue with the authorization when all " 131 | "verifications are in place.\n") 132 | else: 133 | print(" WARNING: Expired order. Renewing order.\n") 134 | os.remove(order_file) 135 | order = create_order(acme, domains, method, order_file) 136 | update_order(order, order_file) 137 | 138 | pending_challenges = [] 139 | 140 | for challenge in acme.get_order_challenges(order): 141 | print(" Requesting challenge for {}.".format(challenge.domain)) 142 | if challenge.status == 'valid': 143 | print(" {} is already authorized until {}.".format( 144 | challenge.domain, challenge.expires)) 145 | continue 146 | else: 147 | challenge_file = os.path.join(order_path, challenge.file_name) 148 | if verbose: 149 | print(" Creating challenge file {}.\n".format( 150 | challenge.file_name)) 151 | fs.write(challenge_file, challenge.serialize().decode()) 152 | pending_challenges.append(challenge) 153 | 154 | # Quit if nothing to authorize 155 | if not pending_challenges: 156 | print("\nAll domains are already authorized, exiting.") 157 | sys.exit(sysexits.EX_OK) 158 | 159 | files = set() 160 | if method == 'dns': 161 | print("\n DNS verification required. Make sure these TXT records" 162 | " are in place:\n") 163 | for challenge in pending_challenges: 164 | print(" _acme-challenge.{}. IN TXT " 165 | "\"{}\"".format(challenge.domain, challenge.key)) 166 | elif method == 'http': 167 | print("\n HTTP verification required. Make sure these files are " 168 | "in place:\n") 169 | for challenge in pending_challenges: 170 | token = challenge.contents['token'] 171 | # path sanity check 172 | assert (token and os.path.sep not in token and '.' not in 173 | token) 174 | files.add(token) 175 | fs.write( 176 | os.path.join(current_path, token), 177 | "%s.%s" % (token, 178 | generate_jwk_thumbprint(account.key)) 179 | ) 180 | print(" http://{}/.well-known/acme-challenge/{}".format( 181 | challenge.domain, token)) 182 | 183 | print("\n The necessary files have been written to the current " 184 | "directory.\n") 185 | # Wait for the user to complete the challenges 186 | input("\nPress Enter to continue.\n") 187 | 188 | # Validate challenges 189 | done, failed, pending = set(), set(), set() 190 | for challenge in pending_challenges: 191 | print(" {}: waiting for verification. Checking in 5 " 192 | "seconds.".format(challenge.domain)) 193 | response = acme.verify_order_challenge(challenge, 5, 1) 194 | if response['status'] == "valid": 195 | print(" {}: OK! Authorization lasts until {}.".format( 196 | challenge.domain, challenge.expires)) 197 | done.add(challenge.domain) 198 | elif response['status'] == 'invalid': 199 | print(" {}: {} ({})".format( 200 | challenge.domain, 201 | response['error']['detail'], 202 | response['error']['type']) 203 | ) 204 | failed.add(challenge.domain) 205 | break 206 | else: 207 | print("{}: Pending!".format(challenge.domain)) 208 | pending.add(challenge.domain) 209 | break 210 | 211 | challenge_file = os.path.join(order_path, challenge.file_name) 212 | # Print results 213 | if failed: 214 | print(" {} domain(s) authorized, {} failed.".format( 215 | len(done), 216 | len(failed), 217 | )) 218 | print(" Authorized: {}".format(' '.join(done) or "N/A")) 219 | print(" Failed: {}".format(' '.join(failed))) 220 | print(" WARNING: The current order will be invalidated. " 221 | "Try again.") 222 | if verbose: 223 | print(" Deleting invalid challenge file {}.\n".format( 224 | challenge.file_name)) 225 | clean_challenge_file(challenge_file) 226 | os.remove(order_file) 227 | os.rmdir(order_path) 228 | if method == 'http': 229 | print(files) 230 | clean_http_challenges(files) 231 | sys.exit(sysexits.EX_FATAL_ERROR) 232 | else: 233 | if pending: 234 | print(" {} domain(s) authorized, {} pending.".format( 235 | len(done), 236 | len(pending))) 237 | print(" Authorized: {}".format(' '.join(done) or "N/A")) 238 | print(" Pending: {}".format(' '.join(pending))) 239 | print(" Try again.") 240 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 241 | else: 242 | if verbose: 243 | print(" Deleting valid challenge file {}.".format( 244 | challenge.file_name)) 245 | clean_challenge_file(challenge_file) 246 | if verbose: 247 | print(" Querying ACME server for current status.\n") 248 | server_order = acme.query_order(order) 249 | order.contents = server_order.contents 250 | update_order(order, order_file) 251 | print(" {} domain(s) authorized. Let's Encrypt!".format( 252 | len(done))) 253 | if method == 'http': 254 | clean_http_challenges(files) 255 | sys.exit(sysexits.EX_OK) 256 | except IOError as e: 257 | print("A connection or service error occurred. Aborting.") 258 | raise AutomatoesError(e) 259 | -------------------------------------------------------------------------------- /automatoes/bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candango/automatoes/d80ae4c83e8ce70812878c613227a7c036ce591a/automatoes/bin/__init__.py -------------------------------------------------------------------------------- /automatoes/bin/automatoes-cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # Copyright 2019-2023 Flavio Garcia 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | from automatoes.cli.automatoes import automatoes_cli 19 | 20 | 21 | if __name__ == "__main__": 22 | automatoes_cli() 23 | -------------------------------------------------------------------------------- /automatoes/bin/manuale-cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # Copyright 2019-2020 Flavio Garcia 5 | # Copyright 2016-2017 Veeti Paananen under MIT License 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | from automatoes.cli import manuale_main 20 | 21 | if __name__ == "__main__": 22 | manuale_main() 23 | -------------------------------------------------------------------------------- /automatoes/cli/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2019-2023 Flávio Gonçalves Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | The command line interface. 20 | """ 21 | from .. import get_version, messages 22 | from ..authorize import authorize 23 | from ..issue import issue 24 | from ..info import info 25 | from ..migrate import migrate 26 | from ..model import Account 27 | from ..register import register 28 | from ..revoke import revoke 29 | from ..upgrade import upgrade 30 | from ..errors import AutomatoesError 31 | 32 | import argparse 33 | from cartola import sysexits 34 | import logging 35 | import os 36 | import sys 37 | 38 | 39 | logger = logging.getLogger(__name__) 40 | 41 | # Defaults 42 | LETS_ENCRYPT_PRODUCTION = "https://acme-v02.api.letsencrypt.org/" 43 | DEFAULT_ACCOUNT_PATH = 'account.json' 44 | DEFAULT_CERT_KEY_SIZE = 4096 45 | 46 | AUTOMATOES_ROOT = os.path.abspath( 47 | os.path.join(os.path.dirname(__file__), "..", "..")) 48 | AUTOMATOES_CONFIG_PATH = os.path.join(AUTOMATOES_ROOT, "automatoes", "conf") 49 | AUTOMATOES_CONFIG_FILE = os.path.join(AUTOMATOES_CONFIG_PATH, "automatoes.yml") 50 | 51 | 52 | # Command handlers 53 | def _register(args): 54 | verbose = False 55 | if args.verbose > 0: 56 | verbose = True 57 | register( 58 | server=args.server, 59 | account_path=args.account, 60 | email=args.email, 61 | key_file=args.key_file, 62 | verbose=verbose 63 | ) 64 | 65 | 66 | def _authorize(args): 67 | paths = get_paths(args.account) 68 | account = load_account(args.account) 69 | verbose = False 70 | if args.verbose > 0: 71 | verbose = True 72 | authorize(args.server, paths, account, args.domain, args.method, verbose) 73 | 74 | 75 | def _issue(args): 76 | paths = get_paths(args.account) 77 | account = load_account(args.account) 78 | verbose = False 79 | if args.verbose > 0: 80 | verbose = True 81 | issue( 82 | server=args.server, 83 | paths=paths, 84 | account=account, 85 | domains=args.domain, 86 | key_size=args.key_size, 87 | key_file=args.key_file, 88 | csr_file=args.csr_file, 89 | output_path=args.output, 90 | output_filename=args.output_filename, 91 | must_staple=args.ocsp_must_staple, 92 | verbose=verbose 93 | ) 94 | 95 | 96 | def _revoke(args): 97 | account = load_account(args.account) 98 | revoke( 99 | server=args.server, 100 | account=account, 101 | certificate=args.certificate 102 | ) 103 | 104 | 105 | def _info(args): 106 | paths = get_paths(args.account) 107 | account = load_account(args.account) 108 | info(args.server, account, paths) 109 | 110 | 111 | def _upgrade(args): 112 | account_path = args.account 113 | account = load_account(args.account) 114 | upgrade(args.server, account, account_path) 115 | 116 | 117 | def _migrate(args): 118 | migrate(account_path=args.account, certbot_path=args.certbot_path) 119 | 120 | 121 | def get_paths(account_file): 122 | current_path = os.path.dirname(os.path.abspath(account_file)) 123 | return { 124 | 'authorizations': os.path.join(current_path, "authorizations"), 125 | 'current': current_path, 126 | 'orders': os.path.join(current_path, "orders"), 127 | } 128 | 129 | 130 | def get_meta_paths(path): 131 | return { 132 | 'orders': os.path.join(path, "orders"), 133 | 'authorizations': os.path.join(path, "authorizations"), 134 | } 135 | 136 | 137 | def load_account(path): 138 | # Show a more descriptive message if the file doesn't exist. 139 | if not os.path.exists(path): 140 | logger.error("Couldn't find an account file at {}.".format(path)) 141 | logger.error("Are you in the right directory? Did you register yet?") 142 | logger.error("Run 'automatoes -h' for instructions.") 143 | raise AutomatoesError() 144 | 145 | try: 146 | # TODO: Use cartola fs.read here 147 | with open(path, 'rb') as f: 148 | return Account.deserialize(f.read()) 149 | except (ValueError, IOError) as e: 150 | logger.error("Couldn't read account file. Aborting.") 151 | raise AutomatoesError(e) 152 | 153 | 154 | class Formatter(argparse.ArgumentDefaultsHelpFormatter, 155 | argparse.RawDescriptionHelpFormatter): 156 | pass 157 | 158 | 159 | def automatoes_main(): 160 | print("The automatoes command is not implemented yet.") 161 | 162 | 163 | # Where it all begins. 164 | def manuale_main(): 165 | parser = argparse.ArgumentParser( 166 | description=messages.DESCRIPTION, 167 | formatter_class=Formatter, 168 | ) 169 | subparsers = parser.add_subparsers() 170 | 171 | # Server switch 172 | parser.add_argument('--server', '-s', 173 | help=messages.OPTION_SERVER_HELP, 174 | default=LETS_ENCRYPT_PRODUCTION) 175 | parser.add_argument('--account', '-a', 176 | help=messages.OPTION_ACCOUNT_HELP, 177 | default=DEFAULT_ACCOUNT_PATH) 178 | 179 | # Verbosity 180 | parser.add_argument('--verbose', '-v', action="count", 181 | help="Set verbose mode", default=0) 182 | 183 | # Account creation 184 | register_sub = subparsers.add_parser( 185 | 'register', 186 | help="Create a new account and register", 187 | description=messages.DESCRIPTION_REGISTER, 188 | formatter_class=Formatter, 189 | ) 190 | register_sub.add_argument('email', type=str, help="Account e-mail address") 191 | register_sub.add_argument('--key-file', '-k', 192 | help="Existing key file to use for the account") 193 | register_sub.set_defaults(func=_register) 194 | 195 | # Domain verification 196 | authorize_sub = subparsers.add_parser( 197 | 'authorize', 198 | help="Verify domain ownership", 199 | description=messages.DESCRIPTION_AUTHORIZE, 200 | formatter_class=Formatter, 201 | ) 202 | authorize_sub.add_argument('domain', 203 | help="One or more domain names to authorize", 204 | nargs='+') 205 | authorize_sub.add_argument('--method', 206 | '-m', 207 | help="Authorization method", 208 | choices=('dns', 'http'), 209 | default='dns') 210 | authorize_sub.set_defaults(func=_authorize) 211 | 212 | # Certificate issuance 213 | issue_sub = subparsers.add_parser( 214 | 'issue', 215 | help="Request a new certificate", 216 | description=messages.DESCRIPTION_ISSUE, 217 | formatter_class=Formatter, 218 | ) 219 | issue_sub.add_argument( 220 | 'domain', 221 | help="One or more domain names to include in the certificate", 222 | nargs='+') 223 | issue_sub.add_argument('--key-size', '-b', 224 | help="The key size to use for the certificate", 225 | type=int, default=DEFAULT_CERT_KEY_SIZE) 226 | issue_sub.add_argument('--key-file', '-k', 227 | help="Existing key file to use for the certificate") 228 | issue_sub.add_argument('--csr-file', help="Existing signing request to use") 229 | issue_sub.add_argument('--output', '-o', 230 | help="The output directory for created objects", 231 | default='.') 232 | issue_sub.add_argument('--output-filename', 233 | help="The filename base for created objects", 234 | default=None) 235 | issue_sub.add_argument('--ocsp-must-staple', 236 | dest='ocsp_must_staple', 237 | help="CSR: Request OCSP Must-Staple extension", 238 | action='store_true') 239 | issue_sub.add_argument('--no-ocsp-must-staple', 240 | dest='ocsp_must_staple', 241 | help=argparse.SUPPRESS, 242 | action='store_false') 243 | issue_sub.set_defaults(func=_issue, ocsp_must_staple=False) 244 | 245 | # Certificate revocation 246 | revoke_sub = subparsers.add_parser( 247 | 'revoke', 248 | help="Revoke an issued certificate", 249 | description=messages.DESCRIPTION_REVOKE, 250 | formatter_class=Formatter, 251 | ) 252 | revoke_sub.add_argument("certificate", help="The certificate file to " 253 | "revoke") 254 | revoke_sub.set_defaults(func=_revoke) 255 | 256 | # Account info 257 | info_sub = subparsers.add_parser( 258 | 'info', 259 | help="Display account information", 260 | description=messages.DESCRIPTION_INFO, 261 | formatter_class=Formatter, 262 | ) 263 | info_sub.set_defaults(func=_info) 264 | 265 | # Account upgrade 266 | upgrade_sub = subparsers.add_parser( 267 | 'upgrade', 268 | help="Upgrade account's uri from Let's Encrypt ACME V1 to V2", 269 | description=messages.DESCRIPTION_UPGRADE, 270 | formatter_class=Formatter, 271 | ) 272 | upgrade_sub.set_defaults(func=_upgrade) 273 | 274 | # Migrate an account from certbot 275 | migrate_sub = subparsers.add_parser( 276 | "migrate", 277 | help="Migrate a certbot account to automatoes format", 278 | description=messages.DESCRIPTION_MIGRATE, 279 | formatter_class=Formatter, 280 | ) 281 | migrate_sub.add_argument("-c", "--certbot-path", 282 | help="Directory path where the account files are" 283 | "located") 284 | migrate_sub.set_defaults(func=_migrate) 285 | 286 | # Version 287 | version = subparsers.add_parser("version", help="Show the version number") 288 | version.set_defaults(func=lambda *args: logger.info( 289 | "automatoes {}\n\nThis tool is a full manuale " 290 | "replacement.\nJust run manuale instead of automatoes" 291 | ".".format(get_version()))) 292 | 293 | # Parse 294 | args = parser.parse_args() 295 | if not hasattr(args, 'func'): 296 | parser.print_help() 297 | sys.exit(sysexits.EX_MISUSE) 298 | 299 | # Set up logging 300 | root = logging.getLogger('automatoes') 301 | root.setLevel(logging.INFO) 302 | handler = logging.StreamHandler(sys.stderr) 303 | handler.setFormatter(logging.Formatter("%(message)s")) 304 | root.addHandler(handler) 305 | 306 | # Let's encrypt 307 | try: 308 | args.func(args) 309 | except AutomatoesError as e: 310 | if str(e): 311 | logger.error(e) 312 | sys.exit(sysexits.EX_SOFTWARE) 313 | except KeyboardInterrupt: 314 | logger.error("") 315 | logger.error("Interrupted.") 316 | sys.exit(sysexits.EX_TERMINATED_BY_CRTL_C) 317 | except Exception as e: 318 | logger.error("Oops! An unhandled error occurred. Please file a bug.") 319 | logger.exception(e) 320 | sys.exit(sysexits.EX_CATCHALL) 321 | -------------------------------------------------------------------------------- /automatoes/cli/automatoes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2019-2023 Flávio Gonçalves Garcia 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from . import DEFAULT_ACCOUNT_PATH, LETS_ENCRYPT_PRODUCTION 18 | from . import load_account 19 | from .. import messages 20 | from ..errors import AutomatoesError 21 | 22 | from cartola import config, sysexits 23 | import click 24 | import os 25 | import taskio 26 | from taskio.core import TaskioCliContext 27 | 28 | AUTOMATOES_ROOT = os.path.abspath( 29 | os.path.join(os.path.dirname(__file__), "..", "..")) 30 | AUTOMATOES_CONFIG_PATH = os.path.join(AUTOMATOES_ROOT, "automatoes", "conf") 31 | AUTOMATOES_CONFIG_FILE = os.path.join(AUTOMATOES_CONFIG_PATH, "automatoes.yml") 32 | 33 | 34 | class AutomatoesCliContext(TaskioCliContext): 35 | 36 | def __init__(self, **kwargs): 37 | super().__init__(**kwargs) 38 | self.AUTOMATOES_ROOT = AUTOMATOES_ROOT 39 | self.AUTOMATOES_CONFIG_PATH = AUTOMATOES_CONFIG_PATH 40 | self.AUTOMATOES_CONFIG_FILE = AUTOMATOES_CONFIG_FILE 41 | self.current_directory = os.getcwd() 42 | self.account = None 43 | self.server = None 44 | self.verbose = False 45 | self.root = None 46 | 47 | @property 48 | def account_files(self): 49 | return [account_file for account_file in 50 | os.listdir(self.current_directory) 51 | if "account.json" in account_file] 52 | 53 | 54 | pass_context = click.make_pass_decorator(AutomatoesCliContext, 55 | ensure=True) 56 | 57 | 58 | @taskio.root(taskio_conf=config.load_yaml_file(AUTOMATOES_CONFIG_FILE)) 59 | @click.option("-a", "--account", help=messages.OPTION_ACCOUNT_HELP, 60 | default=DEFAULT_ACCOUNT_PATH, show_default=True) 61 | @click.option("-s", "--server", help=messages.OPTION_SERVER_HELP, 62 | default=LETS_ENCRYPT_PRODUCTION, show_default=True) 63 | @pass_context 64 | def automatoes_cli(ctx: AutomatoesCliContext, account, server): 65 | """ Interact with ACME certification authorities such as Let's Encrypt. 66 | 67 | No idea what you're doing? Register an account, authorize your domains and 68 | issue a certificate or two. Call a command with -h for more instructions. 69 | """ 70 | try: 71 | account_model = load_account(account) 72 | ctx.account = account_model 73 | except AutomatoesError as ae: 74 | print(ae) 75 | ctx.server = server 76 | -------------------------------------------------------------------------------- /automatoes/cli/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candango/automatoes/d80ae4c83e8ce70812878c613227a7c036ce591a/automatoes/cli/commands/__init__.py -------------------------------------------------------------------------------- /automatoes/cli/commands/account.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2019-2023 Flávio Gonçalves Garcia 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from ..automatoes import pass_context, AutomatoesCliContext 18 | from automatoes.model import Account 19 | from cartola import fs 20 | import taskio 21 | 22 | 23 | @taskio.group(name="account", short_help="Group with commands related to " 24 | "account management") 25 | @pass_context 26 | def account(ctx): 27 | pass 28 | 29 | 30 | @account.command(name="list", short_help="List accounts") 31 | @pass_context 32 | def account_list(ctx: AutomatoesCliContext): 33 | from urllib.parse import urlparse 34 | print("Id\t\t\tServer") 35 | for account_file in ctx.account_files: 36 | default_account = True if account_file == "account.json" else False 37 | # if default_account: 38 | # print("(Default Account)", end=" ") 39 | _account = Account.deserialize(fs.read(account_file)) 40 | parsedurl = urlparse(_account.uri) 41 | account_id = parsedurl.path.split("/")[-1] 42 | account_server = "%s://%s" % (parsedurl.scheme, parsedurl.netloc) 43 | print("%s\t\t%s" % (account_id, account_server)) 44 | -------------------------------------------------------------------------------- /automatoes/cli/commands/help.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2019-2023 Flávio Gonçalves Garcia 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from ..automatoes import AutomatoesCliContext, pass_context 18 | import click 19 | 20 | 21 | @click.command(short_help="Show the list of commands") 22 | @pass_context 23 | def commands(ctx: AutomatoesCliContext): 24 | rv = [] 25 | groups = [] 26 | for source in ctx.context.loader.sources: 27 | for key, item in source.__dict__.items(): 28 | if isinstance(item, click.Command): 29 | if isinstance(item, click.Group): 30 | groups.append(item) 31 | rv.append(item.name) 32 | for group in groups: 33 | for key, item in group.commands.items(): 34 | if item.name in rv: 35 | rv.remove(item.name) 36 | rv.sort() 37 | 38 | print(rv) 39 | print(groups) 40 | 41 | print("Test cli1 command") 42 | 43 | 44 | @click.command(name="help", short_help="Show the help about a command") 45 | def _help(): 46 | print("Test cli1 command") 47 | -------------------------------------------------------------------------------- /automatoes/cli/commands/order.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2019-2023 Flávio Gonçalves Garcia 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from ..automatoes import pass_context 18 | import taskio 19 | 20 | 21 | @taskio.group(name="order", short_help="Group with commands related to order " 22 | "management") 23 | @pass_context 24 | def order(ctx): 25 | pass 26 | 27 | 28 | @order.command(name="list", short_help="List orders") 29 | @pass_context 30 | def order_list(ctx): 31 | print("List orders") 32 | -------------------------------------------------------------------------------- /automatoes/cli/manuale.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candango/automatoes/d80ae4c83e8ce70812878c613227a7c036ce591a/automatoes/cli/manuale.py -------------------------------------------------------------------------------- /automatoes/conf/automatoes.yml: -------------------------------------------------------------------------------- 1 | taskio: 2 | program: 3 | name: automatoes 4 | version: automatoes.get_version 5 | sources: 6 | - automatoes.cli.commands.account 7 | - automatoes.cli.commands.help 8 | - automatoes.cli.commands.order 9 | -------------------------------------------------------------------------------- /automatoes/conf/manuale.yml: -------------------------------------------------------------------------------- 1 | taskio: 2 | program: 3 | name: manuale 4 | version: automatoes.get_version 5 | sources: 6 | - automatoes.cli.commands.account 7 | - automatoes.cli.commands.help 8 | - automatoes.cli.commands.order 9 | -------------------------------------------------------------------------------- /automatoes/crypto.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2025 Flavio Garcia 2 | # Copyright 2016-2017 Veeti Paananen under MIT License 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | Cryptography, hopefully mostly correct. 18 | """ 19 | 20 | import base64 21 | import binascii 22 | import json 23 | import logging 24 | from cryptography import x509 25 | from cryptography.x509 import NameOID, DNSName 26 | from cryptography.hazmat.backends import default_backend 27 | from cryptography.hazmat.primitives.asymmetric import padding 28 | from cryptography.hazmat.primitives.asymmetric.rsa import ( 29 | generate_private_key, RSAPrivateKey, RSAPrivateNumbers, RSAPublicNumbers 30 | ) 31 | from cryptography.hazmat.primitives.asymmetric.ec import ( 32 | EllipticCurvePrivateKey, 33 | ) 34 | from cryptography.hazmat.primitives.serialization import ( 35 | load_pem_private_key, 36 | Encoding, 37 | PrivateFormat, 38 | NoEncryption, 39 | ) 40 | from cryptography.hazmat.primitives import hashes 41 | 42 | import re 43 | 44 | logger = logging.getLogger(__name__) 45 | 46 | 47 | def jose_b64(data): 48 | """ 49 | Encodes data with JOSE/JWS base 64 encoding. 50 | """ 51 | return base64.urlsafe_b64encode(data).decode('ascii').replace('=', '') 52 | 53 | 54 | def generate_rsa_key(size=2048): 55 | """ 56 | Generates a new RSA private key. 57 | """ 58 | return generate_private_key(65537, size, default_backend()) 59 | 60 | 61 | def generate_rsa_key_from_parameters( 62 | p, q, d, dmp1, dmq1, iqmp, e, n 63 | ) -> RSAPrivateKey: 64 | """ 65 | Note: from certbot dp is dmp1, dq is dmq1 and qi is iqmp 66 | """ 67 | public_numbers = RSAPublicNumbers(e, n) 68 | return RSAPrivateNumbers( 69 | p, q, d, dmp1, dmq1, iqmp, public_numbers 70 | ).private_key(default_backend()) 71 | 72 | 73 | def data_to_hex(data: bytes) -> bytes: 74 | missing_padding = 4 - len(data) % 4 75 | if missing_padding: 76 | data += b"=" * missing_padding 77 | return b"0x" + binascii.hexlify(base64.b64decode(data, b'-_')).upper() 78 | 79 | 80 | def certbot_key_data_to_int(key_data: dict) -> dict: 81 | key_data_int = {} 82 | for key, value in key_data.items(): 83 | key_data_int[key] = int(data_to_hex(value.encode()), 16) 84 | return key_data_int 85 | 86 | 87 | def generate_ari_data(cert): 88 | aki_b64 = base64.urlsafe_b64encode(get_certificate_aki(cert).encode()) 89 | serial_b64 = base64.urlsafe_b64encode( 90 | get_certificate_serial(cert).encode()) 91 | return f"{aki_b64}.{serial_b64}" 92 | 93 | 94 | def generate_header(account_key): 95 | """ 96 | Creates a new request header for the specified account key. 97 | """ 98 | numbers = account_key.public_key().public_numbers() 99 | e = numbers.e.to_bytes((numbers.e.bit_length() // 8 + 1), byteorder='big') 100 | n = numbers.n.to_bytes((numbers.n.bit_length() // 8 + 1), byteorder='big') 101 | if n[0] == 0: # for strict JWK 102 | n = n[1:] 103 | return { 104 | 'alg': 'RS256', 105 | 'jwk': { 106 | 'kty': 'RSA', 107 | 'e': jose_b64(e), 108 | 'n': jose_b64(n), 109 | }, 110 | } 111 | 112 | 113 | def generate_jwk_thumbprint(account_key): 114 | """ 115 | Generates a JWK thumbprint for the specified account key. 116 | """ 117 | jwk = generate_header(account_key)['jwk'] 118 | as_json = json.dumps(jwk, sort_keys=True, separators=(',', ':')) 119 | 120 | sha256 = hashes.Hash(hashes.SHA256(), default_backend()) 121 | sha256.update(as_json.encode('utf-8')) 122 | 123 | return jose_b64(sha256.finalize()) 124 | 125 | 126 | def sign_request(key, header, protected_header, payload): 127 | """ 128 | Creates a JSON Web Signature for the request header and payload using the 129 | specified account key. 130 | """ 131 | protected = jose_b64(json.dumps(protected_header).encode('utf8')) 132 | payload = jose_b64(json.dumps(payload).encode('utf8')) 133 | data = "{%s}.{%s}" % (protected, payload) 134 | signed_data = key.sign(data.encode("ascii"), padding.PKCS1v15(), 135 | hashes.SHA256()) 136 | return json.dumps({ 137 | 'header': header, 138 | 'protected': protected, 139 | 'payload': payload, 140 | 'signature': jose_b64(signed_data), 141 | }) 142 | 143 | 144 | def sign_request_v2(key, protected_header, payload): 145 | """ 146 | Creates a JSON Web Signature for the request header and payload using the 147 | specified account key. 148 | """ 149 | protected = jose_b64(json.dumps(protected_header).encode('utf8')) 150 | # Forced payload none for Post-as-Get 151 | if payload is not None and payload != "": 152 | payload = jose_b64(json.dumps(payload).encode('utf8')) 153 | elif payload is None: 154 | payload = "" 155 | data = "{protected}.{payload}".format(protected=protected, payload=payload) 156 | signed_data = key.sign(data.encode("ascii"), padding.PKCS1v15(), 157 | hashes.SHA256()) 158 | return json.dumps({ 159 | 'protected': protected, 160 | 'payload': payload, 161 | 'signature': jose_b64(signed_data), 162 | }) 163 | 164 | 165 | def load_private_key(data): 166 | """ 167 | Loads a PEM-encoded private key. 168 | """ 169 | key = load_pem_private_key(data, password=None, backend=default_backend()) 170 | if not isinstance(key, (RSAPrivateKey, EllipticCurvePrivateKey)): 171 | raise ValueError("Key is not a private RSA or EC key.") 172 | elif isinstance(key, RSAPrivateKey) and key.key_size < 2048: 173 | raise ValueError("The key must be 2048 bits or longer.") 174 | 175 | return key 176 | 177 | 178 | def export_private_key(key): 179 | """ 180 | Exports a private key in OpenSSL PEM format. 181 | """ 182 | return key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, 183 | NoEncryption()) 184 | 185 | 186 | def create_csr(key, domains, must_staple=False): 187 | """ 188 | Creates a CSR in DER format for the specified key and domain names. 189 | """ 190 | assert domains 191 | name = x509.Name([ 192 | x509.NameAttribute(NameOID.COMMON_NAME, domains[0]), 193 | ]) 194 | san = x509.SubjectAlternativeName( 195 | [x509.DNSName(domain) for domain in domains]) 196 | csr = (x509.CertificateSigningRequestBuilder() 197 | .subject_name(name).add_extension(san, critical=False)) 198 | if must_staple: 199 | ocsp_must_staple = x509.TLSFeature( 200 | features=[x509.TLSFeatureType.status_request]) 201 | csr = csr.add_extension(ocsp_must_staple, critical=False) 202 | return csr.sign(key, hashes.SHA256(), default_backend()) 203 | 204 | 205 | def export_csr_for_acme(csr): 206 | """ 207 | Exports a X.509 CSR for the ACME protocol (JOSE Base64 DER). 208 | """ 209 | return export_certificate_for_acme(csr) 210 | 211 | 212 | def load_csr(data): 213 | """ 214 | Loads a PEM X.509 CSR. 215 | """ 216 | return x509.load_pem_x509_csr(data, default_backend()) 217 | 218 | 219 | def load_der_certificate(data): 220 | """ 221 | Loads a DER X.509 certificate. 222 | """ 223 | return x509.load_der_x509_certificate(data, default_backend()) 224 | 225 | 226 | def load_pem_certificate(data): 227 | """ 228 | Loads a PEM X.509 certificate. 229 | """ 230 | return x509.load_pem_x509_certificate(data, default_backend()) 231 | 232 | 233 | def get_issuer_certificate_domain_name(cert): 234 | for cn in cert.subject: 235 | return cn.value 236 | 237 | 238 | def get_certificate_aki(cert): 239 | for ext in cert.extensions: 240 | if isinstance(ext.value, x509.AuthorityKeyIdentifier): 241 | hex = ext.value.key_identifier.hex() 242 | return ":".join(hex[i:i+2] for i in range(0, len(hex), 2)) 243 | 244 | 245 | def get_certificate_serial(cert): 246 | hex = format(cert.serial_number, "x") 247 | return ":".join(hex[i:i+2] for i in range(0, len(hex), 2)) 248 | 249 | 250 | def get_certificate_domain_name(cert): 251 | for ext in cert.extensions: 252 | if isinstance(ext.value, x509.SubjectAlternativeName): 253 | return ext.value.get_values_for_type(DNSName)[0] 254 | 255 | 256 | def get_certificate_domains(cert): 257 | """ 258 | Gets a list of all Subject Alternative Names in the specified certificate. 259 | """ 260 | for ext in cert.extensions: 261 | ext = ext.value 262 | if isinstance(ext, x509.SubjectAlternativeName): 263 | return ext.get_values_for_type(x509.DNSName) 264 | return [] 265 | 266 | 267 | def export_pem_certificate(cert): 268 | """ 269 | Exports a X.509 certificate as PEM. 270 | """ 271 | return cert.public_bytes(Encoding.PEM) 272 | 273 | 274 | def export_certificate_for_acme(cert): 275 | """ 276 | Exports a X.509 certificate for the ACME protocol (JOSE Base64 DER). 277 | """ 278 | return jose_b64(cert.public_bytes(Encoding.DER)) 279 | 280 | 281 | def strip_certificates(data): 282 | p = re.compile("(?s)-----BEGIN CERTIFICATE-----\n.+?" 283 | "-----END CERTIFICATE-----\n") 284 | stripped_data = [] 285 | for cert in p.findall(data.decode()): 286 | stripped_data.append(cert.encode()) 287 | return stripped_data 288 | -------------------------------------------------------------------------------- /automatoes/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019 Flavio Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | class AutomatoesError(Exception): 19 | pass 20 | 21 | 22 | class AcmeError(IOError): 23 | def __init__(self, response): 24 | message = "The ACME request failed." 25 | try: 26 | details = response.json() 27 | self.type = details.get('type', 'unknown') 28 | message = "{} (type {}, HTTP {})".format(details.get('detail'), 29 | self.type, response.status_code) 30 | except (ValueError, TypeError, AttributeError): 31 | pass 32 | super().__init__(message) 33 | 34 | 35 | class AccountAlreadyExistsError(AcmeError): 36 | 37 | def __init__(self, response, existing_uri): 38 | super().__init__(response) 39 | self.existing_uri = existing_uri 40 | -------------------------------------------------------------------------------- /automatoes/helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2022 Flávio Gonçalves Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | import locale 18 | import sys 19 | 20 | 21 | def confirm(msg, default=True, verbose=False): 22 | if verbose: 23 | print("Preferred encoding: %s" % locale.getpreferredencoding()) 24 | print("Default locale:\n lang: %s, encoding: %s" 25 | % locale.getdefaultlocale()) 26 | print("Current input encoding: %s" % sys.stdin.encoding) 27 | print("Current output encoding: %s" % sys.stdout.encoding) 28 | print("Byte order: %s\n" % sys.byteorder) 29 | no_encode_found = False 30 | while True: 31 | choices = "Y/n" if default else "y/N" 32 | try: 33 | answer, encoding = decode(input("%s [%s] " % (msg, choices))) 34 | except UnicodeDecodeError as ude: 35 | if verbose: 36 | print(ude) 37 | print("Setting answer to: UnicodeDecodeError") 38 | answer = "UnicodeDecodeError" 39 | except UnicodeEncodeError as uee: 40 | if verbose: 41 | print(uee) 42 | answer = "UnicodeEncodeError" 43 | if "no encode found" in answer: 44 | no_encode_found = True 45 | if no_encode_found: 46 | if verbose: 47 | print("Answer: %s" % answer) 48 | print("Not able to decode the input with utf-8, utf-16 nor " 49 | "utf-32, please file a bug for that.") 50 | return False 51 | answer = answer.strip().lower() 52 | if answer in {"yes", "y"} or (default and not answer): 53 | return True 54 | if answer in {"no", "n"} or (not default and not answer): 55 | return False 56 | 57 | 58 | def decode(answer: str, encoding="utf-8") -> (str, str): 59 | try: 60 | return answer.encode(encoding).decode(encoding), encoding 61 | except UnicodeDecodeError as ude: 62 | last_exception = "%s" % ude 63 | except UnicodeEncodeError as uee: 64 | last_exception = "%s" % uee 65 | if encoding != "utf-32": 66 | if encoding == "utf-8": 67 | return decode(answer, "utf-16") 68 | if encoding == "utf-16": 69 | return decode(answer, "utf-32") 70 | return "no encode found exception(%s)" % last_exception, encoding 71 | -------------------------------------------------------------------------------- /automatoes/info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2021 Flavio Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | """ 20 | The account info command. 21 | """ 22 | 23 | from . import get_version 24 | from .acme import AcmeV2 25 | from .errors import AutomatoesError 26 | import logging 27 | import os 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | 32 | def info(server, account, paths): 33 | acme_v2 = AcmeV2(server, account) 34 | print("Candango Automatoes {}. Manuale replacement." 35 | "\n\n".format(get_version())) 36 | try: 37 | print("Requesting account data...\n") 38 | response = acme_v2.get_registration() 39 | print(" Account contacts:") 40 | for contact in response['contact']: 41 | print(" {}".format(contact[7:])) 42 | print("\n Account uri is located at %s." % account.uri) 43 | print(" Account id is %s.\n" % account.uri.split("/")[-1]) 44 | if "createdAt" in response: 45 | print(" Creation: {}".format(response['createdAt'])) 46 | if "initialIp" in response: 47 | print(" Initial Ip: {}".format(response['initialIp'])) 48 | if "status" in response: 49 | print(" Status: {}".format(response['status'])) 50 | if "key" in response: 51 | print(" Key Data:") 52 | # TODO: Prepare to display ec keys 53 | print(" Type: {}".format(response['key']['kty'])) 54 | print(" Public key (part I) n: {}".format( 55 | response['key']['n'])) 56 | print(" Public key (part II) e: {}".format( 57 | response['key']['e'])) 58 | else: 59 | print(" WARNING: Server won't return your key information.") 60 | print(" Private key stored at {}".format( 61 | os.path.join(paths['current'], "account.json"))) 62 | except IOError as e: 63 | raise AutomatoesError(e) 64 | -------------------------------------------------------------------------------- /automatoes/issue.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2020 Flavio Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | The moment you've been waiting for: actually getting SSL. For free! 20 | """ 21 | 22 | from . import get_version 23 | from .acme import AcmeV2 24 | from .authorize import update_order 25 | from .crypto import ( 26 | generate_rsa_key, 27 | load_private_key, 28 | export_private_key, 29 | create_csr, 30 | load_csr, 31 | export_csr_for_acme, 32 | load_pem_certificate, 33 | export_pem_certificate, 34 | strip_certificates, 35 | ) 36 | from .errors import AutomatoesError 37 | from .model import Order 38 | 39 | import binascii 40 | from cartola import fs, sysexits 41 | from cryptography.hazmat.primitives.hashes import SHA256 42 | import hashlib 43 | import logging 44 | import os 45 | import sys 46 | 47 | logger = logging.getLogger(__name__) 48 | 49 | EXPIRATION_FORMAT = "%Y-%m-%d" 50 | 51 | 52 | def issue(server, paths, account, domains, key_size, key_file=None, 53 | csr_file=None, output_path=None, output_filename=None, must_staple=False, verbose=False): 54 | print("Candango Automatoes {}. Manuale replacement.\n\n".format( 55 | get_version())) 56 | 57 | current_path = paths['current'] 58 | orders_path = paths['orders'] 59 | domains_hash = hashlib.sha256( 60 | "_".join(domains).encode('ascii')).hexdigest() 61 | order_path = os.path.join(orders_path, domains_hash) 62 | order_file = os.path.join(order_path, "order.json".format(domains_hash)) 63 | 64 | if not os.path.exists(orders_path): 65 | print(" ERROR: Orders path not found. Please run before: manuale " 66 | "authorize {}".format(" ".join(domains))) 67 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 68 | else: 69 | if verbose: 70 | print("Orders path found at {}.".format(orders_path)) 71 | 72 | if verbose: 73 | print("Searching order file {}.".format(order_file)) 74 | 75 | if not os.path.exists(order_path): 76 | print(" ERROR: Order file not found. Please run before: manuale " 77 | "authorize {}".format(" ".join(domains))) 78 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 79 | else: 80 | if verbose: 81 | print("Current order {} path found at orders path.\n".format( 82 | domains_hash)) 83 | 84 | acme = AcmeV2(server, account) 85 | order = Order.deserialize(fs.read(order_file)) 86 | if order.contents['status'] == "pending": 87 | if verbose: 88 | print("Querying ACME server for current status.") 89 | server_order = acme.query_order(order) 90 | order.contents = server_order.contents 91 | update_order(order, order_file) 92 | if order.contents['status'] in ["pending", "invalid"]: 93 | print(" ERROR: Order not ready or invalid. Please re-run: manuale" 94 | " authorize {}.".format(" ".join(domains))) 95 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 96 | elif order.contents['status'] == "invalid": 97 | print(" ERROR: Invalid order. Please re-run: manuale authorize " 98 | "{}.".format(" ".join(domains))) 99 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 100 | 101 | if not output_path or output_path == '.': 102 | output_path = os.getcwd() 103 | 104 | # Load key if given 105 | if key_file: 106 | try: 107 | with open(key_file, 'rb') as f: 108 | certificate_key = load_private_key(f.read()) 109 | order.key = export_private_key(certificate_key).decode('ascii') 110 | update_order(order, order_file) 111 | except (ValueError, AttributeError, TypeError, IOError) as e: 112 | print("ERROR: Couldn't read certificate key.") 113 | raise AutomatoesError(e) 114 | else: 115 | certificate_key = None 116 | 117 | # Load CSR or generate 118 | if csr_file: 119 | try: 120 | with open(csr_file, 'rb') as f: 121 | csr = export_csr_for_acme(load_csr(f.read())) 122 | except (ValueError, AttributeError, TypeError, IOError) as e: 123 | print("ERROR: Couldn't read CSR.") 124 | raise AutomatoesError(e) 125 | else: 126 | # Generate key 127 | if not key_file: 128 | if order.key is None: 129 | print("Generating a {} bit RSA key. This might take a " 130 | "second.".format(key_size)) 131 | certificate_key = generate_rsa_key(key_size) 132 | print(" Key generated.") 133 | order.key = export_private_key(certificate_key).decode('ascii') 134 | update_order(order, order_file) 135 | print(" Order updated with generated key.") 136 | else: 137 | print("Previous RSA key found in the order. Loading the key.") 138 | certificate_key = load_private_key(order.key.encode('ascii')) 139 | 140 | csr = create_csr(certificate_key, domains, must_staple=must_staple) 141 | 142 | try: 143 | logger.info("Requesting certificate issuance...") 144 | if order.contents['status'] == "ready": 145 | final_order = acme.finalize_order(order, csr) 146 | order.contents = final_order 147 | update_order(order, order_file) 148 | if final_order['status'] in ["processing", "valid"]: 149 | if verbose: 150 | print(" Order {} finalized. Certificate is being " 151 | "issued.".format(domains_hash)) 152 | else: 153 | print(" ERROR: Order not ready or invalid. Please re-run: " 154 | "manuale authorize {}.".format(" ".join(domains))) 155 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 156 | elif order.contents['status'] in ["valid", "processing"]: 157 | print(" Order {} is already processing or valid. Downloading " 158 | "certificate.".format(domains_hash)) 159 | else: 160 | print(" ERROR: Order not ready or invalid. Please re-run: manuale " 161 | "authorize {}.".format(" ".join(domains))) 162 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 163 | 164 | if order.certificate_uri is None: 165 | if verbose: 166 | print(" Checking order {} status.".format(domains_hash)) 167 | fulfillment = acme.await_for_order_fulfillment(order) 168 | if fulfillment['status'] == "valid": 169 | order.contents = fulfillment 170 | update_order(order, order_file) 171 | else: 172 | print(" ERROR: Order not ready or invalid. Please re-run: " 173 | "manuale authorize {}.".format(" ".join(domains))) 174 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 175 | else: 176 | print(" We already know the certificate uri for order {}. " 177 | "Downloading certificate.".format(domains_hash)) 178 | 179 | result = acme.download_order_certificate(order) 180 | 181 | logger.info(" Certificate downloaded.") 182 | except IOError as e: 183 | print("Connection or service request failed. Aborting.") 184 | raise AutomatoesError(e) 185 | 186 | try: 187 | certificates = strip_certificates(result.content) 188 | certificate = load_pem_certificate(certificates[0]) 189 | 190 | # Print some neat info 191 | print(" Expires: {}".format(certificate.not_valid_after.strftime( 192 | EXPIRATION_FORMAT))) 193 | print(" SHA256: {}".format(binascii.hexlify( 194 | certificate.fingerprint(SHA256())).decode('ascii'))) 195 | 196 | # Write the key, certificate and full chain 197 | os.makedirs(output_path, exist_ok=True) 198 | cert_name = output_filename if output_filename else domains[0] 199 | cert_path = os.path.join(output_path, cert_name + '.crt') 200 | chain_path = os.path.join(output_path, cert_name + '.chain.crt') 201 | intermediate_path = os.path.join(output_path, 202 | cert_name + '.intermediate.crt') 203 | key_path = os.path.join(output_path, cert_name + '.pem') 204 | 205 | if order.key is not None: 206 | with open(key_path, 'wb') as f: 207 | os.chmod(key_path, 0o600) 208 | f.write(order.key.encode('ascii')) 209 | print("\n Wrote key to {}".format(f.name)) 210 | 211 | with open(cert_path, 'wb') as f: 212 | f.write(export_pem_certificate(certificate)) 213 | print(" Wrote certificate to {}".format(f.name)) 214 | 215 | with open(chain_path, 'wb') as f: 216 | f.write(export_pem_certificate(certificate)) 217 | if len(certificates) > 1: 218 | f.write(export_pem_certificate(load_pem_certificate( 219 | certificates[1]))) 220 | print(" Wrote certificate with intermediate to {}".format(f.name)) 221 | 222 | if len(certificates) > 1: 223 | with open(intermediate_path, 'wb') as f: 224 | f.write(export_pem_certificate(load_pem_certificate( 225 | certificates[1]))) 226 | print(" Wrote intermediate certificate to {}".format(f.name)) 227 | except IOError as e: 228 | print(" ERROR: Failed to write certificate or key. Going to print " 229 | "them for you instead.") 230 | if order.key is not None: 231 | for line in order.key.split('\n'): 232 | print("ERROR: {}".format(line)) 233 | for line in export_pem_certificate( 234 | certificate).decode('ascii').split('\n'): 235 | print("ERROR: {}".format(line)) 236 | raise AutomatoesError(e) 237 | -------------------------------------------------------------------------------- /automatoes/messages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2019-2022 Flávio Gonçalves Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | from . import get_version 19 | 20 | # Text 21 | DESCRIPTION = """ 22 | Candango Automatoes {}. Manuale replacement. 23 | 24 | Interact with ACME certification authorities such as Let's Encrypt. 25 | 26 | No idea what you're doing? Register an account, authorize your domains and 27 | issue a certificate or two. Call a command with -h for more instructions. 28 | """.format(get_version()) 29 | 30 | DESCRIPTION_REGISTER = """ 31 | Create a new account key and register on the server. The resulting --account 32 | is saved in the specified file, and required for most other operations. 33 | 34 | You only have to do this once. Keep the account file safe and secure: it 35 | contains your private key, and you need it to get certificates! 36 | """ 37 | 38 | DESCRIPTION_AUTHORIZE = """ 39 | Authorizes a domain or multiple domains for your account through DNS or HTTP 40 | verification. You will need to set up DNS records or HTTP files as prompted. 41 | 42 | After authorizing a domain, you can issue certificates for it. Authorizations 43 | can last for a long time, so you might not need to do this every time you want 44 | a new certificate. This depends on the server being used. You should see an 45 | expiration date for the authorization after completion. 46 | 47 | If a domain is already authorized, the authorization's expiration date will be 48 | printed. 49 | """ 50 | 51 | DESCRIPTION_ISSUE = """ 52 | Issues a certificate for one or more domains. Hopefully needless to say, you 53 | must have valid authorizations for the domains you specify first. 54 | 55 | This will generate a new RSA key and CSR for you. But if you want, you can 56 | bring your own with the --key-file and --csr-file attributes. You can also set 57 | a custom --key-size. (Don't try something stupid like 512, the server won't 58 | accept it. I tried.) 59 | 60 | The resulting key and certificate are written into domain.pem and domain.crt. 61 | A chained certificate with the intermediate included is also written to 62 | domain.chain.crt. You can change the --output directory to something else from 63 | the working directory as well. 64 | 65 | (If you're passing your own CSR, the given domains can be whatever you want.) 66 | 67 | Note that unlike many other certification authorities, ACME does not add a 68 | non-www or www alias to certificates. If you want this to happen, add it 69 | yourself. You need to authorize both as well. 70 | 71 | Certificate issuance has a server-side rate limit. Don't overdo it. 72 | """ 73 | 74 | DESCRIPTION_MIGRATE = """ 75 | Migrate an account created by certbot to the automatoes format. 76 | """ 77 | 78 | DESCRIPTION_REVOKE = """ 79 | Revokes a certificate. The certificate must have been issued using the 80 | current account. 81 | """ 82 | 83 | DESCRIPTION_INFO = """ 84 | Display registration info for the current account. 85 | """ 86 | 87 | DESCRIPTION_UPGRADE = """ 88 | Upgrade current account's uri from Let's Encrypt ACME V1 to ACME V2. 89 | """ 90 | 91 | OPTION_ACCOUNT_HELP = "The account file to use or create" 92 | OPTION_SERVER_HELP = "The ACME server to use" 93 | -------------------------------------------------------------------------------- /automatoes/migrate.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2023 Flávio Gonçalves Garcia 2 | # Copyright 2016-2017 Veeti Paananen under MIT License 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from .crypto import certbot_key_data_to_int, generate_rsa_key_from_parameters 17 | from .errors import AutomatoesError 18 | from .helpers import confirm 19 | from .model import Account 20 | from cartola import fs, sysexits 21 | import json 22 | import os 23 | import sys 24 | 25 | 26 | def migrate(account_path, certbot_path=None): 27 | if not certbot_path: 28 | certbot_path = input("Inform where is located the certbot account " 29 | "path:") 30 | if not os.path.exists(certbot_path): 31 | print("ERROR: The informed path \"{}\" does not exist".format( 32 | certbot_path)) 33 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 34 | 35 | key_path = os.path.join(certbot_path, "private_key.json") 36 | regr_path = os.path.join(certbot_path, "regr.json") 37 | 38 | if not os.path.isfile(key_path): 39 | print("ERROR: The file private_key.json is not present at " 40 | "\"{}\"".format(certbot_path)) 41 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 42 | 43 | if not os.path.isfile(regr_path): 44 | print("ERROR: The file regr.json is not present at " 45 | "\"{}\"".format(certbot_path)) 46 | sys.exit(sysexits.EX_CANNOT_EXECUTE) 47 | 48 | key_data = json.loads(fs.read(key_path)) 49 | regr_data = json.loads(fs.read(regr_path)) 50 | 51 | print("Migrating...") 52 | 53 | key_data_int = certbot_key_data_to_int(key_data) 54 | private_key = generate_rsa_key_from_parameters( 55 | key_data_int['p'], key_data_int['q'], key_data_int['d'], 56 | key_data_int['dp'], key_data_int['dq'], key_data_int['qi'], 57 | key_data_int['e'], key_data_int['n'] 58 | ) 59 | 60 | account = Account(key=private_key, uri=regr_data['uri']) 61 | 62 | if os.path.isfile(account_path): 63 | if not confirm("The account file account.json already exists." 64 | "Continuing will overwrite it with the new migrated " 65 | "key. Continue?", False): 66 | print("Aborting.") 67 | raise AutomatoesError("Aborting.") 68 | 69 | fs.write(account_path, account.serialize(), True) 70 | print("Wrote migrated account to {}.\n".format(account_path)) 71 | print("What's next? Verify your domains with 'authorize' and use 'issue' " 72 | "to get new certificates.") 73 | -------------------------------------------------------------------------------- /automatoes/model.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2025 Flavio Garcia 2 | # Copyright 2016-2017 Veeti Paananen under MIT License 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | import json 18 | 19 | from .crypto import (generate_jwk_thumbprint, load_private_key, 20 | export_private_key) 21 | from collections import namedtuple 22 | from datetime import datetime 23 | 24 | 25 | class Account: 26 | 27 | def __init__(self, key, uri=None): 28 | self.key = key 29 | self.uri = uri 30 | 31 | def serialize(self): 32 | return json.dumps({ 33 | 'key': export_private_key(self.key).decode("utf-8"), 34 | 'uri': self.uri, 35 | }).encode("utf-8") 36 | 37 | @property 38 | def thumbprint(self): 39 | return generate_jwk_thumbprint(self.key) 40 | 41 | @staticmethod 42 | def deserialize(data): 43 | try: 44 | if not isinstance(data, str): 45 | data = data.decode('utf-8') 46 | data = json.loads(data) 47 | if "key" not in data or "uri" not in data: 48 | raise ValueError("Missing 'key' or 'uri' fields.") 49 | return Account(key=load_private_key(data['key'].encode("utf8")), 50 | uri=data['uri']) 51 | except (TypeError, ValueError, AttributeError) as e: 52 | raise IOError("Invalid account structure: {}".format(e)) 53 | 54 | 55 | class Authorization: 56 | 57 | def __init__(self, contents, uri, ty_pe): 58 | self.contents = contents 59 | self.uri = uri 60 | self.type = ty_pe 61 | self.certificate_uri = None 62 | self.certificate = {} 63 | 64 | 65 | class Challenge: 66 | 67 | def __init__(self, contents, domain, expires, status, ty_pe, key): 68 | self.contents = contents 69 | self.domain = domain 70 | self.expires = expires 71 | self.status = status 72 | self.type = ty_pe 73 | self.key = key 74 | 75 | def serialize(self): 76 | return json.dumps({ 77 | 'contents': self.contents, 78 | 'domain': self.domain, 79 | 'expires': self.expires, 80 | 'status': self.status, 81 | 'type': self.type, 82 | 'key': self.key 83 | }).encode('utf-8') 84 | 85 | @property 86 | def file_name(self): 87 | return "{}_challenge.json".format(self.domain) 88 | 89 | 90 | class Order: 91 | 92 | def __init__(self, contents, uri, ty_pe): 93 | self.contents = contents 94 | self.uri = uri 95 | self.type = ty_pe 96 | self.certificate_uri = None 97 | self.certificate = {} 98 | self.key = None 99 | 100 | @property 101 | def expired(self): 102 | if self.contents['status'] == "expired": 103 | return True 104 | order_timestamp = datetime.strptime(self.contents['expires'][0:19], 105 | "%Y-%m-%dT%H:%M:%S") 106 | return order_timestamp < datetime.now() 107 | 108 | @property 109 | def invalid(self): 110 | return self.contents['status'] == "invalid" 111 | 112 | def serialize(self): 113 | return json.dumps({ 114 | 'contents': self.contents, 115 | 'uri': self.uri, 116 | 'type': self.type, 117 | 'certificate_uri': self.certificate_uri, 118 | 'key': self.key 119 | }).encode("utf-8") 120 | 121 | @staticmethod 122 | def deserialize(data): 123 | try: 124 | if not isinstance(data, str): 125 | data = data.decode("utf-8") 126 | data = json.loads(data) 127 | if 'contents' not in data: 128 | raise ValueError("Missing 'contents' field.") 129 | if 'uri' not in data: 130 | raise ValueError("Missing 'uri' field.") 131 | if 'type' not in data: 132 | raise ValueError("Missing 'type' field.") 133 | order = Order(contents=data['contents'], 134 | uri=data['uri'], 135 | ty_pe=data['type']) 136 | if data['certificate_uri']: 137 | order.certificate_uri = data['certificate_uri'] 138 | if data['key']: 139 | order.key = data['key'] 140 | return order 141 | except (TypeError, ValueError, AttributeError) as e: 142 | raise IOError("Invalid account structure: {}".format(e)) 143 | 144 | 145 | RegistrationResult = namedtuple("RegistrationResult", "contents uri terms") 146 | NewAuthorizationResult = namedtuple("NewAuthorizationResult", "contents uri") 147 | IssuanceResult = namedtuple("IssuanceResult", 148 | "certificate location intermediate") 149 | -------------------------------------------------------------------------------- /automatoes/protocol.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2024 Flavio Garcia 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from . import get_version 16 | from peasant.client.protocol import Peasant 17 | from peasant.client.transport_requests import RequestsTransport 18 | 19 | 20 | class AcmeV2Pesant(Peasant): 21 | 22 | def __init__(self, transport, **kwargs): 23 | """ 24 | """ 25 | super().__init__(transport) 26 | self._url = kwargs.get("url") 27 | self._account = kwargs.get("account") 28 | self._directory_path = kwargs.get("directory", "directory") 29 | self._verify = kwargs.get("verify") 30 | 31 | @property 32 | def url(self): 33 | return self._url 34 | 35 | @property 36 | def account(self): 37 | return self.account 38 | 39 | @account.setter 40 | def account(self, account): 41 | # TODO: Throw an error right here if account is None 42 | self._account = account 43 | 44 | @property 45 | def directory_path(self): 46 | return self._directory_path 47 | 48 | @directory_path.setter 49 | def directory_path(self, path): 50 | self._directory_path = path 51 | 52 | @property 53 | def verify(self): 54 | return self._verify 55 | 56 | 57 | class AcmeRequestsTransport(RequestsTransport): 58 | 59 | peasant: AcmeV2Pesant 60 | 61 | def __init__(self, bastion_address): 62 | super().__init__(bastion_address) 63 | self._directory = None 64 | self.user_agent = (f"Automatoes/{get_version()} {self.user_agent}") 65 | self.basic_headers = { 66 | 'User-Agent': self.user_agent 67 | } 68 | self.kwargs_updater = self.update_kwargs 69 | 70 | def update_kwargs(self, method, **kwargs): 71 | if self.peasant.verify: 72 | kwargs['verify'] = self.peasant.verify 73 | return kwargs 74 | 75 | def set_directory(self): 76 | response = self.get("/%s" % self.peasant.directory_path) 77 | if response.status_code == 200: 78 | self.peasant.directory_cache = response.json() 79 | else: 80 | raise Exception 81 | 82 | def new_nonce(self): 83 | """ Returns a new nonce """ 84 | return self.head(self.peasant.directory()['newNonce'], headers={ 85 | 'resource': "new-reg", 86 | 'payload': None, 87 | }).headers.get('Replay-Nonce') 88 | -------------------------------------------------------------------------------- /automatoes/register.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2022 Flávio Gonçálves Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | Account registration. 20 | """ 21 | 22 | from . import get_version 23 | from .acme import AcmeV2 24 | from .errors import AutomatoesError, AccountAlreadyExistsError 25 | from .crypto import ( 26 | generate_rsa_key, 27 | load_private_key, 28 | ) 29 | from .helpers import confirm 30 | from .model import Account 31 | 32 | import os 33 | 34 | 35 | def register(server, account_path, email, key_file, verbose=False): 36 | print("Candango Automatoes {}. Manuale replacement.\n\n".format( 37 | get_version())) 38 | # Don't overwrite silently 39 | if os.path.exists(account_path): 40 | if not confirm("The account file {} already exists. Continuing will" 41 | " overwrite it with the new key." 42 | " Continue?".format(account_path), default=False, 43 | verbose=verbose): 44 | raise AutomatoesError("Aborting.") 45 | 46 | # Confirm e-mail 47 | if not confirm("You're about to register a new account with e-mail " 48 | "{} as contact. Continue?".format(email), verbose=verbose): 49 | raise AutomatoesError("Aborting.") 50 | 51 | # Load key or generate 52 | if key_file: 53 | try: 54 | with open(key_file, 'rb') as f: 55 | account = Account(key=load_private_key(f.read())) 56 | except (ValueError, AttributeError, TypeError, IOError) as e: 57 | print("ERROR: Couldn't read key.") 58 | raise AutomatoesError(e) 59 | else: 60 | print("Generating a new account key. This might take a second.") 61 | account = Account(key=generate_rsa_key(4096)) 62 | print(" Key generated.") 63 | 64 | # Register 65 | acmev2 = AcmeV2(server, account) 66 | print("Registering...") 67 | try: 68 | terms_agreed = False 69 | print(" Retrieving terms of agreement ...") 70 | terms = acmev2.terms_from_directory() 71 | if terms is None: 72 | print(" There is no terms being enforced in this server. " 73 | "Resuming execution...") 74 | terms_agreed = True 75 | else: 76 | print(" This server requires you to agree to these terms:") 77 | print(" {}".format(terms)) 78 | if confirm("Agreed?", verbose=verbose): 79 | terms_agreed = True 80 | else: 81 | print("Your account will still be created, but it won't be " 82 | "usable before agreeing to terms.") 83 | acmev2.register(email, terms_agreed) 84 | print(" Account {} created.".format(account.uri)) 85 | except IOError as e: 86 | print("ERROR: Registration failed due to a connection or request " 87 | "error.") 88 | raise AutomatoesError(e) 89 | 90 | # Write account 91 | directory = os.path.dirname(os.path.abspath(account_path)) 92 | os.makedirs(directory, exist_ok=True) 93 | with open(account_path, 'wb') as f: 94 | os.chmod(account_path, 0o600) 95 | f.write(account.serialize()) 96 | 97 | print(" Wrote account to {}.\n".format(account_path)) 98 | print("What's next? Verify your domains with 'authorize' and use 'issue' " 99 | "to get new certificates.") 100 | -------------------------------------------------------------------------------- /automatoes/revoke.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2020 Flavio Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | The certificate revocation command. 20 | """ 21 | 22 | import logging 23 | 24 | from . import get_version 25 | from .acme import AcmeV2 26 | from .errors import AutomatoesError 27 | from .crypto import ( 28 | load_pem_certificate, 29 | get_certificate_domains 30 | ) 31 | from .helpers import confirm 32 | 33 | logger = logging.getLogger(__name__) 34 | 35 | 36 | def revoke(server, account, certificate): 37 | print("Candango Automatoes {}. Manuale replacement.\n\n".format( 38 | get_version())) 39 | 40 | # Load the certificate 41 | try: 42 | with open(certificate, 'rb') as f: 43 | certificate = load_pem_certificate(f.read()) 44 | except IOError as e: 45 | print("ERROR: Couldn't read the certificate.") 46 | raise AutomatoesError(e) 47 | 48 | # Confirm 49 | print("Are you sure you want to revoke this certificate? It includes the " 50 | "following domains:") 51 | for domain in get_certificate_domains(certificate): 52 | print(" {}".format(domain)) 53 | if not confirm("This can't be undone. Confirm?", default=False): 54 | raise AutomatoesError("Aborting.") 55 | 56 | # Revoke. 57 | acme = AcmeV2(server, account) 58 | try: 59 | acme.revoke_certificate(certificate) 60 | except IOError as e: 61 | raise AutomatoesError(e) 62 | 63 | print("Certificate revoked.") 64 | -------------------------------------------------------------------------------- /automatoes/upgrade.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2020 Flavio Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | The account upgrade command. 20 | """ 21 | 22 | from . import get_version 23 | from .acme import AcmeV2 24 | from .errors import AutomatoesError 25 | from .helpers import confirm 26 | from cartola import fs 27 | import logging 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | 32 | def upgrade(server, account, account_path): 33 | acme_v2 = AcmeV2(server, account, upgrade=True) 34 | print("Candango Automatoes {}. Manuale replacement." 35 | "\n\n".format(get_version())) 36 | if acme_v2.is_uri_letsencrypt_acme_v1(): 37 | print("Account's uri format is Let's Encrypt ACME V1.") 38 | print("Current uri: %s" % acme_v2.account.uri) 39 | if not confirm("Let's Encrypt ACME V2: %s.\nDo you want to " 40 | "upgrade?" % acme_v2.letsencrypt_acme_uri_v1_to_v2()): 41 | raise AutomatoesError("Aborting.") 42 | account.uri = acme_v2.letsencrypt_acme_uri_v1_to_v2() 43 | fs.write(account_path, account.serialize(), binary=True) 44 | print("Account's uri upgraded to Let's Encrypt ACME V2.\n") 45 | else: 46 | print("Account's uri format isn't Let's Encrypt ACME V1.") 47 | print("Skipping upgrade action.") 48 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/automatoes.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/automatoes.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/automatoes" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/automatoes" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2019-2021 Flávio Gonçalves Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import sys 19 | import os 20 | import shlex 21 | 22 | # If extensions (or modules to document with autodoc) are in another directory, 23 | # add these directories to sys.path here. If the directory is relative to the 24 | # documentation root, use os.path.abspath to make it absolute, like shown here. 25 | #sys.path.insert(0, os.path.abspath('.')) 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | #needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ["_templates"] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = ".rst" 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = "index" 50 | 51 | # General information about the project. 52 | project = u"Candango Automat-o-es" 53 | copyright = u"2019-2022, Flávio Gonçalves Garcia" 54 | author = u"Flávio Gonçalves Garcia" 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = "0.9" 62 | # The full version, including alpha/beta/rc tags. 63 | release = "0.9.7" 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # 68 | # This is also used if you do content translation via gettext catalogs. 69 | # Usually you set "language" from the command line for these cases. 70 | language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | #today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | #today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | exclude_patterns = ["_build"] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all 83 | # documents. 84 | #default_role = None 85 | 86 | # If true, '()' will be appended to :func: etc. cross-reference text. 87 | #add_function_parentheses = True 88 | 89 | # If true, the current module name will be prepended to all description 90 | # unit titles (such as .. function::). 91 | #add_module_names = True 92 | 93 | # If true, sectionauthor and moduleauthor directives will be shown in the 94 | # output. They are ignored by default. 95 | #show_authors = False 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = "sphinx" 99 | 100 | # A list of ignored prefixes for module index sorting. 101 | #modindex_common_prefix = [] 102 | 103 | # If true, keep warnings as "system message" paragraphs in the built documents. 104 | #keep_warnings = False 105 | 106 | # If true, `todo` and `todoList` produce output, else they produce nothing. 107 | todo_include_todos = False 108 | 109 | 110 | # -- Options for HTML output ---------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | #html_theme = 'alabaster' 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | #html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | #html_theme_path = [] 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | #html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | #html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | #html_logo = None 134 | 135 | # The name of an image file (within the static path) to use as favicon of the 136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | #html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | #html_static_path = ['_static'] 144 | 145 | # Add any extra paths that contain custom files (such as robots.txt or 146 | # .htaccess) here, relative to this directory. These files are copied 147 | # directly to the root of the documentation. 148 | #html_extra_path = [] 149 | 150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 151 | # using the given strftime format. 152 | #html_last_updated_fmt = '%b %d, %Y' 153 | 154 | # If true, SmartyPants will be used to convert quotes and dashes to 155 | # typographically correct entities. 156 | #html_use_smartypants = True 157 | 158 | # Custom sidebar templates, maps document names to template names. 159 | #html_sidebars = {} 160 | 161 | # Additional templates that should be rendered to pages, maps page names to 162 | # template names. 163 | #html_additional_pages = {} 164 | 165 | # If false, no module index is generated. 166 | #html_domain_indices = True 167 | 168 | # If false, no index is generated. 169 | #html_use_index = True 170 | 171 | # If true, the index is split into individual pages for each letter. 172 | #html_split_index = False 173 | 174 | # If true, links to the reST sources are added to the pages. 175 | #html_show_sourcelink = True 176 | 177 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 178 | #html_show_sphinx = True 179 | 180 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 181 | #html_show_copyright = True 182 | 183 | # If true, an OpenSearch description file will be output, and all pages will 184 | # contain a tag referring to it. The value of this option must be the 185 | # base URL from which the finished HTML is served. 186 | #html_use_opensearch = '' 187 | 188 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 189 | #html_file_suffix = None 190 | 191 | # Language to be used for generating the HTML full-text search index. 192 | # Sphinx supports the following languages: 193 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 194 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 195 | #html_search_language = 'en' 196 | 197 | # A dictionary with options for the search language support, empty by default. 198 | # Now only 'ja' uses this config value 199 | #html_search_options = {'type': 'default'} 200 | 201 | # The name of a javascript file (relative to the configuration directory) that 202 | # implements a search results scorer. If empty, the default will be used. 203 | #html_search_scorer = 'scorer.js' 204 | 205 | # Output file base name for HTML help builder. 206 | htmlhelp_basename = 'automatoes_doc' 207 | 208 | # -- Options for LaTeX output --------------------------------------------- 209 | 210 | latex_elements = { 211 | # The paper size ('letterpaper' or 'a4paper'). 212 | #'papersize': 'letterpaper', 213 | 214 | # The font size ('10pt', '11pt' or '12pt'). 215 | #'pointsize': '10pt', 216 | 217 | # Additional stuff for the LaTeX preamble. 218 | #'preamble': '', 219 | 220 | # Latex figure (float) alignment 221 | #'figure_align': 'htbp', 222 | } 223 | 224 | # Grouping the document tree into LaTeX files. List of tuples 225 | # (source start file, target name, title, 226 | # author, documentclass [howto, manual, or own class]). 227 | latex_documents = [ 228 | (master_doc, "automatoes.tex", u"Automatoes Documentation", 229 | author, "manual"), 230 | ] 231 | 232 | # The name of an image file (relative to this directory) to place at the top of 233 | # the title page. 234 | #latex_logo = None 235 | 236 | # For "manual" documents, if this is true, then toplevel headings are parts, 237 | # not chapters. 238 | #latex_use_parts = False 239 | 240 | # If true, show page references after internal links. 241 | #latex_show_pagerefs = False 242 | 243 | # If true, show URL addresses after external links. 244 | #latex_show_urls = False 245 | 246 | # Documents to append as an appendix to all manuals. 247 | #latex_appendices = [] 248 | 249 | # If false, no module index is generated. 250 | #latex_domain_indices = True 251 | 252 | 253 | # -- Options for manual page output --------------------------------------- 254 | 255 | # One entry per manual page. List of tuples 256 | # (source start file, name, description, authors, manual section). 257 | man_pages = [ 258 | (master_doc, 'automatoes', u'Automatoes Documentation', 259 | [author], 1) 260 | ] 261 | 262 | # If true, show URL addresses after external links. 263 | #man_show_urls = False 264 | 265 | 266 | # -- Options for Texinfo output ------------------------------------------- 267 | 268 | # Grouping the document tree into Texinfo files. List of tuples 269 | # (source start file, target name, title, author, 270 | # dir menu entry, description, category) 271 | texinfo_documents = [ 272 | (master_doc, "Automatoes", u"Automatoes Documentation", 273 | author, "automatoes", "Automatoes is a Let's Encrypt/ACME client for " 274 | "advanced users and developers.", 275 | "Miscellaneous"), 276 | ] 277 | 278 | # Documents to append as an appendix to all manuals. 279 | #texinfo_appendices = [] 280 | 281 | # If false, no module index is generated. 282 | #texinfo_domain_indices = True 283 | 284 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 285 | #texinfo_show_urls = 'footnote' 286 | 287 | # If true, do not generate a @detailmenu in the "Top" node's menu. 288 | #texinfo_no_detailmenu = False 289 | 290 | 291 | # -- Options for Epub output ---------------------------------------------- 292 | 293 | # Bibliographic Dublin Core info. 294 | epub_title = project 295 | epub_author = author 296 | epub_publisher = author 297 | epub_copyright = copyright 298 | 299 | # The basename for the epub file. It defaults to the project name. 300 | #epub_basename = project 301 | 302 | # The HTML theme for the epub output. Since the default themes are not optimized 303 | # for small screen space, using the same theme for HTML and epub output is 304 | # usually not wise. This defaults to 'epub', a theme designed to save visual 305 | # space. 306 | #epub_theme = 'epub' 307 | 308 | # The language of the text. It defaults to the language option 309 | # or 'en' if the language is not set. 310 | #epub_language = '' 311 | 312 | # The scheme of the identifier. Typical schemes are ISBN or URL. 313 | #epub_scheme = '' 314 | 315 | # The unique identifier of the text. This can be a ISBN number 316 | # or the project homepage. 317 | #epub_identifier = '' 318 | 319 | # A unique identification for the text. 320 | #epub_uid = '' 321 | 322 | # A tuple containing the cover image and cover page html template filenames. 323 | #epub_cover = () 324 | 325 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 326 | #epub_guide = () 327 | 328 | # HTML files that should be inserted before the pages created by sphinx. 329 | # The format is a list of tuples containing the path and title. 330 | #epub_pre_files = [] 331 | 332 | # HTML files shat should be inserted after the pages created by sphinx. 333 | # The format is a list of tuples containing the path and title. 334 | #epub_post_files = [] 335 | 336 | # A list of files that should not be packed into the epub file. 337 | epub_exclude_files = ["search.html"] 338 | 339 | # The depth of the table of contents in toc.ncx. 340 | #epub_tocdepth = 3 341 | 342 | # Allow duplicate toc entries. 343 | #epub_tocdup = True 344 | 345 | # Choose between 'default' and 'includehidden'. 346 | #epub_tocscope = 'default' 347 | 348 | # Fix unsupported image types using the Pillow. 349 | #epub_fix_images = False 350 | 351 | # Scale large images. 352 | #epub_max_image_width = 0 353 | 354 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 355 | #epub_show_urls = 'inline' 356 | 357 | # If false, no index is generated. 358 | #epub_use_index = True 359 | -------------------------------------------------------------------------------- /docs/features.rst: -------------------------------------------------------------------------------- 1 | Features 2 | ======== 3 | 4 | * Simple interface with no hoops to jump through. Keys and certificate signing 5 | requests are automatically generated: no more cryptic OpenSSL one-liners. 6 | (However, you do need to know what to do with generated certificates and keys 7 | yourself!) 8 | 9 | * Support for DNS & HTTP validation. No need to figure out how to serve 10 | challenge files from a live domain. 11 | 12 | * Obviously, runs without root access. Use it from any machine you want, it 13 | doesn't care. Internet connection recommended. 14 | 15 | * Awful, undiscoverable name. 16 | 17 | * And finally, if the `openssl` binary is your spirit animal after all, you can 18 | still bring your own keys and/or CSR's. Everybody wins. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Candango Automatoes documentation master file, created by 2 | sphinx-quickstart on Mon Jan 31 20:37:23 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | Candango Automat-o-es 8 | ===================== 9 | 10 | 11 | Automatoes is a `Let's Encrypt `_/ 12 | `ACME `_ 13 | client for advanced users and developers. It is intended to be used by anyone 14 | because we don't care if you're a robot, a processes or a person. 15 | 16 | We will keep the `manuale` command to provide manual workflow designed by the 17 | original project and to be a direct replacement from 18 | `ManuaLE `_. 19 | 20 | Why? 21 | ==== 22 | 23 | Because Let's Encrypt's point is to be automatic and seamless and ManuaLE was 24 | designed to be manual. 25 | 26 | Automatoes will add automatic workflows and new features to evolve ManuaLe's 27 | legacy. The project also will keep performing maintenance tasks as bug fixes 28 | and refactory. 29 | 30 | Automatoes is an ACME V2 replacement to ManuaLE. 31 | 32 | 33 | Contents: 34 | 35 | .. toctree:: 36 | 37 | guide 38 | 39 | Indices and tables 40 | ================== 41 | 42 | * :ref:`genindex` 43 | * :ref:`modindex` 44 | * :ref:`search` 45 | 46 | Discussion and support 47 | ---------------------- 48 | 49 | Report bugs on the `GitHub issue tracker 50 | `_. 51 | 52 | Create a new discussion at the `GitHub discussions 53 | `_. 54 | 55 | Automatoes is one of `Candango Open Source Group initiatives 56 | `_. It is available under the 57 | `Apache License, Version 2.0 58 | `_. 59 | 60 | This website and all documentation are licensed under `Creative 61 | Commons 3.0 `_. 62 | 63 | Copyright © 2019-2022 Flávio Gonçalves Garcia 64 | 65 | Copyright © 2016-2017 Veeti Paananen under MIT License 66 | -------------------------------------------------------------------------------- /requirements/basic.txt: -------------------------------------------------------------------------------- 1 | peasant[requests]>=0.7.5 2 | taskio>=0.0.7 3 | -------------------------------------------------------------------------------- /requirements/development.txt: -------------------------------------------------------------------------------- 1 | -r basic.txt 2 | -r tests.txt 3 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | sphinx==5.3.0 2 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | behave==1.2.6 2 | tornado==6.4.2 3 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## 3 | ## Copyright 2019-2025 Flavio Garcia 4 | ## 5 | ## Licensed under the Apache License, Version 2.0 (the "License"); 6 | ## you may not use this file except in compliance with the License. 7 | ## You may obtain a copy of the License at 8 | ## 9 | ## http://www.apache.org/licenses/LICENSE-2.0 10 | ## 11 | ## Unless required by applicable law or agreed to in writing, software 12 | ## distributed under the License is distributed on an "AS IS" BASIS, 13 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ## See the License for the specific language governing permissions and 15 | ## limitations under the License. 16 | ## 17 | ## build.sh Build packages to be uploaded to pypi. 18 | ## 19 | ## Author: Flavio Garcia 20 | 21 | python -m build 22 | rm -rf build 23 | rm -rf automatoes.egg-info 24 | -------------------------------------------------------------------------------- /scripts/install_cryptography.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PYTHONPATH="${PYTHONPATH}:." 4 | 5 | PYTHON_MINOR_VERSION="$(python -c 'import sys; print(sys.version_info.minor)')" 6 | 7 | if [ "${PYTHON_MINOR_VERSION}" -eq 5 ]; then 8 | pip install -r requirements/cryptography_legacy.txt 9 | exit 0 10 | fi 11 | 12 | pip install -r requirements/cryptography.txt 13 | exit 0 14 | -------------------------------------------------------------------------------- /scripts/install_pebble.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## 3 | ## Copyright 2019-2023 Flavio Gonçalves Garcia 4 | ## 5 | ## Licensed under the Apache License, Version 2.0 (the "License"); 6 | ## you may not use this file except in compliance with the License. 7 | ## You may obtain a copy of the License at 8 | ## 9 | ## http://www.apache.org/licenses/LICENSE-2.0 10 | ## 11 | ## Unless required by applicable law or agreed to in writing, software 12 | ## distributed under the License is distributed on an "AS IS" BASIS, 13 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ## See the License for the specific language governing permissions and 15 | ## limitations under the License. 16 | ## 17 | ## install_service.sh Install peeble, minica and generates minica keys for 18 | ## a localhost instance of peeble be certified for tests. 19 | ## 20 | ## description: Peeble service runner. 21 | ## processname: install_service.sh 22 | ## 23 | ## Author: Flavio Garcia 24 | 25 | OK_STRING="[ \033[32mOK\033[37m ]" 26 | 27 | echo "Installing Peeble: " 28 | git clone https://github.com/letsencrypt/pebble.git 29 | cd pebble || exit 30 | go install ./cmd/pebble 31 | cd - || exit 32 | rm -rf pebble 33 | echo -e "Peeble installed .......... $OK_STRING" 34 | echo -n "Installing Minica ........ " 35 | go install github.com/jsha/minica@latest 36 | echo -e " $OK_STRING" 37 | echo -n "Generating minica keys ... " 38 | rm -rf tests/certs/*.pem tests/certs/localhost 39 | cd tests/certs || exit 40 | "$GOPATH"/bin/minica -domains localhost -ca-cert candango.minica.pem -ca-key candango.minica.key.pem 41 | cd - > /dev/null || exit 42 | echo -e " $OK_STRING" 43 | -------------------------------------------------------------------------------- /scripts/pebble_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## 3 | ## Copyright 2019-2023 Flavio Gonçalves Garcia 4 | ## Copyright 2020 Viktor Szépe 5 | ## 6 | ## Licensed under the Apache License, Version 2.0 (the "License"); 7 | ## you may not use this file except in compliance with the License. 8 | ## You may obtain a copy of the License at 9 | ## 10 | ## http://www.apache.org/licenses/LICENSE-2.0 11 | ## 12 | ## Unless required by applicable law or agreed to in writing, software 13 | ## distributed under the License is distributed on an "AS IS" BASIS, 14 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | ## See the License for the specific language governing permissions and 16 | ## limitations under the License. 17 | ## 18 | ## pebble_service.sh This shell script takes care of starting and stopping 19 | ## pebble. 20 | ## 21 | ## description: Peeble service runner. 22 | ## processname: pebble_service.sh 23 | ## 24 | ## Author: Flavio Garcia 25 | 26 | AWK_CMD="/usr/bin/awk" 27 | PWD_PATH="/usr/bin/pwd" 28 | 29 | SCRIPT_PATH=$(dirname "$0") 30 | SCRIPT_NAME=$(basename "$0") 31 | 32 | SCRIPT_OK=0 33 | SCRIPT_ERROR=1 34 | PEEBLE_CMD=$GOPATH/bin/pebble 35 | PEEBLE_SERVICE_URL="https://localhost:14000" 36 | 37 | OK_STRING="[ \033[32mOK\033[37m ]" 38 | 39 | # contains(string, substring) 40 | # 41 | # Returns 0 if the specified string contains the specified substring, 42 | # otherwise returns 1. 43 | contains() 44 | { 45 | local string=i"$1" 46 | local substring="$2" 47 | 48 | # Whether $substring is in $string 49 | test "${string#*$substring}" != "$string" 50 | } 51 | 52 | send_error() 53 | { 54 | local error="$1" 55 | cat <&2 56 | $error 57 | EOF 58 | 59 | exit $SCRIPT_ERROR 60 | } 61 | 62 | is_running() 63 | { 64 | for out in $(ps aux | grep "$2" | $AWK_CMD '{print $11";"$2}') 65 | do 66 | PROC=$(echo $out | sed -e "s/;/ /g" | $AWK_CMD '{print $1}') 67 | if contains "$PROC" "pebble"; then 68 | return 0 69 | fi 70 | done 71 | 72 | return $SCRIPT_ERROR 73 | } 74 | 75 | start_pebble() 76 | { 77 | export PEBBLE_WFE_NONCEREJECT=0 78 | export PEBBLE_VA_ALWAYS_VALID=1 79 | export PEBBLE_VA_NOSLEEP=1 80 | 81 | echo "*************************************************************************************************" 82 | echo "* Candango automatoes Pebble Server Start Process" 83 | echo "* Config File: $2" 84 | echo "*" 85 | echo -n "* Starting Pebble Server " 86 | nohup $PEEBLE_CMD -config $2 >/dev/null 2>&1 & 87 | RETVAL=$(curl --cacert "$SCRIPT_PATH/../tests/certs/candango.minica.pem" --write-out %{http_code} --silent --output /dev/null "$PEEBLE_SERVICE_URL/dir" | tr -d ' ') 88 | while [ $RETVAL -ne 200 ] 89 | do 90 | sleep 1 91 | echo -n "." 92 | RETVAL=$(curl --cacert "$SCRIPT_PATH/../tests/certs/candango.minica.pem" --write-out %{http_code} --silent --output /dev/null "$PEEBLE_SERVICE_URL/dir" | tr -d ' ') 93 | done 94 | echo -e " $OK_STRING" 95 | echo "*************************************************************************************************" 96 | 97 | return $SCRIPT_OK 98 | } 99 | 100 | stop_pebble() 101 | { 102 | echo "*************************************************************************************************" 103 | echo "* Candango automatoes Pebble Server Start Process" 104 | echo "* Config File: $2" 105 | echo "*" 106 | echo -n "* Stopping Pebble Pebble Server " 107 | for out in $(ps aux | grep $2 | $AWK_CMD '{print $11";"$2}') 108 | do 109 | PROC=$(echo $out | sed -e "s/;/ /g" | $AWK_CMD '{print $1}') 110 | if contains $PROC "pebble"; then 111 | PID=$(echo $out | sed -e "s/;/ /g" | $AWK_CMD '{print $2}') 112 | kill -9 $PID 113 | echo -e " $OK_STRING" 114 | echo "*************************************************************************************************" 115 | return $SCRIPT_OK 116 | fi 117 | done 118 | 119 | return $SCRIPT_ERROR 120 | } 121 | 122 | pebble_option_list() 123 | { 124 | case "$1" in 125 | start) 126 | if is_running "$@"; then 127 | send_error "Peeble Server $2 is still running..." 128 | else 129 | start_pebble "$@" 130 | fi 131 | ;; 132 | stop) 133 | if is_running "$@"; then 134 | stop_pebble "$@" 135 | else 136 | send_error "Peeble Server $2 is not running..." 137 | fi 138 | ;; 139 | status) 140 | if is_running "$@"; then 141 | echo "Peeble Server $2 is running..." 142 | else 143 | echo "Peeble Server $2 is not running..." 144 | fi 145 | ;; 146 | *) 147 | send_error "Usage: $SCRIPT_NAME {start|stop|status} FILE_NAME" 148 | ;; 149 | esac 150 | } 151 | 152 | pebble_option_list "$@" 153 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2025 Flavio Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import automatoes 19 | from setuptools import find_packages, setup 20 | import os 21 | 22 | with open("README.md", "r") as fh: 23 | long_description = fh.read() 24 | 25 | 26 | # Solution from http://bit.ly/29Yl8VN 27 | def resolve_requires(requirements_file): 28 | requires = [] 29 | if os.path.isfile(f"./{requirements_file}"): 30 | file_dir = os.path.dirname(f"./{requirements_file}") 31 | with open(f"./{requirements_file}") as f: 32 | for raw_line in f.readlines(): 33 | line = raw_line.strip().replace("\n", "") 34 | if len(line) > 0: 35 | if line.startswith("-r "): 36 | partial_file = os.path.join(file_dir, line.replace( 37 | "-r ", "")) 38 | partial_requires = resolve_requires(partial_file) 39 | requires = requires + partial_requires 40 | continue 41 | requires.append(line) 42 | return requires 43 | 44 | 45 | setup( 46 | name="automatoes", 47 | version=automatoes.get_version(), 48 | license=automatoes.__licence__, 49 | description=("Let's Encrypt/ACME V2 client replacement for Manuale. Manual" 50 | " or automated your choice."), 51 | long_description=long_description, 52 | long_description_content_type="text/markdown", 53 | url="https://github.com/candango/automatoes", 54 | author=automatoes.get_author(), 55 | author_email=automatoes.get_author_email(), 56 | python_requires=">= 3.9", 57 | classifiers=[ 58 | "Development Status :: 5 - Production/Stable", 59 | "License :: OSI Approved :: Apache Software License", 60 | "Environment :: Console", 61 | "Environment :: Web Environment", 62 | "Intended Audience :: Developers", 63 | "Intended Audience :: System Administrators", 64 | "Programming Language :: Python :: 3", 65 | "Programming Language :: Python :: 3.9", 66 | "Programming Language :: Python :: 3.10", 67 | "Programming Language :: Python :: 3.11", 68 | "Programming Language :: Python :: 3.12", 69 | "Programming Language :: Python :: 3.13", 70 | "Programming Language :: Python :: 3 :: Only", 71 | ], 72 | packages=find_packages(), 73 | package_dir={'automatoes': "automatoes"}, 74 | include_package_data=True, 75 | install_requires=resolve_requires("requirements/basic.txt"), 76 | entry_points={ 77 | 'console_scripts': [ 78 | "automatoes = automatoes.cli:automatoes_main", 79 | "manuale = automatoes.cli:manuale_main", 80 | ], 81 | }, 82 | ) 83 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2024 Flavio Garcia 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | 17 | 18 | TEST_ROOT = os.path.dirname(os.path.abspath(__file__)) 19 | FIXTURES_ROOT = os.path.abspath(os.path.join(TEST_ROOT, "fixtures")) 20 | PROJECT_ROOT = os.path.abspath(os.path.join(TEST_ROOT, "..")) 21 | 22 | 23 | def get_absolute_path(directory): 24 | return os.path.realpath( 25 | os.path.join(os.path.dirname(__file__), directory) 26 | ) 27 | -------------------------------------------------------------------------------- /tests/ari_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2025 Flavio Garcia 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from . import FIXTURES_ROOT 16 | from automatoes.crypto import (generate_ari_data, 17 | get_certificate_aki, 18 | get_certificate_serial, 19 | load_pem_certificate) 20 | import base64 21 | from cartola import fs 22 | import unittest 23 | import os 24 | 25 | 26 | class ARITestCase(unittest.TestCase): 27 | """ Tests the crypto module from automatoes 28 | """ 29 | 30 | def test_aki(self): 31 | """ Test the strip_certificate function """ 32 | key_directory = os.path.join(FIXTURES_ROOT, "keys", "candango.org", 33 | "another") 34 | 35 | key_crt = fs.read( 36 | os.path.join(key_directory, "another.candango.org.crt"), 37 | True 38 | ) 39 | 40 | pem_crt = load_pem_certificate(key_crt) 41 | 42 | aki = get_certificate_aki(pem_crt) 43 | serial = get_certificate_serial(pem_crt) 44 | expected_aki = ("c0:cc:03:46:b9:58:20:cc:5c:72:70:f3:e1:2e:cb:20:a6:" 45 | "f5:68:3a") 46 | expected_serial = ("fa:f3:97:73:26:ea:e8:44:e7:14:00:20:ae:90:60:af:" 47 | "ba:44") 48 | aki_b64 = base64.urlsafe_b64encode(expected_aki.encode()) 49 | serial_b64 = base64.urlsafe_b64encode(expected_serial.encode()) 50 | 51 | self.assertEqual(expected_aki, aki) 52 | self.assertEqual(expected_serial, serial) 53 | self.assertEqual(f"{aki_b64}.{serial_b64}", generate_ari_data(pem_crt)) 54 | -------------------------------------------------------------------------------- /tests/certs/.keep_cert_dir: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candango/automatoes/d80ae4c83e8ce70812878c613227a7c036ce591a/tests/certs/.keep_cert_dir -------------------------------------------------------------------------------- /tests/conf/pebble-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "pebble": { 3 | "listenAddress": "0.0.0.0:14000", 4 | "managementListenAddress": null, 5 | "certificate": "tests/certs/localhost/cert.pem", 6 | "privateKey": "tests/certs/localhost/key.pem", 7 | "httpPort": 5002, 8 | "tlsPort": 5001, 9 | "ocspResponderURL": "" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/crypto_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2024 Flavio Garcia 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from . import FIXTURES_ROOT 16 | from automatoes.crypto import strip_certificates 17 | from cartola import fs 18 | import unittest 19 | import os 20 | 21 | 22 | class CryptoTestCase(unittest.TestCase): 23 | """ Tests the crypto module from automatoes 24 | """ 25 | 26 | def test_strip_certificate(self): 27 | """ Test the strip_certificate function """ 28 | key_directory = os.path.join(FIXTURES_ROOT, "keys", "candango.org", 29 | "another") 30 | chain_crt = fs.read( 31 | os.path.join(key_directory, "another.candango.org.chain.crt"), 32 | True 33 | ) 34 | chain_crt_x = strip_certificates(chain_crt) 35 | self.assertEqual(2, len(chain_crt_x)) 36 | 37 | key_crt = fs.read( 38 | os.path.join(key_directory, "another.candango.org.crt"), 39 | True 40 | ) 41 | 42 | intermediate_crt = fs.read( 43 | os.path.join(key_directory, 44 | "another.candango.org.intermediate.crt"), 45 | True 46 | ) 47 | 48 | self.assertEqual(key_crt, chain_crt_x[0]) 49 | self.assertEqual(intermediate_crt, chain_crt_x[1]) 50 | -------------------------------------------------------------------------------- /tests/features/00_nonce.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Flavio Garcia 2 | # Copyright 2016-2017 Veeti Paananen under MIT License 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | Feature: Replay Nonce 17 | 18 | Scenario: Get ACME V2 Replay Nonce 19 | # Enter steps here 20 | Given We have a newNonce url from ACME V2 directory 21 | When We request nonce from ACME V2 server 22 | Then ACME V2 server provides nonce in response headers 23 | -------------------------------------------------------------------------------- /tests/features/01_user_management.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2020 Flavio Garcia 2 | # Copyright 2016-2017 Veeti Paananen under MIT License 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | Feature: User Management 17 | 18 | Scenario: Create a new ACME V2 user 19 | 20 | Given We have a newAccount url from ACME V2 directory 21 | When We have permission to create the user file at features/sandbox 22 | And We ask to create an ACME V2 user 23 | Then User file is created successfully at features/sandbox/account.json 24 | And User contacts are stored at features/sandbox/user_contacts.txt 25 | 26 | Scenario: Retrieve existent ACME V2 user information 27 | 28 | Given User file exists at features/sandbox/account.json 29 | When We ask to get registration from ACME V2 user 30 | And User contacts are read from features/sandbox/user_contacts.txt 31 | Then Contacts from response match against stored ones 32 | And File is cleaned from features/sandbox/user_contacts.txt 33 | And File is cleaned from features/sandbox/account.json 34 | -------------------------------------------------------------------------------- /tests/features/03_authorize_and_issue_domains.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2020 Flavio Garcia 2 | # Copyright 2016-2017 Veeti Paananen under MIT License 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | Feature: ACME V2 Authorize domains and issue certificates 17 | 18 | Scenario: Initialize user to authorize and issue domains 19 | Given We have a newAccount url from ACME V2 directory 20 | When We ask to create an ACME V2 user 21 | Then User file is created successfully at features/sandbox/account.json 22 | 23 | Scenario: Create order for one domain by dns 24 | 25 | Given User file exists at features/sandbox/account.json 26 | When We create new order for testdns.candango.org by dns 27 | Then Response identifiers and authorizations size must be 1 28 | And Order file is stored at features/sandbox/testdns.candango.org.order.json 29 | 30 | Scenario: Validate challenges and finalize order for one domain by dns 31 | 32 | Given User file exists at features/sandbox/account.json 33 | And Order file exists at features/sandbox/testdns.candango.org.order.json 34 | When We verify challenges from order for testdns.candango.org by dns 35 | And We finalize order for testdns.candango.org by dns 36 | Then Finalized order response status must be processing 37 | And We wait for fulfillment to be valid 38 | And Order file is stored at features/sandbox/testdns.candango.org.order.json 39 | 40 | Scenario: Issue a certificate for one domain by dns 41 | 42 | Given User file exists at features/sandbox/account.json 43 | And Order file exists at features/sandbox/testdns.candango.org.order.json 44 | When Order has a certificate uri 45 | And We download testdns.candango.org certificate 46 | Then Order has a certificate with testdns.candango.org domain 47 | And File is cleaned from features/sandbox/testdns.candango.org.order.json 48 | 49 | Scenario: Create order for wildcard domain by dns 50 | 51 | Given User file exists at features/sandbox/account.json 52 | When We create new order for *.candango.org by dns 53 | Then Response identifiers and authorizations size must be 1 54 | And Order file is stored at features/sandbox/wildcard.candango.org.order.json 55 | 56 | Scenario: Validate challenges and finalize order for wildcard domain by dns 57 | 58 | Given User file exists at features/sandbox/account.json 59 | And Order file exists at features/sandbox/wildcard.candango.org.order.json 60 | When We verify challenges from order for candango.org by dns 61 | And We finalize order for *.candango.org by dns 62 | Then Finalized order response status must be processing 63 | And We wait for fulfillment to be valid 64 | And Order file is stored at features/sandbox/wildcard.candango.org.order.json 65 | 66 | Scenario: Issue a certificate for wildcard domain by dns 67 | 68 | Given User file exists at features/sandbox/account.json 69 | And Order file exists at features/sandbox/wildcard.candango.org.order.json 70 | When Order has a certificate uri 71 | And We download *.candango.org certificate 72 | Then Order has a certificate with *.candango.org domain 73 | And File is cleaned from features/sandbox/wildcard.candango.org.order.json 74 | 75 | Scenario: Create order for multiple domains by dns 76 | 77 | Given User file exists at features/sandbox/account.json 78 | When We create new order for testmulti1.candango.org testmulti2.candango.org by dns 79 | Then Response identifiers and authorizations size must be 2 80 | And Order file is stored at features/sandbox/testmulti.candango.org.order.json 81 | 82 | Scenario: Validate challenges and finalize order for multiple domains by dns 83 | 84 | Given User file exists at features/sandbox/account.json 85 | And Order file exists at features/sandbox/testmulti.candango.org.order.json 86 | When We verify challenges from order for testmulti1.candango.org by dns 87 | And We verify challenges from order for testmulti2.candango.org by dns 88 | And We finalize order for testmulti1.candango.org testmulti2.candango.org by dns 89 | Then Finalized order response status must be processing 90 | And We wait for fulfillment to be valid 91 | And Order file is stored at features/sandbox/testmulti.candango.org.order.json 92 | 93 | Scenario: Issue a certificate for multiple domains by dns 94 | 95 | Given User file exists at features/sandbox/account.json 96 | And Order file exists at features/sandbox/testmulti.candango.org.order.json 97 | When Order has a certificate uri 98 | And We download testmulti1.candango.org testmulti2.candango.org certificate 99 | Then Order has a certificate with testmulti1.candango.org domain 100 | And File is cleaned from features/sandbox/testmulti.candango.org.order.json 101 | 102 | 103 | Scenario: Creating order for one domain by http 104 | 105 | Given User file exists at features/sandbox/account.json 106 | When We create new order for testhttp.candango.org by http 107 | Then Response identifiers and authorizations size must be 1 108 | And Order file is stored at features/sandbox/testhttp.candango.org.order.json 109 | 110 | Scenario: Validate challenges and finalizing order for one domain by http 111 | 112 | Given User file exists at features/sandbox/account.json 113 | And Order file exists at features/sandbox/testhttp.candango.org.order.json 114 | When We verify challenges from order for testhttp.candango.org by http 115 | And We finalize order for testhttp.candango.org by http 116 | Then Finalized order response status must be processing 117 | And We wait for fulfillment to be valid 118 | And Order file is stored at features/sandbox/testhttp.candango.org.order.json 119 | 120 | 121 | Scenario: Issue a certificate for one domain by http 122 | 123 | Given User file exists at features/sandbox/account.json 124 | And Order file exists at features/sandbox/testhttp.candango.org.order.json 125 | When Order has a certificate uri 126 | And We download testhttp.candango.org certificate 127 | Then Order has a certificate with testhttp.candango.org domain 128 | And File is cleaned from features/sandbox/testhttp.candango.org.order.json 129 | -------------------------------------------------------------------------------- /tests/features/04_certificate_revogation.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2020 Flavio Garcia 2 | # Copyright 2016-2017 Veeti Paananen under MIT License 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | Feature: ACME V2 Certificate revocation 17 | 18 | Scenario: Initialize user to authorize and issue domains 19 | Given We have a newAccount url from ACME V2 directory 20 | When We ask to create an ACME V2 user 21 | Then User file is created successfully at features/sandbox/account.json 22 | 23 | Scenario: Create order and validate a domain 24 | 25 | Given User file exists at features/sandbox/account.json 26 | When We create new order for valid.candango.org by dns 27 | And We verify challenges from order for valid.candango.org by dns 28 | And We finalize order for valid.candango.org by dns 29 | Then Finalized order response status must be processing 30 | And We wait for fulfillment to be valid 31 | And Order file is stored at features/sandbox/valid.candango.org.order.json 32 | 33 | Scenario: Issue a certificate for a domain 34 | 35 | Given User file exists at features/sandbox/account.json 36 | And Order file exists at features/sandbox/valid.candango.org.order.json 37 | When Order has a certificate uri 38 | And We download valid.candango.org certificate 39 | Then Order has a certificate with valid.candango.org domain 40 | And File is cleaned from features/sandbox/valid.candango.org.order.json 41 | And Certificate file is stored at features/sandbox/valid.candango.org.cert 42 | 43 | Scenario: Revoke a certificate 44 | 45 | Given User file exists at features/sandbox/account.json 46 | And Certificate file exists at features/sandbox/valid.candango.org.cert 47 | When We revoke a certificate 48 | Then File is cleaned from features/sandbox/valid.candango.org.cert 49 | And File is cleaned from features/sandbox/account.json 50 | -------------------------------------------------------------------------------- /tests/features/05_migrate_account.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Flávio Gonçalves Garcia 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | Feature: Migrate account 16 | 17 | Scenario: Create a new ACME V2 user and convert to certbot format 18 | 19 | Given We have a newAccount url from ACME V2 directory 20 | When We have permission to create the user file at features/sandbox 21 | And We ask to create an ACME V2 user 22 | Then Convert account to certbot format 23 | And User contacts are stored at features/sandbox/user_contacts.txt 24 | 25 | Scenario: Migrate an account register using certbot to automatoes 26 | # Enter steps here 27 | Given A certbot account is located at features/sandbox path 28 | When A RSA key is converted from the key data file parameter 29 | And An automatoes account is created 30 | Then User file is created successfully at features/sandbox/account.json 31 | #Then ACME V2 server provides nonce in response headers 32 | 33 | Scenario: Retrieve existent ACME V2 user information 34 | 35 | Given User file exists at features/sandbox/account.json 36 | When We ask to get registration from ACME V2 user 37 | And User contacts are read from features/sandbox/user_contacts.txt 38 | Then Contacts from response match against stored ones 39 | And File is cleaned from features/sandbox/user_contacts.txt 40 | And File is cleaned from features/sandbox/account.json 41 | And File is cleaned from features/sandbox/meta.json 42 | And File is cleaned from features/sandbox/private_key.json 43 | And File is cleaned from features/sandbox/regr.json 44 | -------------------------------------------------------------------------------- /tests/features/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candango/automatoes/d80ae4c83e8ce70812878c613227a7c036ce591a/tests/features/__init__.py -------------------------------------------------------------------------------- /tests/features/environment.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2024 Flavio Garcia 2 | # Copyright 2016-2017 Veeti Paananen under MIT License 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # Staging server from: 17 | # https://community.letsencrypt.org/t/staging-endpoint-for-acme-v2/49605 18 | from behave import fixture, use_fixture 19 | from automatoes.acme import AcmeV2 20 | from automatoes.protocol import AcmeV2Pesant, AcmeRequestsTransport 21 | import os 22 | from unittest.case import TestCase 23 | 24 | peeble_url = "https://localhost:14000" 25 | 26 | 27 | def get_absolute_path(directory): 28 | return os.path.realpath( 29 | os.path.join(os.path.dirname(__file__), "..", directory) 30 | ) 31 | 32 | 33 | @fixture 34 | def acme_protocol(context, timeout=1, **kwargs): 35 | transport = AcmeRequestsTransport(peeble_url) 36 | context.acme_protocol = AcmeV2Pesant( 37 | transport, 38 | url=peeble_url, 39 | directory="dir", 40 | verify=get_absolute_path("certs/candango.minica.pem") 41 | ) 42 | yield context.acme_protocol 43 | 44 | 45 | @fixture 46 | def acme_v2(context, timeout=1, **kwargs): 47 | context.acme_v2 = AcmeV2( 48 | peeble_url, 49 | None, 50 | directory="dir", 51 | verify=get_absolute_path("certs/candango.minica.pem") 52 | ) 53 | yield context.acme_v2 54 | 55 | 56 | @fixture 57 | def peeble_url_context(context, timeout=1, **kwargs): 58 | context.peeble_url = peeble_url 59 | yield context.peeble_url 60 | 61 | 62 | @fixture 63 | def tester(context, timeout=1, **kwargs): 64 | context.tester = TestCase() 65 | yield context.tester 66 | 67 | 68 | def before_all(context): 69 | use_fixture(acme_protocol, context) 70 | use_fixture(acme_v2, context) 71 | use_fixture(peeble_url_context, context) 72 | use_fixture(tester, context) 73 | -------------------------------------------------------------------------------- /tests/features/sandbox/account_sandbox.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candango/automatoes/d80ae4c83e8ce70812878c613227a7c036ce591a/tests/features/sandbox/account_sandbox.txt -------------------------------------------------------------------------------- /tests/features/steps/acme_v2_steps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2019-2022 Flávio Gonçalves Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | from behave import given, when, then 19 | from automatoes.crypto import (create_csr, generate_rsa_key, 20 | strip_certificates, load_pem_certificate, 21 | get_certificate_domain_name, 22 | get_issuer_certificate_domain_name) 23 | from automatoes.model import Account 24 | from cartola import security 25 | 26 | 27 | @given("We have a {what_url} url from ACME V2 directory") 28 | def step_v2_server_is_accessible(context, what_url): 29 | new_nonce_url = context.acme_protocol.directory()[what_url] 30 | context.tester.assertEqual( 31 | context.peeble_url, "/".join(new_nonce_url.split("/")[0:3])) 32 | 33 | 34 | @when("We request nonce from ACME V2 server") 35 | def step_we_request_nonce_from_acme_v2_server(context): 36 | context.nonce = context.acme_v2.get_nonce() 37 | 38 | 39 | @then("ACME V2 server provides nonce in response headers") 40 | def step_acme_v2_server_provides_nonce_in_response_headers(context): 41 | context.tester.assertFalse(context.nonce is None) 42 | 43 | 44 | @when("We ask to create an ACME V2 user") 45 | def step_we_ask_to_create_an_acme_v2_user(context): 46 | user_name = "candango_{}_{}@candango.org".format( 47 | security.random_string(5, False, False), 48 | security.random_string(5, False, False) 49 | ) 50 | # To check against the get_registration method after 51 | # TODO: check against more than one emails in the contacts 52 | context.user_contacts = [user_name] 53 | peeble_term = ("data:text/plain,Do%20what%20thou%20wilt") 54 | context.acme_v2.set_account(Account(key=generate_rsa_key(4096))) 55 | response = context.acme_v2.register(user_name, True) 56 | context.tester.assertEqual(peeble_term, response.terms) 57 | context.tester.assertEqual("valid", response.contents['status']) 58 | context.tester.assertEqual( 59 | context.peeble_url, "/".join(response.uri.split("/")[0:3])) 60 | context.tester.assertEqual( 61 | "my-account", "/".join(response.uri.split("/")[3:4])) 62 | context.tester.assertIsInstance(response.uri.split("/")[4:5][0], str) 63 | context.acme_v2.get_registration() 64 | 65 | 66 | @when("We ask to get registration from ACME V2 user") 67 | def step_we_ask_to_get_registration_from_ACME_V2_user(context): 68 | response = context.acme_v2.get_registration() 69 | context.tester.assertEqual("valid", response['status']) 70 | context.get_registration_response = response 71 | 72 | 73 | @then("Contacts from response match against stored ones") 74 | def step_contacts_from_response_match_against_stored_ones(context): 75 | context.tester.assertEqual( 76 | context.stored_user_contacts, 77 | context.get_registration_response['contact'][0].replace("mailto:", "") 78 | ) 79 | 80 | 81 | @when("We create new order for {what_domain} by {what_type}") 82 | def step_we_create_new_order_for_domain_by_type( 83 | context, what_domain, what_type): 84 | what_domains = what_domain.split(" ") 85 | if len(what_domains) > 1: 86 | what_domain = what_domains 87 | 88 | context.order = context.acme_v2.new_order(what_domain, what_type) 89 | 90 | 91 | @then("Response identifiers and authorizations size must be {what_size}") 92 | def step_response_identifiers_and_authorizations_size_must_be_size( 93 | context, what_size): 94 | context.tester.assertEqual("pending", 95 | context.order.contents['status']) 96 | context.tester.assertEqual( 97 | int(what_size), 98 | len(context.order.contents['identifiers']) 99 | ) 100 | context.tester.assertEqual( 101 | int(what_size), 102 | len(context.order.contents['authorizations']) 103 | ) 104 | 105 | 106 | @when("We verify challenges from order for {what_domain} by {what_type}") 107 | def step_we_verify_challenges_from_order_for_domain_by_type( 108 | context, what_domain, what_type): 109 | challenges = context.acme_v2.get_order_challenges(context.order) 110 | for challenge in challenges: 111 | if challenge.domain == what_domain: 112 | challenge_response = context.acme_v2.verify_order_challenge( 113 | challenge, 1) 114 | context.tester.assertEqual('processing', 115 | challenge_response['status']) 116 | 117 | 118 | @when("We finalize order for {what_domain} by {what_type}") 119 | def step_we_finalize_order_for_domain_by_type( 120 | context, what_domain, what_type): 121 | domains = what_domain.split(" ") 122 | csr = create_csr(generate_rsa_key(4096), domains) 123 | context.finalize_order_response = context.acme_v2.finalize_order( 124 | context.order, csr) 125 | 126 | 127 | @then("Finalized order response status must be {status}") 128 | def step_finalized_order_response_status_must_be_status(context, status): 129 | context.tester.assertEqual(status, 130 | context.finalize_order_response['status']) 131 | 132 | 133 | @then("We wait for fulfillment to be {status}") 134 | def step_we_wait_for_fulfillment_to_be_status(context, status): 135 | context.order_fulfillment_response = ( 136 | context.acme_v2.await_for_order_fulfillment(context.order)) 137 | context.tester.assertEqual(status, 138 | context.order_fulfillment_response['status']) 139 | 140 | 141 | @when("Order has a certificate uri") 142 | def step_order_has_a_certificate_uri(context): 143 | context.tester.assertFalse(context.order.certificate_uri is None) 144 | 145 | 146 | @when("We download {what_domain} certificate") 147 | def step_we_download_domain_certificate(context, what_domain): 148 | context.order_certificate_response = ( 149 | context.acme_v2.download_order_certificate(context.order)) 150 | context.tester.assertFalse(context.order.certificate is None) 151 | 152 | 153 | @when("We revoke a certificate") 154 | def step_we_revoke_certificate(context): 155 | certificate = load_pem_certificate(context.certificate.encode("ascii")) 156 | revoke_certificate_response = ( 157 | context.acme_v2.revoke_certificate(certificate)) 158 | context.tester.assertEqual(200, revoke_certificate_response.status_code) 159 | 160 | 161 | @then("Order has a certificate with {what_domain} domain") 162 | def step_order_has_a_certificate_with_domain(context, what_domain): 163 | context.tester.assertFalse(context.order.certificate is None) 164 | certificates = strip_certificates(context.order.certificate) 165 | entity_certificate = load_pem_certificate(certificates[0]) 166 | context.certificate = certificates[0] 167 | context.tester.assertEqual( 168 | what_domain, 169 | get_certificate_domain_name(entity_certificate) 170 | ) 171 | issuer_certificate = load_pem_certificate(certificates[1]) 172 | context.tester.assertTrue( 173 | get_issuer_certificate_domain_name( 174 | issuer_certificate 175 | ).startswith("Pebble Intermediate CA") 176 | ) 177 | -------------------------------------------------------------------------------- /tests/features/steps/environment_steps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2020 Flavio Garcia 4 | # Copyright 2016-2017 Veeti Paananen under MIT License 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | from automatoes.model import Account, Order 19 | from behave import given, when, then, step 20 | from cartola import fs 21 | import os 22 | 23 | 24 | def get_absolute_path(directory): 25 | return os.path.realpath( 26 | os.path.join(os.path.dirname(__file__), "..", "..", directory) 27 | ) 28 | 29 | 30 | def create_file(path, content, binary=False): 31 | real_path = get_absolute_path(path) 32 | fs.write(real_path, content, binary) 33 | os.chmod(real_path, 0o600) 34 | return real_path 35 | 36 | 37 | @when("We have permission to create the user file at {directory}") 38 | def step_we_have_permission_to_create_the_user_file(context, directory): 39 | real_directory = get_absolute_path(directory) 40 | context.tester.assertTrue(os.path.isdir(real_directory)) 41 | context.tester.assertTrue(os.access(real_directory, os.W_OK)) 42 | 43 | 44 | @then("User file is created successfully at {account_path}") 45 | def user_file_is_created_successfully(context, account_path): 46 | real_account_path = create_file(account_path, 47 | context.acme_v2.account.serialize(), 48 | True) 49 | context.tester.assertTrue(os.path.exists(real_account_path)) 50 | context.tester.assertTrue(os.path.isfile(real_account_path)) 51 | 52 | 53 | @then("User contacts are stored at {account_path}") 54 | def user_contacts_are_stored_at(context, account_path): 55 | # This is for further checking against get registration 56 | real_account_path = create_file(account_path, 57 | ",".join(context.user_contacts)) 58 | context.tester.assertTrue(os.path.exists(real_account_path)) 59 | context.tester.assertTrue(os.path.isfile(real_account_path)) 60 | 61 | 62 | @step("User contacts are read from {account_path}") 63 | def user_contacts_are_read_from(context, account_path): 64 | user_contacts = None 65 | real_account_path = get_absolute_path(account_path) 66 | with open(real_account_path, 'r') as f: 67 | user_contacts = f.read() 68 | context.stored_user_contacts = user_contacts 69 | 70 | 71 | @given("User file exists at {account_path}") 72 | def user_file_exists_at(context, account_path): 73 | real_account_path = get_absolute_path(account_path) 74 | context.tester.assertTrue(os.path.exists(real_account_path)) 75 | context.tester.assertTrue(os.path.isfile(real_account_path)) 76 | data = None 77 | with open(real_account_path, 'r') as f: 78 | data = f.read() 79 | context.acme_v2.account = Account.deserialize(data) 80 | 81 | 82 | @given("Certificate file exists at {certificate_path}") 83 | def certificate_file_exists_at(context, certificate_path): 84 | real_certificate_path = get_absolute_path(certificate_path) 85 | context.tester.assertTrue(os.path.exists(real_certificate_path)) 86 | context.tester.assertTrue(os.path.isfile(real_certificate_path)) 87 | context.certificate = fs.read(real_certificate_path) 88 | 89 | 90 | @then("Order file is stored at {order_path}") 91 | def order_file_is_stored_at_path(context, order_path): 92 | real_order_path = create_file(order_path, 93 | context.order.serialize(), 94 | True) 95 | context.tester.assertTrue(os.path.exists(real_order_path)) 96 | context.tester.assertTrue(os.path.isfile(real_order_path)) 97 | 98 | 99 | @then("Certificate file is stored at {certificate_path}") 100 | def certificate_file_is_stored_at_path(context, certificate_path): 101 | real_certificate_path = create_file(certificate_path, 102 | context.certificate, 103 | True) 104 | context.tester.assertTrue(os.path.exists(real_certificate_path)) 105 | context.tester.assertTrue(os.path.isfile(real_certificate_path)) 106 | 107 | 108 | @then("File is cleaned from {path}") 109 | def order_file_is_stored_at_path(context, path): 110 | real_path = get_absolute_path(path) 111 | context.tester.assertTrue(os.path.exists(real_path)) 112 | context.tester.assertTrue(os.path.isfile(real_path)) 113 | os.remove(real_path) 114 | context.tester.assertFalse(os.path.exists(real_path)) 115 | 116 | 117 | @given("Order file exists at {order_path}") 118 | def order_file_exists_at_path(context, order_path): 119 | real_order_path = get_absolute_path(order_path) 120 | context.tester.assertTrue(os.path.exists(real_order_path)) 121 | context.tester.assertTrue(os.path.isfile(real_order_path)) 122 | data = None 123 | with open(real_order_path, 'r') as f: 124 | data = f.read() 125 | context.order = Order.deserialize(data) 126 | -------------------------------------------------------------------------------- /tests/features/steps/migrate_steps.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2023 Flávio Gonçalves Garcia 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from automatoes.crypto import (certbot_key_data_to_int, 16 | generate_rsa_key_from_parameters) 17 | from automatoes.acme import AcmeV2 18 | from automatoes.model import Account 19 | import base64 20 | from behave import given, when, then 21 | import binascii 22 | from cartola import fs 23 | from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey 24 | import json 25 | import os 26 | from tests.features.steps.environment_steps import (create_file, 27 | get_absolute_path) 28 | 29 | 30 | def key_int_to_data(value: int) -> str: 31 | """ This is the inversion of what happens inside 32 | automatoes.crypto.certbot_key_data_to_int 33 | """ 34 | hex_value = hex(value).replace("0x", "") 35 | if len(hex_value) % 2: 36 | hex_value = "0%s" % hex_value 37 | return base64.b64encode( 38 | binascii.unhexlify(hex_value), b"-_" 39 | ).replace(b"=", b"").decode() 40 | 41 | 42 | @given("A certbot account is located at {certbot_path} path") 43 | def certbot_account_located_at(context, certbot_path): 44 | certbot_path = get_absolute_path(certbot_path) 45 | 46 | context.tester.assertTrue(os.path.exists(certbot_path)) 47 | meta_path = os.path.join(certbot_path, "meta.json") 48 | key_path = os.path.join(certbot_path, "private_key.json") 49 | regr_path = os.path.join(certbot_path, "regr.json") 50 | context.tester.assertTrue(os.path.isfile(meta_path)) 51 | context.tester.assertTrue(os.path.isfile(key_path)) 52 | context.tester.assertTrue(os.path.isfile(regr_path)) 53 | 54 | context.meta_data = json.loads(fs.read(meta_path)) 55 | context.key_data = json.loads(fs.read(key_path)) 56 | context.regr_data = json.loads(fs.read(regr_path)) 57 | 58 | 59 | @when("A RSA key is converted from the key data file parameter") 60 | def rsa_key_converted_from_key_data(context): 61 | key_data_int = certbot_key_data_to_int(context.key_data) 62 | context.private_key = generate_rsa_key_from_parameters( 63 | key_data_int['p'], key_data_int['q'], key_data_int['d'], 64 | key_data_int['dp'], key_data_int['dq'], key_data_int['qi'], 65 | key_data_int['e'], key_data_int['n'] 66 | ) 67 | context.tester.assertTrue(isinstance(context.private_key, RSAPrivateKey)) 68 | 69 | 70 | @when("An automatoes account is created") 71 | def automatoes_account_created(context): 72 | account = Account(key=context.private_key, uri=context.regr_data['uri']) 73 | context.acme_v2.set_account(account) 74 | 75 | 76 | @then("Convert account to certbot format") 77 | def convert_account_to_certbot_format(context): 78 | key: RSAPrivateKey = context.acme_v2.account.key 79 | 80 | sandbox_path = os.path.join(os.getcwd(), "tests", "features", "sandbox") 81 | key_path = os.path.join(sandbox_path, "private_key.json") 82 | meta_path = os.path.join(sandbox_path, "meta.json") 83 | regr_path = os.path.join(sandbox_path, "regr.json") 84 | 85 | key_data = { 86 | 'p': key_int_to_data(key.private_numbers().p), 87 | 'q': key_int_to_data(key.private_numbers().q), 88 | 'd': key_int_to_data(key.private_numbers().d), 89 | 'dp': key_int_to_data(key.private_numbers().dmp1), 90 | 'dq': key_int_to_data(key.private_numbers().dmq1), 91 | 'qi': key_int_to_data(key.private_numbers().iqmp), 92 | 'e': key_int_to_data(key.public_key().public_numbers().e), 93 | 'n': key_int_to_data(key.public_key().public_numbers().n) 94 | } 95 | 96 | meta_data = { 97 | 'creation_dt': "2023-01-08T19:32:30Z", 98 | 'creation_host': "any-host", 99 | 'register_to_eff': ",".join(context.user_contacts) 100 | } 101 | 102 | acme_v2: AcmeV2 = context.acme_v2 103 | 104 | regr_data = { 105 | 'body': {}, 106 | 'uri': acme_v2.account.uri 107 | } 108 | 109 | real_key_path = create_file(key_path, json.dumps(key_data)) 110 | context.tester.assertTrue(os.path.isfile(real_key_path)) 111 | real_meta_path = create_file(meta_path, json.dumps(meta_data)) 112 | context.tester.assertTrue(os.path.isfile(real_meta_path)) 113 | real_regr_path = create_file(regr_path, json.dumps(regr_data)) 114 | context.tester.assertTrue(os.path.isfile(real_regr_path)) 115 | -------------------------------------------------------------------------------- /tests/fixtures/keys/candango.org/another/another.candango.org.chain.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGQDCCBSigAwIBAgITAPrzl3Mm6uhE5xQAIK6QYK+6RDANBgkqhkiG9w0BAQsF 3 | ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0yMDAxMjAy 4 | MTU3MzVaFw0yMDA0MTkyMTU3MzVaMB8xHTAbBgNVBAMTFGFub3RoZXIuY2FuZGFu 5 | Z28ub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA68YUnLMGlN4K 6 | KE1y2+v8kcpEu5gXvSRK0nZBgFWTScwEeSUt5jtC9FwCE5VzSlY4bFJn1V6VjJ6G 7 | fdnr2JRJeer9uUVIQbvVYIx/fcFrmpuG13Y81NdMMqfiRqE0boI7gGScaFdE7MG7 8 | 1TpwTrSiXbq3GD9AbcCtX25pBUOvBQntG4xhOZO8eUp7gmbStOEalFAssY338BZp 9 | d1EXXrHsc+Q79Hw4VM+Oz0q30GhwGCSBIfqkyrz2pnkkIATC9c2sGV7ZUrT3HzwC 10 | yw6J6FVG6QTZR5K/I1umnXo8yybJ0+M5IgOL0gn5Yoq+mb5xnrynaaAGp3GW+HAY 11 | fPypZXXPihMqECHl49q/ThMcrzUyhFeX4qnnXwJHP41+pTE+X2nKATtHGJrwlClx 12 | D6IOXkdKdh1dYOTS1t3KCF06TWZkS6TKow04vrwtMkGb2XLwDcOl7RcbNf+0yqHE 13 | v8Y9At+VKKva0fMpeE75bwJMFLYW+be2KXKGuSrwGDP8nIy2bIn6HmFLp9l45rll 14 | sNv7jDNSiv8M12pUgCK8RfnrGGKibcP9TdSUI1p6fXbEdvCIDjZ5WsBxHFmxFx0r 15 | Cph5rYI4KX7viNqZQ+1UNTANkX6qem1d2sCN10M6xv/UJBB6fhUaiQFEj0Wmh3Lc 16 | BEWNZMHjziMJXQu7ifaD6pisXinaN6sCAwEAAaOCAnAwggJsMA4GA1UdDwEB/wQE 17 | AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIw 18 | ADAdBgNVHQ4EFgQUwYbNcKuvyoWp1B6M5cEjYMMBmY0wHwYDVR0jBBgwFoAUwMwD 19 | RrlYIMxccnDz4S7LIKb1aDowdwYIKwYBBQUHAQEEazBpMDIGCCsGAQUFBzABhiZo 20 | dHRwOi8vb2NzcC5zdGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZzAzBggrBgEFBQcw 21 | AoYnaHR0cDovL2NlcnQuc3RnLWludC14MS5sZXRzZW5jcnlwdC5vcmcvMB8GA1Ud 22 | EQQYMBaCFGFub3RoZXIuY2FuZGFuZ28ub3JnMEwGA1UdIARFMEMwCAYGZ4EMAQIB 23 | MDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2Vu 24 | Y3J5cHQub3JnMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDvAHUAFuhpwdGV6tfD+Jca 25 | 4/B2AfeM4badMahSGLaDfzGoFQgAAAFvxSz17gAABAMARjBEAiA2UwoQ3+1ECmpz 26 | t7gAutTDSPtWrhR1T5FWpkHBDa7xqgIgaADfyA4FfsrpeuTV5POllWhSGxgSWzKo 27 | LR3Vwh/KAFAAdgDdmTT8peckgMlWaH2BNJkISbJJ97Vp2Me8qz9cwfNuZAAAAW/F 28 | LPXuAAAEAwBHMEUCIFGzpkb6JE1HQd7H57Y7LJ2iJgWKb3WZAvhqdDf3HydeAiEA 29 | qL8vB8EsHZ/TmndOwNyLQ99cj5k4CsGKk9hcUQoFzIwwDQYJKoZIhvcNAQELBQAD 30 | ggEBALbbUIeD9uuNE5gONWjiK499jEEKRAsP+J1qCQ9jyn8wV2iKkwoQiWsNzPlC 31 | 3fZKIKocsVuEStWW4DlIyqkoynA6m8CndZUw/YJUhuicrt2UTPNCWjNkjozlXbzT 32 | NfAK5vBdFU4f2gEqEqhK7zw3eDK8m2Ey9lzKAaI7adz3ruHz6R0Kr1omvoXyM1Lq 33 | WgKcgJsA1sx0CC0UjBPPOZCBS7WCu+pfb+m6M49qBi+m42lMfYrEoRJNAk26xgnq 34 | uI3uMraoQmB7dPX6hXWt5tFK9i3N2NhFJOkE84OZDQJzepojWd0wplVmXJqZ2Ln2 35 | P57H0O8N4i2RG+ITkLPs+e/n8L0= 36 | -----END CERTIFICATE----- 37 | -----BEGIN CERTIFICATE----- 38 | MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw 39 | GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 40 | MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw 41 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 42 | 8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym 43 | oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 44 | ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN 45 | xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 46 | dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 47 | AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw 48 | HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 49 | BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu 50 | b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu 51 | Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq 52 | hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF 53 | UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 54 | AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp 55 | DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 56 | IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf 57 | zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI 58 | PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w 59 | SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em 60 | 2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 61 | WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt 62 | n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= 63 | -----END CERTIFICATE----- 64 | -------------------------------------------------------------------------------- /tests/fixtures/keys/candango.org/another/another.candango.org.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGQDCCBSigAwIBAgITAPrzl3Mm6uhE5xQAIK6QYK+6RDANBgkqhkiG9w0BAQsF 3 | ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0yMDAxMjAy 4 | MTU3MzVaFw0yMDA0MTkyMTU3MzVaMB8xHTAbBgNVBAMTFGFub3RoZXIuY2FuZGFu 5 | Z28ub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA68YUnLMGlN4K 6 | KE1y2+v8kcpEu5gXvSRK0nZBgFWTScwEeSUt5jtC9FwCE5VzSlY4bFJn1V6VjJ6G 7 | fdnr2JRJeer9uUVIQbvVYIx/fcFrmpuG13Y81NdMMqfiRqE0boI7gGScaFdE7MG7 8 | 1TpwTrSiXbq3GD9AbcCtX25pBUOvBQntG4xhOZO8eUp7gmbStOEalFAssY338BZp 9 | d1EXXrHsc+Q79Hw4VM+Oz0q30GhwGCSBIfqkyrz2pnkkIATC9c2sGV7ZUrT3HzwC 10 | yw6J6FVG6QTZR5K/I1umnXo8yybJ0+M5IgOL0gn5Yoq+mb5xnrynaaAGp3GW+HAY 11 | fPypZXXPihMqECHl49q/ThMcrzUyhFeX4qnnXwJHP41+pTE+X2nKATtHGJrwlClx 12 | D6IOXkdKdh1dYOTS1t3KCF06TWZkS6TKow04vrwtMkGb2XLwDcOl7RcbNf+0yqHE 13 | v8Y9At+VKKva0fMpeE75bwJMFLYW+be2KXKGuSrwGDP8nIy2bIn6HmFLp9l45rll 14 | sNv7jDNSiv8M12pUgCK8RfnrGGKibcP9TdSUI1p6fXbEdvCIDjZ5WsBxHFmxFx0r 15 | Cph5rYI4KX7viNqZQ+1UNTANkX6qem1d2sCN10M6xv/UJBB6fhUaiQFEj0Wmh3Lc 16 | BEWNZMHjziMJXQu7ifaD6pisXinaN6sCAwEAAaOCAnAwggJsMA4GA1UdDwEB/wQE 17 | AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIw 18 | ADAdBgNVHQ4EFgQUwYbNcKuvyoWp1B6M5cEjYMMBmY0wHwYDVR0jBBgwFoAUwMwD 19 | RrlYIMxccnDz4S7LIKb1aDowdwYIKwYBBQUHAQEEazBpMDIGCCsGAQUFBzABhiZo 20 | dHRwOi8vb2NzcC5zdGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZzAzBggrBgEFBQcw 21 | AoYnaHR0cDovL2NlcnQuc3RnLWludC14MS5sZXRzZW5jcnlwdC5vcmcvMB8GA1Ud 22 | EQQYMBaCFGFub3RoZXIuY2FuZGFuZ28ub3JnMEwGA1UdIARFMEMwCAYGZ4EMAQIB 23 | MDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2Vu 24 | Y3J5cHQub3JnMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDvAHUAFuhpwdGV6tfD+Jca 25 | 4/B2AfeM4badMahSGLaDfzGoFQgAAAFvxSz17gAABAMARjBEAiA2UwoQ3+1ECmpz 26 | t7gAutTDSPtWrhR1T5FWpkHBDa7xqgIgaADfyA4FfsrpeuTV5POllWhSGxgSWzKo 27 | LR3Vwh/KAFAAdgDdmTT8peckgMlWaH2BNJkISbJJ97Vp2Me8qz9cwfNuZAAAAW/F 28 | LPXuAAAEAwBHMEUCIFGzpkb6JE1HQd7H57Y7LJ2iJgWKb3WZAvhqdDf3HydeAiEA 29 | qL8vB8EsHZ/TmndOwNyLQ99cj5k4CsGKk9hcUQoFzIwwDQYJKoZIhvcNAQELBQAD 30 | ggEBALbbUIeD9uuNE5gONWjiK499jEEKRAsP+J1qCQ9jyn8wV2iKkwoQiWsNzPlC 31 | 3fZKIKocsVuEStWW4DlIyqkoynA6m8CndZUw/YJUhuicrt2UTPNCWjNkjozlXbzT 32 | NfAK5vBdFU4f2gEqEqhK7zw3eDK8m2Ey9lzKAaI7adz3ruHz6R0Kr1omvoXyM1Lq 33 | WgKcgJsA1sx0CC0UjBPPOZCBS7WCu+pfb+m6M49qBi+m42lMfYrEoRJNAk26xgnq 34 | uI3uMraoQmB7dPX6hXWt5tFK9i3N2NhFJOkE84OZDQJzepojWd0wplVmXJqZ2Ln2 35 | P57H0O8N4i2RG+ITkLPs+e/n8L0= 36 | -----END CERTIFICATE----- 37 | -------------------------------------------------------------------------------- /tests/fixtures/keys/candango.org/another/another.candango.org.intermediate.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw 3 | GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 4 | MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw 5 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 6 | 8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym 7 | oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 8 | ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN 9 | xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 10 | dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 11 | AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw 12 | HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 13 | BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu 14 | b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu 15 | Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq 16 | hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF 17 | UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 18 | AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp 19 | DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 20 | IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf 21 | zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI 22 | PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w 23 | SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em 24 | 2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 25 | WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt 26 | n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /tests/fixtures/keys/candango.org/another/another.candango.org.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJJwIBAAKCAgEA68YUnLMGlN4KKE1y2+v8kcpEu5gXvSRK0nZBgFWTScwEeSUt 3 | 5jtC9FwCE5VzSlY4bFJn1V6VjJ6Gfdnr2JRJeer9uUVIQbvVYIx/fcFrmpuG13Y8 4 | 1NdMMqfiRqE0boI7gGScaFdE7MG71TpwTrSiXbq3GD9AbcCtX25pBUOvBQntG4xh 5 | OZO8eUp7gmbStOEalFAssY338BZpd1EXXrHsc+Q79Hw4VM+Oz0q30GhwGCSBIfqk 6 | yrz2pnkkIATC9c2sGV7ZUrT3HzwCyw6J6FVG6QTZR5K/I1umnXo8yybJ0+M5IgOL 7 | 0gn5Yoq+mb5xnrynaaAGp3GW+HAYfPypZXXPihMqECHl49q/ThMcrzUyhFeX4qnn 8 | XwJHP41+pTE+X2nKATtHGJrwlClxD6IOXkdKdh1dYOTS1t3KCF06TWZkS6TKow04 9 | vrwtMkGb2XLwDcOl7RcbNf+0yqHEv8Y9At+VKKva0fMpeE75bwJMFLYW+be2KXKG 10 | uSrwGDP8nIy2bIn6HmFLp9l45rllsNv7jDNSiv8M12pUgCK8RfnrGGKibcP9TdSU 11 | I1p6fXbEdvCIDjZ5WsBxHFmxFx0rCph5rYI4KX7viNqZQ+1UNTANkX6qem1d2sCN 12 | 10M6xv/UJBB6fhUaiQFEj0Wmh3LcBEWNZMHjziMJXQu7ifaD6pisXinaN6sCAwEA 13 | AQKCAgBbssl3fIV1xrnqnNysPicYSLPcxjWNYZ0cJ9Qn5qCHVirzNwuX/Lp4sYQ5 14 | O6oAYnJFZvNtYAEIFOu37Na2gE3ndgbCVPjaYASILXy0W1LPaefSyvCz0xyWowb1 15 | c2MiC9K6h+bxCegLsPmt37GoWsa/b4wgiRE03nhPonCEFKPwcMDgMwPpUdU+00Ig 16 | IH+Yy+f7gQcw80dYAE96kXZmZQrStBTBbMK38ZXYd3XwP35BVYlHvuTy2PWRH5J2 17 | RYPR9/EUWPhrG2j1o/BuKzRlwSz4GeOr254tUUsG+nDEjkx29WvWF8z8ntZ4hf/F 18 | afVkyHjQMuYDn8EY1PghPF9hz6UsjlbM6qofIpCgBomVPEUItsStHXn4p9uhN74Z 19 | ROoz1+7PRgWjFvfw4HWj1xGhZspI2yQBSu9hhin6cFpr7oKmuZe8EYWBoJDnsb9u 20 | txsvFE1JIcYkPf9OddvJ3dpq0/RZdTk1pnPc0ayIpMnsLs/X2XcS8F4+sxTI7Yrd 21 | 2tBgIrBfoXaUWWwYhDAId2UGWq6f7CnGBbNpNCm99mjD5g5mJOYc2i6SYwRdaCaT 22 | sApOQe9jm4DvLIe8lTM5b0lFaBlDZoyaZVDcZwOE6pB/SZH8iWYyLCoa4H3W801I 23 | XVuUbzi10uORMUa/2EI++vjjptDUfqioz4stjp7uV2JxPjZfMQKCAQEA9gjws96i 24 | nn7hTdi28zeGpRkvCeuCaUre+Fl9ZiEC7lZJRybXBuj12Tw/fC5QeAED0WtiIdHI 25 | T9aFqKgQPs6qzmPLPYyz7ZDR3kzTyDkGPFmi4+jP5Me412OFOpsbxMRrPLaGKacy 26 | tKVVJUBlUNn4/OfzGWSzjMiKQ51FlvDSXN63mllSOi+Dspq1zgFTBY8Tv3+vnFbR 27 | RkTpf8yBF4WiO454QrHOgKHVCEGsJ8QO+EAvp5XdpF3N0IuOyX1Isx3FJW6fOSaN 28 | zeSgvzc7CLaEbZGUthZhKeVPJ2kT9ggzdGXAM+mwpjjQ6WkSqzgFUyyUbLGI6zr9 29 | BnJJqUj12dwzmQKCAQEA9VK+0PT3sInMD+cuAJL67V3FIRpSjP2xNV3fCa5nDoXv 30 | fHAcfcsEZuZHkZPWugch6fOodUfHZokIaWqxz6W5yIHDXCfJixpR6y0w2USPrIkr 31 | YsKnd5o3Ng3euItZIBtHb9Fq/u92bd9Kl22Zl/U8LKPN68RQoTkUkcY4yErZuyr8 32 | YJ7vSdoOAUsmXj2T8fwLixodZQ/IUNn3sEHHzjfM1eqVVA/SsNInzw9IbD6YfC99 33 | r69bnneVI4bXJkRt3+YzYRD6Uuer3p2XDR7p9an6pT98yN6T+PdiwzbE/cCp1SHO 34 | gDoEL+z5Pv7JqFC/GamAua0NrzsrL5l1PhTpLLaP4wKCAQAAm7EMoMZCIgooiRqa 35 | q0535gIJwj1GSVsXvhDjSb2aHTh9JoltiPHioZV+lxa31TjPMbbCYqReHieibRft 36 | L9GYRQLhmhFvyuXZ6NK1Mf7j80zFx3OMGzpHvgU8SdnduKaTNdhLDhDABZCaUPhv 37 | JLrK1T/ylh8jf/Nni2H6p4m9lQdbKFFdt3RZ4qwEYFh2MQ5ZcNVcS5Kk29K/8B95 38 | kWj/QLDnaU0bYEsIhDEKwB9RWcdEYMh1eY3isBWgReECIFa2avmVyJSdJ62GC3rn 39 | 4JufMbbuZEvFML3rWQUlnIuUBBRfjHpVTtqlWQ8kwTSyKfAL/Lxb6H9rnqiz9Gw3 40 | J6YxAoIBABknoulKNRulBYeb7NuiNpigRNZgHJbYbJNMTNJxT5/tm+DmngVIC+uB 41 | MVV8E0h/8rpKgbuE4K1i37nvdswT8jjdFWsdqUzaJgw3VgrxPMo0Rn6Z9xIMfhzM 42 | z0mdAEaKhYixsIbzhvE9NCjS1C0AuGrbYQIL3zio4bMQ4EGpayoF/lrp8R9hfI4l 43 | ZonDRqhJb+WTh/AU7jVcJEmudQ5wKlDE/QrhlHkNE4fnyUVQJdKWDA03re1R1bkO 44 | 2oDSA3Ix6JLLat+VYcox3os54EOQamMHX4Q5TnL46ueZdhmO59sy3DnJuSQw6F3N 45 | QmA/51h6SPjBBeYx6vu2gI8dFcu/7IECggEAQt9i2bYQrdk0U0Ma/SqzYfwmn7x6 46 | swKlMM1is2tVcaYmahJFdSef4trqJ5gopQEyUu5mD8i1YonGr+zqS4Lg3rCw9Ap5 47 | DL70HwtyccH2ilPs2z6BU2O0uHn8OdbcOg28uPk7Wx5FsUW08TknfvLXorsrfgmm 48 | i2PIZqe1ffO835Z5rMonqoUKMzVHxNFiHkgrcMJlgo9SZhiR5EqFfWJYJn9tMB71 49 | llGPifrPWQ8NsUu0KAowwRyWNkwbR95R1SGMr8JzsYxSv+zyHHFLaWVqNTvyVcS0 50 | 2OBKc9mcn6wGqunbdK6cJtARIh0qa2DRnCEUTAwktjDj5PnpM9mtIGmH1A== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /tests/nonce_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2024 Flavio Garcia 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from . import get_absolute_path 16 | from automatoes.protocol import AcmeV2Pesant, AcmeRequestsTransport 17 | from tornado import testing 18 | 19 | 20 | class NonceTestCase(testing.AsyncTestCase): 21 | """ Test letsencrypt nonce 22 | """ 23 | 24 | @testing.gen_test 25 | async def test_auth(self): 26 | transport = AcmeRequestsTransport("https://localhost:14000") 27 | protocol = AcmeV2Pesant( 28 | transport, 29 | directory="dir", 30 | verify=get_absolute_path("certs/candango.minica.pem") 31 | ) 32 | self.assertIsNotNone(protocol.new_nonce()) 33 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2019-2024 Flavio Garcia 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | from tests import crypto_test 19 | 20 | 21 | def suite(): 22 | testLoader = unittest.TestLoader() 23 | alltests = unittest.TestSuite() 24 | alltests.addTests(testLoader.loadTestsFromModule(crypto_test)) 25 | return alltests 26 | 27 | 28 | if __name__ == "__main__": 29 | runner = unittest.TextTestRunner(verbosity=3) 30 | result = runner.run(suite()) 31 | if not result.wasSuccessful(): 32 | exit(2) 33 | --------------------------------------------------------------------------------