certmate-cli 0.1.4__tar.gz → 0.1.6__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.
- {certmate_cli-0.1.4 → certmate_cli-0.1.6}/.gitignore +7 -0
- {certmate_cli-0.1.4 → certmate_cli-0.1.6}/PKG-INFO +11 -4
- {certmate_cli-0.1.4 → certmate_cli-0.1.6}/README.md +8 -1
- {certmate_cli-0.1.4 → certmate_cli-0.1.6}/certmate_cli/__init__.py +1 -1
- {certmate_cli-0.1.4 → certmate_cli-0.1.6}/certmate_cli/main.py +62 -12
- {certmate_cli-0.1.4 → certmate_cli-0.1.6}/pyproject.toml +7 -4
|
@@ -79,6 +79,7 @@ htmlcov/
|
|
|
79
79
|
.pytest_cache/
|
|
80
80
|
.coverage
|
|
81
81
|
coverage.xml
|
|
82
|
+
coverage.json
|
|
82
83
|
test_results.json
|
|
83
84
|
|
|
84
85
|
# Local dev / debug scripts (kept on developer machines only)
|
|
@@ -114,3 +115,9 @@ theme-baseline/
|
|
|
114
115
|
UI_DRACONIAN_ANALYSIS.md
|
|
115
116
|
UI_UX_Opinioni_CertMate.pdf
|
|
116
117
|
UI_REDESIGN_PLAN.md
|
|
118
|
+
|
|
119
|
+
# Local nginx reverse-proxy config: copy nginx.conf.example to nginx.conf and
|
|
120
|
+
# fill in your domain. The copy is yours; the example is tracked (#576).
|
|
121
|
+
nginx.conf
|
|
122
|
+
tls-erros.md
|
|
123
|
+
.mypy_cache/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: certmate-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: CertMate command-line interface — the SSL certificate lifecycle from your terminal
|
|
5
5
|
Project-URL: Homepage, https://github.com/fabriziosalmi/certmate
|
|
6
6
|
Project-URL: Source, https://github.com/fabriziosalmi/certmate/tree/main/clients/certmate-cli
|
|
@@ -23,7 +23,7 @@ Classifier: Topic :: Security
|
|
|
23
23
|
Classifier: Topic :: System :: Systems Administration
|
|
24
24
|
Classifier: Topic :: Utilities
|
|
25
25
|
Requires-Python: >=3.9
|
|
26
|
-
Requires-Dist: certmate-sdk>=0.1.
|
|
26
|
+
Requires-Dist: certmate-sdk>=0.1.6
|
|
27
27
|
Requires-Dist: rich>=13
|
|
28
28
|
Requires-Dist: typer>=0.9
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
@@ -34,7 +34,7 @@ The [CertMate](https://github.com/fabriziosalmi/certmate) SSL certificate
|
|
|
34
34
|
lifecycle from your terminal — built on `certmate-sdk`.
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
pip install certmate-cli
|
|
37
|
+
pip install certmate-cli # or, on macOS/Linux: brew install fabriziosalmi/certmate/certmate-cli
|
|
38
38
|
export CERTMATE_URL=http://localhost:8000
|
|
39
39
|
export CERTMATE_TOKEN=...
|
|
40
40
|
|
|
@@ -43,9 +43,16 @@ certmate cert ls
|
|
|
43
43
|
certmate cert info app.example.com
|
|
44
44
|
certmate cert renew app.example.com --force
|
|
45
45
|
certmate cert create app.example.com --dns cloudflare --dry-run
|
|
46
|
+
certmate cert create app.example.com --name internal.example.com --dns cloudflare
|
|
46
47
|
certmate audit verify
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
`--name` names a second certificate for a domain apart from it: the certificate
|
|
51
|
+
is issued for `app.example.com`, and every later command takes
|
|
52
|
+
`internal.example.com`. The server must be on API contract 2.61 or later; an
|
|
53
|
+
older one would ignore the name, so the CLI asks first and refuses, and
|
|
54
|
+
`--dry-run` says whether the server takes it.
|
|
55
|
+
|
|
49
56
|
## Pulling certificates onto a host
|
|
50
57
|
|
|
51
58
|
`cert download` fetches one file at a time, so a target server can pull
|
|
@@ -4,7 +4,7 @@ The [CertMate](https://github.com/fabriziosalmi/certmate) SSL certificate
|
|
|
4
4
|
lifecycle from your terminal — built on `certmate-sdk`.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
pip install certmate-cli
|
|
7
|
+
pip install certmate-cli # or, on macOS/Linux: brew install fabriziosalmi/certmate/certmate-cli
|
|
8
8
|
export CERTMATE_URL=http://localhost:8000
|
|
9
9
|
export CERTMATE_TOKEN=...
|
|
10
10
|
|
|
@@ -13,9 +13,16 @@ certmate cert ls
|
|
|
13
13
|
certmate cert info app.example.com
|
|
14
14
|
certmate cert renew app.example.com --force
|
|
15
15
|
certmate cert create app.example.com --dns cloudflare --dry-run
|
|
16
|
+
certmate cert create app.example.com --name internal.example.com --dns cloudflare
|
|
16
17
|
certmate audit verify
|
|
17
18
|
```
|
|
18
19
|
|
|
20
|
+
`--name` names a second certificate for a domain apart from it: the certificate
|
|
21
|
+
is issued for `app.example.com`, and every later command takes
|
|
22
|
+
`internal.example.com`. The server must be on API contract 2.61 or later; an
|
|
23
|
+
older one would ignore the name, so the CLI asks first and refuses, and
|
|
24
|
+
`--dry-run` says whether the server takes it.
|
|
25
|
+
|
|
19
26
|
## Pulling certificates onto a host
|
|
20
27
|
|
|
21
28
|
`cert download` fetches one file at a time, so a target server can pull
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""certmate-cli — the CertMate SSL lifecycle from your terminal (built on certmate-sdk)."""
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.6"
|
|
@@ -21,7 +21,7 @@ import typer
|
|
|
21
21
|
from rich.console import Console
|
|
22
22
|
from rich.table import Table
|
|
23
23
|
|
|
24
|
-
from certmate import Client,
|
|
24
|
+
from certmate import CertMateError, Client, Job, UnsupportedByServer
|
|
25
25
|
|
|
26
26
|
app = typer.Typer(no_args_is_help=True, add_completion=False,
|
|
27
27
|
help="CertMate — the SSL certificate lifecycle from your terminal.")
|
|
@@ -166,11 +166,31 @@ def cert_ls(ctx: typer.Context):
|
|
|
166
166
|
table.add_column("DAYS", justify="right")
|
|
167
167
|
table.add_column("CA")
|
|
168
168
|
table.add_column("AUTO-RENEW")
|
|
169
|
-
|
|
169
|
+
# Sort by seconds where the server sends them: a certificate with 23 hours
|
|
170
|
+
# left and one that lapsed an hour ago are both 0 or -1 in whole days, and
|
|
171
|
+
# would otherwise land next to each other in either order.
|
|
172
|
+
def _remaining(cert):
|
|
173
|
+
if cert.seconds_left is not None:
|
|
174
|
+
return cert.seconds_left
|
|
175
|
+
if cert.days_until_expiry is not None:
|
|
176
|
+
return cert.days_until_expiry * 86400
|
|
177
|
+
return 1 << 40
|
|
178
|
+
|
|
179
|
+
for c in sorted(certs, key=_remaining):
|
|
170
180
|
days = c.days_until_expiry
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
expired = c.has_expired()
|
|
182
|
+
# "0" for a certificate with 23 hours left was read as expired by
|
|
183
|
+
# everything that saw it, including CertMate's own dashboard until
|
|
184
|
+
# server 2.32.2. Say which of the two it is instead of printing the
|
|
185
|
+
# number that cannot tell them apart.
|
|
186
|
+
if expired is True:
|
|
187
|
+
days_str, colour = "expired", "red"
|
|
188
|
+
elif days is None:
|
|
189
|
+
days_str, colour = "-", "green"
|
|
190
|
+
elif days == 0:
|
|
191
|
+
days_str, colour = ("<1" if expired is False else "?"), "red"
|
|
192
|
+
else:
|
|
193
|
+
days_str = str(days)
|
|
174
194
|
colour = "red" if days < 7 else ("yellow" if days < 30 else "green")
|
|
175
195
|
table.add_row(
|
|
176
196
|
c.domain,
|
|
@@ -187,8 +207,20 @@ def cert_info(ctx: typer.Context, domain: str):
|
|
|
187
207
|
"""Show a certificate's details."""
|
|
188
208
|
c = _run(lambda: _client(ctx).get_certificate(domain))
|
|
189
209
|
out.print(f"[bold]{c.domain}[/]")
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
if c.host and c.host != c.domain:
|
|
211
|
+
# Where it is served, when that is not its name: an IP certificate's
|
|
212
|
+
# address, or the host a certificate named apart from it is for.
|
|
213
|
+
out.print(f" host: {c.host}")
|
|
214
|
+
if c.has_expired() is True:
|
|
215
|
+
remaining = "[red]expired[/]"
|
|
216
|
+
elif c.days_until_expiry is None:
|
|
217
|
+
remaining = "? days"
|
|
218
|
+
elif c.days_until_expiry == 0:
|
|
219
|
+
# Under a day, and not expired, or the server is too old to say.
|
|
220
|
+
remaining = "less than a day" if c.has_expired() is False else "? days"
|
|
221
|
+
else:
|
|
222
|
+
remaining = f"{c.days_until_expiry} days"
|
|
223
|
+
out.print(f" expires: {c.expiry_date or '-'} ({remaining})")
|
|
192
224
|
out.print(f" CA: {c.ca_provider or '-'}")
|
|
193
225
|
out.print(f" DNS: {c.dns_provider or '-'}")
|
|
194
226
|
out.print(f" auto-renew: {c.auto_renew}")
|
|
@@ -205,6 +237,12 @@ def cert_create(
|
|
|
205
237
|
dns: Optional[str] = typer.Option(None, "--dns", help="DNS provider (e.g. cloudflare)."),
|
|
206
238
|
ca: Optional[str] = typer.Option(None, "--ca", help="CA provider (e.g. letsencrypt)."),
|
|
207
239
|
san: Optional[str] = typer.Option(None, "--san", help="Comma-separated SAN domains."),
|
|
240
|
+
# A str with an empty default, not Optional: typer reads the annotation at
|
|
241
|
+
# runtime, and `str | None` is not one on Python 3.9.
|
|
242
|
+
name: str = typer.Option(
|
|
243
|
+
"", "--name",
|
|
244
|
+
help="Name the certificate apart from its domain, for a second certificate for it "
|
|
245
|
+
"(server API contract 2.61 or later)."),
|
|
208
246
|
wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for issuance to finish."),
|
|
209
247
|
dry_run: bool = typer.Option(False, "--dry-run",
|
|
210
248
|
help="Validate inputs and preflight the DNS provider WITHOUT issuing."),
|
|
@@ -214,6 +252,8 @@ def cert_create(
|
|
|
214
252
|
# bogus "" SAN entry — mirrors cert_reissue.
|
|
215
253
|
sans: List[str] = [s.strip() for s in san.split(",") if s.strip()] if san else []
|
|
216
254
|
client = _client(ctx)
|
|
255
|
+
# What it is issued as: the domain, and the name it is given apart from it.
|
|
256
|
+
issued_as = f"{domain} as {name}" if name else domain
|
|
217
257
|
|
|
218
258
|
if dry_run:
|
|
219
259
|
problems = []
|
|
@@ -222,6 +262,16 @@ def cert_create(
|
|
|
222
262
|
for s in sans:
|
|
223
263
|
if not _DOMAIN_RE.match(s):
|
|
224
264
|
problems.append(f"SAN {s!r} does not look valid")
|
|
265
|
+
if name:
|
|
266
|
+
# The server's rule (#1149): the shape of a DNS name, never a wildcard.
|
|
267
|
+
if not _DOMAIN_RE.match(name) or name.startswith("*."):
|
|
268
|
+
problems.append(f"name {name!r} is not a DNS name")
|
|
269
|
+
try:
|
|
270
|
+
client.require_contract("2.61", "name")
|
|
271
|
+
except UnsupportedByServer as e:
|
|
272
|
+
problems.append(str(e))
|
|
273
|
+
except CertMateError as e:
|
|
274
|
+
_die(str(e))
|
|
225
275
|
if dns:
|
|
226
276
|
res = _run(lambda: client.test_dns_provider(dns))
|
|
227
277
|
ok = bool(res.get("success", res.get("ok", True)))
|
|
@@ -233,23 +283,23 @@ def cert_create(
|
|
|
233
283
|
out.print("[dim]No --dns given; skipping provider preflight.[/]")
|
|
234
284
|
if problems:
|
|
235
285
|
_die("dry-run found issues:\n - " + "\n - ".join(problems))
|
|
236
|
-
out.print(f"[green]dry-run OK[/] — would issue [bold]{
|
|
286
|
+
out.print(f"[green]dry-run OK[/] — would issue [bold]{issued_as}[/]"
|
|
237
287
|
f"{(' (+' + str(len(sans)) + ' SAN)') if sans else ''}"
|
|
238
288
|
f"{(' via ' + dns) if dns else ''}{(' on ' + ca) if ca else ''}. Nothing issued.")
|
|
239
289
|
return
|
|
240
290
|
|
|
241
291
|
def _issue():
|
|
242
|
-
with out.status(f"Issuing [bold]{
|
|
292
|
+
with out.status(f"Issuing [bold]{issued_as}[/] …") as status:
|
|
243
293
|
def _progress(j: Job):
|
|
244
294
|
if j.status:
|
|
245
|
-
status.update(f"Issuing [bold]{
|
|
295
|
+
status.update(f"Issuing [bold]{issued_as}[/] … [{j.status}]")
|
|
246
296
|
return client.create_certificate(
|
|
247
297
|
domain, dns_provider=dns, ca_provider=ca, san_domains=sans or None,
|
|
248
|
-
wait=wait, on_progress=_progress)
|
|
298
|
+
name=name or None, wait=wait, on_progress=_progress)
|
|
249
299
|
|
|
250
300
|
job = _run(_issue)
|
|
251
301
|
if wait:
|
|
252
|
-
out.print(f"[green]issued[/] [bold]{
|
|
302
|
+
out.print(f"[green]issued[/] [bold]{issued_as}[/]"
|
|
253
303
|
f"{(' (' + job.status + ')') if job.status else ''}.")
|
|
254
304
|
else:
|
|
255
305
|
out.print(f"[yellow]accepted[/] — job [bold]{job.job_id}[/] "
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "certmate-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "CertMate command-line interface — the SSL certificate lifecycle from your terminal"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -30,10 +30,13 @@ classifiers = [
|
|
|
30
30
|
]
|
|
31
31
|
# Built ON the SDK (never the reverse). typer for the command surface, rich
|
|
32
32
|
# for tables/spinners. Still light — no server deps.
|
|
33
|
-
# The SDK floor tracks the CLI release
|
|
34
|
-
#
|
|
33
|
+
# The SDK floor tracks the CLI release, and now something enforces that:
|
|
34
|
+
# tests/test_version_consistency.py fails if this floor falls behind the SDK
|
|
35
|
+
# in the tree. It had already drifted once, sitting at 0.1.3 while both
|
|
36
|
+
# packages were 0.1.4. 0.1.6 is needed for Client.require_contract(), which
|
|
37
|
+
# `cert create --name --dry-run` calls, and create_certificate(name=...).
|
|
35
38
|
dependencies = [
|
|
36
|
-
"certmate-sdk>=0.1.
|
|
39
|
+
"certmate-sdk>=0.1.6",
|
|
37
40
|
"typer>=0.9",
|
|
38
41
|
"rich>=13",
|
|
39
42
|
]
|