certapi 1.0.4__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. certapi-1.1.0/PKG-INFO +90 -0
  2. certapi-1.1.0/README.md +66 -0
  3. {certapi-1.0.4 → certapi-1.1.0}/setup.py +6 -1
  4. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +6 -0
  5. certapi-1.1.0/src/certapi/cli.py +242 -0
  6. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/client/cert_manager_client.py +2 -2
  7. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/issuers/AcmeCertIssuer.py +1 -1
  8. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/server/cert_api.py +2 -2
  9. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/server/key_api.py +3 -3
  10. certapi-1.1.0/src/certapi.egg-info/PKG-INFO +90 -0
  11. {certapi-1.0.4 → certapi-1.1.0}/src/certapi.egg-info/SOURCES.txt +3 -0
  12. certapi-1.1.0/src/certapi.egg-info/entry_points.txt +2 -0
  13. certapi-1.1.0/tests/test_cli.py +385 -0
  14. certapi-1.0.4/PKG-INFO +0 -117
  15. certapi-1.0.4/README.md +0 -93
  16. certapi-1.0.4/src/certapi.egg-info/PKG-INFO +0 -117
  17. {certapi-1.0.4 → certapi-1.1.0}/MANIFEST.in +0 -0
  18. {certapi-1.0.4 → certapi-1.1.0}/pyproject.toml +0 -0
  19. {certapi-1.0.4 → certapi-1.1.0}/setup.cfg +0 -0
  20. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/__init__.py +0 -0
  21. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/Acme.py +0 -0
  22. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/AcmeError.py +0 -0
  23. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/Challenge.py +0 -0
  24. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/Order.py +0 -0
  25. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/__init__.py +0 -0
  26. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/http.py +0 -0
  27. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
  28. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
  29. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/__init__.py +0 -0
  30. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/__init__.py +0 -0
  31. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
  32. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
  33. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
  34. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
  35. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
  36. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
  37. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/client/__init__.py +0 -0
  38. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/crypto/__init__.py +0 -0
  39. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/crypto/crypto.py +0 -0
  40. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/crypto/crypto_classes.py +0 -0
  41. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/errors.py +0 -0
  42. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/http/HttpClientBase.py +0 -0
  43. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/http/__init__.py +0 -0
  44. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/http/types.py +0 -0
  45. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/issuers/SelfCertIssuer.py +0 -0
  46. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/issuers/__init__.py +0 -0
  47. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/issuers/abstract_certissuer.py +0 -0
  48. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
  49. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/KeyStore.py +0 -0
  50. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
  51. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/RemoteKeyStore.py +0 -0
  52. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/SqliteKeyStore.py +0 -0
  53. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/__init__.py +0 -0
  54. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/manager/__init__.py +0 -0
  55. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/manager/acme_cert_manager.py +0 -0
  56. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/server/__init__.py +0 -0
  57. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/server/api.py +0 -0
  58. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/util.py +0 -0
  59. {certapi-1.0.4 → certapi-1.1.0}/src/certapi/utils.py +0 -0
  60. {certapi-1.0.4 → certapi-1.1.0}/src/certapi.egg-info/dependency_links.txt +0 -0
  61. {certapi-1.0.4 → certapi-1.1.0}/src/certapi.egg-info/requires.txt +0 -0
  62. {certapi-1.0.4 → certapi-1.1.0}/src/certapi.egg-info/top_level.txt +0 -0
  63. {certapi-1.0.4 → certapi-1.1.0}/tests/test_acme_error_handling.py +0 -0
  64. {certapi-1.0.4 → certapi-1.1.0}/tests/test_cert_issuer_generic.py +0 -0
  65. {certapi-1.0.4 → certapi-1.1.0}/tests/test_certs_with_key_types.py +0 -0
  66. {certapi-1.0.4 → certapi-1.1.0}/tests/test_http_error_handling.py +0 -0
  67. {certapi-1.0.4 → certapi-1.1.0}/tests/test_keystores.py +0 -0
