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.
- certapi-1.1.0/PKG-INFO +90 -0
- certapi-1.1.0/README.md +66 -0
- {certapi-1.0.4 → certapi-1.1.0}/setup.py +6 -1
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/InmemoryChallengeSolver.py +6 -0
- certapi-1.1.0/src/certapi/cli.py +242 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/client/cert_manager_client.py +2 -2
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/issuers/AcmeCertIssuer.py +1 -1
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/server/cert_api.py +2 -2
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/server/key_api.py +3 -3
- certapi-1.1.0/src/certapi.egg-info/PKG-INFO +90 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi.egg-info/SOURCES.txt +3 -0
- certapi-1.1.0/src/certapi.egg-info/entry_points.txt +2 -0
- certapi-1.1.0/tests/test_cli.py +385 -0
- certapi-1.0.4/PKG-INFO +0 -117
- certapi-1.0.4/README.md +0 -93
- certapi-1.0.4/src/certapi.egg-info/PKG-INFO +0 -117
- {certapi-1.0.4 → certapi-1.1.0}/MANIFEST.in +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/pyproject.toml +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/setup.cfg +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/Acme.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/AcmeError.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/Challenge.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/Order.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/acme/http.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/ChallengeSolver.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/FileSystemChallengeSolver.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/cloudflare/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_challenge_solver.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/cloudflare/cloudflare_client.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/digitalocean/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_challenge_solver.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/challenge_solver/dns/digitalocean/digitalocean_client.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/client/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/crypto/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/crypto/crypto.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/crypto/crypto_classes.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/errors.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/http/HttpClientBase.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/http/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/http/types.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/issuers/SelfCertIssuer.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/issuers/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/issuers/abstract_certissuer.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/FileSystemKeyStore.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/KeyStore.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/PostgresqlKeyStore.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/RemoteKeyStore.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/SqliteKeyStore.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/keystore/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/manager/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/manager/acme_cert_manager.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/server/__init__.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/server/api.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/util.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi/utils.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi.egg-info/dependency_links.txt +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi.egg-info/requires.txt +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/src/certapi.egg-info/top_level.txt +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/tests/test_acme_error_handling.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/tests/test_cert_issuer_generic.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/tests/test_certs_with_key_types.py +0 -0
- {certapi-1.0.4 → certapi-1.1.0}/tests/test_http_error_handling.py +0 -0
- {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
|
+
[](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
|
|
33
|
+
[](https://codecov.io/github/mesudip/certapi)
|
|
34
|
+
[](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
|
+
```
|
certapi-1.1.0/README.md
ADDED
|
@@ -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
|
+
[](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
|
|
9
|
+
[](https://codecov.io/github/mesudip/certapi)
|
|
10
|
+
[](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
|
|
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
|
|
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("/
|
|
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("
|
|
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("/
|
|
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("
|
|
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("/
|
|
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
|
+
[](https://github.com/mesudip/certapi/actions/workflows/tests.yml)
|
|
33
|
+
[](https://codecov.io/github/mesudip/certapi)
|
|
34
|
+
[](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
|