certapi-1.1.0/PKG-INFO ADDED
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: certapi
3
+ Version: 1.1.0
4
+ Summary: Python Package for managing keys, request SSL certificates from ACME.
5
+ Home-page: https://github.com/mesudip/certapi
6
+ Author: Sudip Bhattarai
7
+ Author-email: sudipbhattarai100@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: cryptography
14
+ Requires-Dist: requests
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: requires-dist
22
+ Dynamic: requires-python
23
+ Dynamic: summary
24
+
25
+ CertApi
26
+ =============================
27
+
28
+ Certapi talks with DNS provider and ACME to issue SSL certificates and save it to a keystore.
29
+
30
+ CertApi is a base library for building other tools, or to integrate Certificate creation feature in your app. CertAPI also provides HTTP api server and can be deployed using Docker
31
+
32
+ [![Build Status](https://github.com/mesudip/certapi/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
33
+ [![codecov](https://codecov.io/github/mesudip/certapi/graph/badge.svg?token=NYTNCH29IT)](https://codecov.io/github/mesudip/certapi)
34
+ [![PyPI version](https://img.shields.io/pypi/v/certapi.svg)](https://pypi.org/project/certapi/)
35
+
36
+ ## Why another library?
37
+
38
+ I designed this library so that it can be imported and plugged in to other python projects. Goal is not to provide CLIs or quick working demo, but to be versatile for any use case.
39
+
40
+ - Pluggable keystores for keys and certificates
41
+ - Pluggable Challenge solvers for DNS and Http challenge solving
42
+ - High-level manager with renewal checks and multi-solver support
43
+ - Same interface for working locally, or requesting certificate from certapi server.
44
+
45
+ See the developer guide in [Developer.md](Developer.md) for library usage and workflows.
46
+
47
+
48
+ ## Installation
49
+
50
+ You can install CertApi using pip
51
+
52
+ ```bash
53
+ pip install certapi
54
+ ```
55
+
56
+ ## CLI
57
+
58
+ CertApi also ships with a CLI for quick verification and certificate issuance.
59
+
60
+ ```bash
61
+ ## Certapi's dependencies are already included in the python installation. This doesn't affect the system.
62
+ sudo python3 -m pip install certapi --break-system-packages
63
+ ```
64
+
65
+ ### 1. With HTTP Challenge
66
+
67
+ HTTP challenge requires you to have seup the DNS correctly. The ACME server will verify your domain by making an HTTP request.
68
+
69
+ ```bash
70
+ # Verify environment and HTTP routing
71
+ sudo certapi verify example.com www.example.com
72
+
73
+ # Obtain a certificate (requires root for HTTP-01)
74
+ sudo certapi obtain example.com www.example.com
75
+ ```
76
+
77
+ ### 2. With DNS Provider Key
78
+
79
+ Using DNS-01 challenge with Cloudflare doesn't require the DNS to be setup. Set your Cloudflare API key as an environment variable.
80
+
81
+ ```bash
82
+ # Set Cloudflare API key or token
83
+ export CLOUDFLARE_API_KEY=... # or CLOUDFLARE_API_TOKEN
84
+
85
+ # Verify DNS configuration
86
+ sudo certapi verify example.com
87
+
88
+ # Obtain a certificate using DNS-01
89
+ sudo certapi obtain example.com www.example.com
90
+ ```
@@ -0,0 +1,66 @@
1
+ CertApi
2
+ =============================
3
+
4
+ Certapi talks with DNS provider and ACME to issue SSL certificates and save it to a keystore.
5
+
6
+ CertApi is a base library for building other tools, or to integrate Certificate creation feature in your app. CertAPI also provides HTTP api server and can be deployed using Docker
7
+
8
+ [![Build Status](https://github.com/mesudip/certapi/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
9
+ [![codecov](https://codecov.io/github/mesudip/certapi/graph/badge.svg?token=NYTNCH29IT)](https://codecov.io/github/mesudip/certapi)
10
+ [![PyPI version](https://img.shields.io/pypi/v/certapi.svg)](https://pypi.org/project/certapi/)
11
+
12
+ ## Why another library?
13
+
14
+ I designed this library so that it can be imported and plugged in to other python projects. Goal is not to provide CLIs or quick working demo, but to be versatile for any use case.
15
+
16
+ - Pluggable keystores for keys and certificates
17
+ - Pluggable Challenge solvers for DNS and Http challenge solving
18
+ - High-level manager with renewal checks and multi-solver support
19
+ - Same interface for working locally, or requesting certificate from certapi server.
20
+
21
+ See the developer guide in [Developer.md](Developer.md) for library usage and workflows.
22
+
23
+
24
+ ## Installation
25
+
26
+ You can install CertApi using pip
27
+
28
+ ```bash
29
+ pip install certapi
30
+ ```
31
+
32
+ ## CLI
33
+
34
+ CertApi also ships with a CLI for quick verification and certificate issuance.
35
+
36
+ ```bash
37
+ ## Certapi's dependencies are already included in the python installation. This doesn't affect the system.
38
+ sudo python3 -m pip install certapi --break-system-packages
39
+ ```
40
+
41
+ ### 1. With HTTP Challenge
42
+
43
+ HTTP challenge requires you to have seup the DNS correctly. The ACME server will verify your domain by making an HTTP request.
44
+
45
+ ```bash
46
+ # Verify environment and HTTP routing
47
+ sudo certapi verify example.com www.example.com
48
+
49
+ # Obtain a certificate (requires root for HTTP-01)
50
+ sudo certapi obtain example.com www.example.com
51
+ ```
52
+
53
+ ### 2. With DNS Provider Key
54
+
55
+ Using DNS-01 challenge with Cloudflare doesn't require the DNS to be setup. Set your Cloudflare API key as an environment variable.
56
+
57
+ ```bash
58
+ # Set Cloudflare API key or token
59
+ export CLOUDFLARE_API_KEY=... # or CLOUDFLARE_API_TOKEN
60
+
61
+ # Verify DNS configuration
62
+ sudo certapi verify example.com
63
+
64
+ # Obtain a certificate using DNS-01
65
+ sudo certapi obtain example.com www.example.com
66
+ ```
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="certapi",
5
- version="1.0.4",
5
+ version="1.1.0",
6
6
  packages=find_packages(where="src"),
7
7
  package_dir={"": "src"},
8
8
  install_requires=[
@@ -22,4 +22,9 @@ setup(
22
22
  "Operating System :: OS Independent",
23
23
  ],
24
24
  python_requires=">=3.6",
25
+ entry_points={
26
+ "console_scripts": [
27
+ "certapi=certapi.cli:main",
28
+ ],
29
+ },
25
30
  )
@@ -13,6 +13,9 @@ class InMemoryChallengeSolver(ChallengeSolver):
13
13
  def supported_challenge_type(self) -> Literal["http-01"]:
14
14
  return "http-01"
15
15
 
16
+ def supports_domain(self, domain: str) -> bool:
17
+ return "*" not in domain
18
+
16
19
  def save_challenge(self, key: str, value: str, domain: str = None):
17
20
  self.challenges[key] = value
18
21
 
@@ -23,6 +26,9 @@ class InMemoryChallengeSolver(ChallengeSolver):
23
26
  if key in self.challenges:
24
27
  del self.challenges[key]
25
28
 
29
+ def cleanup_old_challenges(self):
30
+ self.challenges.clear()
31
+
26
32
  def __iter__(self):
27
33
  return iter(self.challenges)
28
34
 
@@ -0,0 +1,242 @@
1
+ import argparse
2
+ import os
3
+ import secrets
4
+ import subprocess
5
+ import sys
6
+ import threading
7
+ from http.server import BaseHTTPRequestHandler
8
+ from socketserver import TCPServer
9
+ from typing import List, Optional, Tuple
10
+ from urllib.parse import unquote
11
+
12
+ import requests
13
+
14
+ from certapi import (
15
+ AcmeCertIssuer,
16
+ CertApiException,
17
+ CloudflareChallengeSolver,
18
+ FileSystemKeyStore,
19
+ InMemoryChallengeSolver,
20
+ )
21
+ from certapi.crypto import certs_from_pem
22
+
23
+
24
+ def is_root() -> bool:
25
+ """Check if running with elevated privileges (cross-platform)."""
26
+ try:
27
+ if os.name == "nt": # Windows
28
+ import ctypes
29
+
30
+ return ctypes.windll.shell32.IsUserAnAdmin() != 0
31
+ else: # Unix-like (Linux, macOS)
32
+ return os.geteuid() == 0
33
+ except (AttributeError, ImportError, OSError):
34
+ return False
35
+
36
+
37
+ def find_process_on_port(port: int) -> List[str]:
38
+ """Find processes using a specific port (cross-platform)."""
39
+ try:
40
+ if os.name == "nt": # Windows
41
+ result = subprocess.check_output(["netstat", "-ano"], stderr=subprocess.DEVNULL).decode().strip()
42
+ pids = []
43
+ for line in result.split("\n"):
44
+ if f":{port}" in line and "LISTENING" in line:
45
+ parts = line.split()
46
+ if parts:
47
+ pids.append(parts[-1])
48
+ return pids
49
+ else: # Unix-like (Linux, macOS)
50
+ result = (
51
+ subprocess.check_output(["lsof", "-i", f":{port}", "-t"], stderr=subprocess.DEVNULL).decode().strip()
52
+ )
53
+ return result.split("\n") if result else []
54
+ except (subprocess.CalledProcessError, FileNotFoundError):
55
+ return []
56
+
57
+
58
+ def _start_http_challenge_server(
59
+ challenge_solver: InMemoryChallengeSolver, port: int = 80
60
+ ) -> Tuple[TCPServer, threading.Thread]:
61
+ class AcmeChallengeHandler(BaseHTTPRequestHandler):
62
+ def do_GET(self):
63
+ prefix = "/.well-known/acme-challenge/"
64
+ if not self.path.startswith(prefix):
65
+ self.send_error(404)
66
+ return
67
+ token = unquote(self.path[len(prefix) :])
68
+ if not token or "/" in token or "\\" in token:
69
+ self.send_error(400)
70
+ return
71
+ content = challenge_solver.get_challenge(token)
72
+ if not content:
73
+ self.send_error(404)
74
+ return
75
+ if isinstance(content, str):
76
+ content = content.encode("utf-8")
77
+ self.send_response(200)
78
+ self.send_header("Content-Type", "text/plain")
79
+ self.send_header("Content-Length", str(len(content)))
80
+ self.end_headers()
81
+ self.wfile.write(content)
82
+
83
+ def log_message(self, format, *args):
84
+ return
85
+
86
+ class ReusableTCPServer(TCPServer):
87
+ allow_reuse_address = True
88
+
89
+ server = ReusableTCPServer(("", port), AcmeChallengeHandler)
90
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
91
+ thread.start()
92
+ return server, thread
93
+
94
+
95
+ def _resolve_cloudflare_api_key() -> Optional[str]:
96
+ return os.environ.get("CLOUDFLARE_API_KEY") or os.environ.get("CLOUDFLARE_API_TOKEN")
97
+
98
+
99
+ def _ensure_port_80_available() -> None:
100
+ if not is_root():
101
+ print("Must be run as root to bind to port 80 for HTTP challenge.")
102
+ sys.exit(1)
103
+
104
+ while True:
105
+ pids = find_process_on_port(80)
106
+ if not pids:
107
+ return
108
+ print(f"Process(es) running on port 80: {', '.join(pids)}")
109
+ response = input("Stop the process(es) above, then press Enter to retry (or type 'q' to quit): ")
110
+ if response.strip().lower() == "q":
111
+ sys.exit(1)
112
+
113
+
114
+ def obtain_certificate(domains: List[str], api_key: Optional[str] = None):
115
+ challenge_solver = None
116
+ server = None
117
+
118
+ if api_key:
119
+ challenge_solver = CloudflareChallengeSolver(api_key=api_key)
120
+ print("Using Cloudflare DNS challenge.")
121
+ else:
122
+ _ensure_port_80_available()
123
+ challenge_solver = InMemoryChallengeSolver()
124
+ print("Starting HTTP challenge server on port 80...")
125
+ server, _ = _start_http_challenge_server(challenge_solver, port=80)
126
+
127
+ keystore_path = "/etc/ssl"
128
+ key_store = FileSystemKeyStore(keystore_path)
129
+ cert_issuer = AcmeCertIssuer.with_keystore(
130
+ key_store,
131
+ challenge_solver,
132
+ account_key_name="acme_account",
133
+ )
134
+ cert_issuer.setup()
135
+ try:
136
+ key, cert = cert_issuer.generate_key_and_cert_for_domains(domains, key_type="rsa")
137
+ key_name = domains[0]
138
+ key_id = key_store.save_key(key, key_name)
139
+ key_store.save_cert(key_id, cert, domains, name=key_name)
140
+
141
+ cert_chain = certs_from_pem(cert.encode("utf-8"))
142
+ leaf_cert = cert_chain[0] if cert_chain else None
143
+ expiry = leaf_cert.not_valid_after.isoformat() if leaf_cert else "unknown"
144
+
145
+ key_path = os.path.join(key_store.keys_dir, f"{key_name}.key")
146
+ cert_path = os.path.join(key_store.certs_dir, f"{key_name}.crt")
147
+ print(f"\n Certificate expires at: {expiry}")
148
+ print(f" Key path: {key_path}")
149
+ print(f" Cert path: {cert_path}")
150
+ except CertApiException as e:
151
+ print("An error occurred:")
152
+ print(e.json_obj())
153
+ finally:
154
+ if server is not None:
155
+ server.shutdown()
156
+ server.server_close()
157
+
158
+
159
+ def verify_environment(domains: List[str], api_key: Optional[str] = None) -> None:
160
+ if api_key:
161
+ print("[verify] Cloudflare API key detected; DNS-01 challenge available")
162
+ if domains:
163
+ solver = CloudflareChallengeSolver(api_key=api_key)
164
+ unsupported = [domain for domain in domains if not solver.supports_domain(domain)]
165
+ if unsupported:
166
+ print("[verify] Warning: Cloudflare account does not appear to manage:")
167
+ for domain in unsupported:
168
+ print(f"[verify] - {domain}")
169
+ else:
170
+ print("[verify] Cloudflare account appears to manage the provided domain(s)")
171
+ else:
172
+ print("[verify] HTTP-01 challenge will be used")
173
+ if not domains:
174
+ print("[verify] No domains provided; skipping HTTP routing check")
175
+ return
176
+ if is_root():
177
+ pids = find_process_on_port(80)
178
+ if pids:
179
+ print(f"[verify] Warning: port 80 is in use by process(es): {', '.join(pids)}")
180
+ else:
181
+ print("[verify] Port 80 is available")
182
+ else:
183
+ print("[verify] Warning: not running as root; port 80 binding will fail")
184
+ return
185
+
186
+ _ensure_port_80_available()
187
+ challenge_solver = InMemoryChallengeSolver()
188
+ print("[verify] Starting HTTP challenge server on port 80...")
189
+ server, _ = _start_http_challenge_server(challenge_solver, port=80)
190
+ ok = 0
191
+ failed = 0
192
+ try:
193
+ for domain in domains:
194
+ token = secrets.token_urlsafe(24)
195
+ value = secrets.token_urlsafe(32)
196
+ challenge_solver.save_challenge(token, value, domain)
197
+ url = f"http://{domain}/.well-known/acme-challenge/{token}"
198
+ print(f"[verify] Checking HTTP routing for {domain}...")
199
+ try:
200
+ response = requests.get(url, allow_redirects=False, timeout=5)
201
+ if response.status_code == 200 and response.text.strip() == value:
202
+ ok += 1
203
+ print(f"[verify] OK: {domain} routes to this server")
204
+ else:
205
+ failed += 1
206
+ print(
207
+ f"[verify] FAILED: {domain} returned status {response.status_code} "
208
+ f"with body '{response.text.strip()}'"
209
+ )
210
+ except requests.RequestException as exc:
211
+ failed += 1
212
+ print(f"[verify] FAILED: {domain} request error: {exc}")
213
+ finally:
214
+ challenge_solver.delete_challenge(token, domain)
215
+ finally:
216
+ server.shutdown()
217
+ server.server_close()
218
+ print(f"\n Summary: {ok} OK, {failed} FAILED")
219
+
220
+
221
+ def main():
222
+ parser = argparse.ArgumentParser(prog="certapi")
223
+ subparsers = parser.add_subparsers(dest="command")
224
+
225
+ verify_parser = subparsers.add_parser("verify", help="Verify certapi installation and environment.")
226
+ verify_parser.add_argument("domains", nargs="*", help="Optional domain(s) to verify.")
227
+
228
+ obtain_parser = subparsers.add_parser("obtain", help="Obtain certificate for domains.")
229
+ obtain_parser.add_argument("domains", nargs="+", help="Domain(s) to obtain certificate for.")
230
+
231
+ args = parser.parse_args()
232
+
233
+ if args.command == "verify":
234
+ api_key = _resolve_cloudflare_api_key()
235
+ verify_environment(args.domains, api_key=api_key)
236
+ sys.exit(0)
237
+ elif args.command == "obtain":
238
+ api_key = _resolve_cloudflare_api_key()
239
+ obtain_certificate(args.domains, api_key=api_key)
240
+ else:
241
+ parser.print_help()
242
+ sys.exit(1)
@@ -69,7 +69,7 @@ class CertManagerClient:
69
69
  params["renew_threshold_days"] = renew_threshold_days
70
70
  params["skip_failing"] = skip_failing
71
71
 
72
- data = self._get("/obtain", params=params)
72
+ data = self._get("/api/obtain", params=params)
73
73
  res = CertificateResponse.from_json(data)
74
74
 
75
75
  if self.key_store:
@@ -93,7 +93,7 @@ class CertManagerClient:
93
93
  csr_pem = csr
94
94
 
95
95
  response = requests.post(
96
- f"{self.base_url}/sign_csr", data=csr_pem, headers={"Content-Type": "application/x-pem-file"}
96
+ f"{self.base_url}/api/sign_csr", data=csr_pem, headers={"Content-Type": "application/x-pem-file"}
97
97
  )
98
98
  response.raise_for_status()
99
99
  return response.text
@@ -25,7 +25,7 @@ class AcmeCertIssuer(CertIssuer):
25
25
  def with_keystore(
26
26
  key_store: "KeyStore",
27
27
  challenge_solver: ChallengeSolver,
28
- account_key_name: str = "acme_account.key",
28
+ account_key_name: str = "acme_account",
29
29
  acme_url: str = None,
30
30
  ) -> "AcmeCertIssuer":
31
31
  account_key, _ = key_store._get_or_generate_key(account_key_name, "rsa")
@@ -34,7 +34,7 @@ def create_cert_resources(cert_ns, key_store):
34
34
  cert_list_parser = reqparse.RequestParser()
35
35
  cert_list_parser.add_argument("domain", type=str, help="Filter certificates by domain")
36
36
 
37
- @cert_ns.route("/certs")
37
+ @cert_ns.route("/")
38
38
  class CertList(Resource):
39
39
  @cert_ns.doc("list_certs")
40
40
  @cert_ns.expect(cert_list_parser)
@@ -94,7 +94,7 @@ def create_cert_resources(cert_ns, key_store):
94
94
  except Exception as e:
95
95
  cert_ns.abort(500, f"Error saving certificate: {e}")
96
96
 
97
- @cert_ns.route("/certs/<string:cert_id>")
97
+ @cert_ns.route("/<string:cert_id>")
98
98
  @cert_ns.param("cert_id", "The certificate identifier")
99
99
  class CertById(Resource):
100
100
  @cert_ns.doc("get_cert_by_id")
@@ -23,7 +23,7 @@ def create_key_resources(key_ns, key_store):
23
23
  },
24
24
  )
25
25
 
26
- @key_ns.route("/keys")
26
+ @key_ns.route("/")
27
27
  class KeyList(Resource):
28
28
  @key_ns.doc("list_keys")
29
29
  @key_ns.marshal_list_with(key_model)
@@ -49,7 +49,7 @@ def create_key_resources(key_ns, key_store):
49
49
  except Exception as e:
50
50
  key_ns.abort(500, f"Error saving key: {e}")
51
51
 
52
- @key_ns.route("/keys/<string:key_id>")
52
+ @key_ns.route("/<string:key_id>")
53
53
  @key_ns.param("key_id", "The key identifier")
54
54
  class KeyById(Resource):
55
55
  @key_ns.doc("get_key_by_id")
@@ -62,7 +62,7 @@ def create_key_resources(key_ns, key_store):
62
62
  else:
63
63
  key_ns.abort(404, message="Key not found")
64
64
 
65
- @key_ns.route("/keys/name/<string:name>")
65
+ @key_ns.route("/name/<string:name>")
66
66
  @key_ns.param("name", "The key name")
67
67
  class KeyByName(Resource):
68
68
  @key_ns.doc("get_key_by_name")
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: certapi
3
+ Version: 1.1.0
4
+ Summary: Python Package for managing keys, request SSL certificates from ACME.
5
+ Home-page: https://github.com/mesudip/certapi
6
+ Author: Sudip Bhattarai
7
+ Author-email: sudipbhattarai100@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: cryptography
14
+ Requires-Dist: requests
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: requires-dist
22
+ Dynamic: requires-python
23
+ Dynamic: summary
24
+
25
+ CertApi
26
+ =============================
27
+
28
+ Certapi talks with DNS provider and ACME to issue SSL certificates and save it to a keystore.
29
+
30
+ CertApi is a base library for building other tools, or to integrate Certificate creation feature in your app. CertAPI also provides HTTP api server and can be deployed using Docker
31
+
32
+ [![Build Status](https://github.com/mesudip/certapi/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
33
+ [![codecov](https://codecov.io/github/mesudip/certapi/graph/badge.svg?token=NYTNCH29IT)](https://codecov.io/github/mesudip/certapi)
34
+ [![PyPI version](https://img.shields.io/pypi/v/certapi.svg)](https://pypi.org/project/certapi/)
35
+
36
+ ## Why another library?
37
+
38
+ I designed this library so that it can be imported and plugged in to other python projects. Goal is not to provide CLIs or quick working demo, but to be versatile for any use case.
39
+
40
+ - Pluggable keystores for keys and certificates
41
+ - Pluggable Challenge solvers for DNS and Http challenge solving
42
+ - High-level manager with renewal checks and multi-solver support
43
+ - Same interface for working locally, or requesting certificate from certapi server.
44
+
45
+ See the developer guide in [Developer.md](Developer.md) for library usage and workflows.
46
+
47
+
48
+ ## Installation
49
+
50
+ You can install CertApi using pip
51
+
52
+ ```bash
53
+ pip install certapi
54
+ ```
55
+
56
+ ## CLI
57
+
58
+ CertApi also ships with a CLI for quick verification and certificate issuance.
59
+
60
+ ```bash
61
+ ## Certapi's dependencies are already included in the python installation. This doesn't affect the system.
62
+ sudo python3 -m pip install certapi --break-system-packages
63
+ ```
64
+
65
+ ### 1. With HTTP Challenge
66
+
67
+ HTTP challenge requires you to have seup the DNS correctly. The ACME server will verify your domain by making an HTTP request.
68
+
69
+ ```bash
70
+ # Verify environment and HTTP routing
71
+ sudo certapi verify example.com www.example.com
72
+
73
+ # Obtain a certificate (requires root for HTTP-01)
74
+ sudo certapi obtain example.com www.example.com
75
+ ```
76
+
77
+ ### 2. With DNS Provider Key
78
+
79
+ Using DNS-01 challenge with Cloudflare doesn't require the DNS to be setup. Set your Cloudflare API key as an environment variable.
80
+
81
+ ```bash
82
+ # Set Cloudflare API key or token
83
+ export CLOUDFLARE_API_KEY=... # or CLOUDFLARE_API_TOKEN
84
+
85
+ # Verify DNS configuration
86
+ sudo certapi verify example.com
87
+
88
+ # Obtain a certificate using DNS-01
89
+ sudo certapi obtain example.com www.example.com
90
+ ```
@@ -3,12 +3,14 @@ README.md
3
3
  pyproject.toml
4
4
  setup.py
5
5
  src/certapi/__init__.py
6
+ src/certapi/cli.py
6
7
  src/certapi/errors.py
7
8
  src/certapi/util.py
8
9
  src/certapi/utils.py
9
10
  src/certapi.egg-info/PKG-INFO
10
11
  src/certapi.egg-info/SOURCES.txt
11
12
  src/certapi.egg-info/dependency_links.txt
13
+ src/certapi.egg-info/entry_points.txt
12
14
  src/certapi.egg-info/requires.txt
13
15
  src/certapi.egg-info/top_level.txt
14
16
  src/certapi/acme/Acme.py
@@ -55,5 +57,6 @@ src/certapi/server/key_api.py
55
57
  tests/test_acme_error_handling.py
56
58
  tests/test_cert_issuer_generic.py
57
59
  tests/test_certs_with_key_types.py
60
+ tests/test_cli.py
58
61
  tests/test_http_error_handling.py
59
62
  tests/test_keystores.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ certapi = certapi.cli:main