Skip to content

Commit

Permalink
add certgrinderd 'show acmeaccount' and certgrinder 'show caa' comman…
Browse files Browse the repository at this point in the history
…ds, fix unit tests, update changelogs
  • Loading branch information
tykling committed Oct 24, 2023
1 parent d18545f commit 3588cf3
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 61 deletions.
62 changes: 58 additions & 4 deletions client/certgrinder/certgrinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def __init__(self) -> None:
"""Define the default config."""
self.conf: typing.Dict[str, typing.Union[str, int, bool, typing.List[str]]] = {
"alternate-chain": False,
"caa-validation-methods": "dns-01,http-01",
"certgrinderd": "certgrinderd",
"cert-renew-threshold-days": 30,
"domain-list": [],
Expand Down Expand Up @@ -710,9 +711,9 @@ def run_certgrinderd(
self,
stdin: bytes,
command: typing.List[str],
certgrinderd_stdout: typing.Optional[bytes] = None,
certgrinderd_stderr: typing.Optional[bytes] = None,
) -> typing.Optional[bytes]:
certgrinderd_stdout: bytes = b"",
certgrinderd_stderr: bytes = b"",
) -> bytes:
"""Run the configured ``self.conf["certgrinderd"]`` command.
The stdin argument will be passed to stdin of the command. A CSR is needed for
Expand All @@ -738,9 +739,12 @@ def run_certgrinderd(
stderr=subprocess.PIPE,
)

# send stdin and save stdout (the certificate chain/OCSP response) +
# send stdin and save stdout (the certificate chain/OCSP response/other output) +
# stderr (the certgrinderd logging)
certgrinderd_stdout, certgrinderd_stderr = p.communicate(input=stdin)
logger.debug(
f"certgrinderd command returned {len(certgrinderd_stdout)} bytes stdout and {len(certgrinderd_stderr)} bytes stderr output"
)

# log certgrinderd_stderr (which contains all the certgrinderd logging) at the level it was logged to, as possible
if isinstance(certgrinderd_stderr, bytes):
Expand Down Expand Up @@ -1611,6 +1615,39 @@ def check_tlsa(self) -> None:
f"Done checking DNS for TLSA records for {domain} port {self.conf['tlsa-port']} protocol {self.conf['tlsa-protocol']}"
)

# CAA METHODS

def show_caa(self) -> None:
"""The ``show caa`` subcommand method, called for each domainset by ``self.grind()``.
Returns:
None
"""
# get acmeaccount from certgrinderd
stdout = self.run_certgrinderd(stdin=b"", command=["show", "acmeaccount"])
url: str = ""
for line in stdout.decode().split("\n"):
if line[:15] == " Account URL: ":
url = line[15:]
break
else:
logger.error("certgrinderd did not return an acmeaccount")
sys.exit(1)

# output CAA records
for domain in self.domainset:
if domain[0] == "*":
# wildcard certificates only support dns-01
print(
f'{domain} IN CAA 128 issuewild "letsencrypt.org; validationmethods=dns-01; accounturi={url}"'
)
print(f'{domain} IN CAA 128 issue ";"')
else:
print(
f'{domain} IN CAA 128 issue "letsencrypt.org; validationmethods={self.conf["caa-validation-methods"]}; accounturi={url}"'
)
print(f'{domain} IN CAA 128 issuewild ";"')

# MAIN METHODS

def periodic(self) -> bool:
Expand Down Expand Up @@ -1820,6 +1857,9 @@ def grind(self, args: argparse.Namespace) -> None:
kcounter = 0
assert isinstance(self.conf["key-type-list"], list)
for keytype in self.conf["key-type-list"]:
if kcounter == 1 and args.method in ["show_caa"]:
# we dont need to see CAA records once per keytype
break
kcounter += 1
# loop over domains
dcounter = 0
Expand Down Expand Up @@ -1995,6 +2035,13 @@ def get_parser() -> argparse.ArgumentParser:
"tlsa-protocol", help="The protocol of the service, for example tcp"
)

# "show caa" subcommand
show_caa_parser = show_subparsers.add_parser(
"caa",
help="Use the 'show caa' sub-command to tell certgrinder to output a CAA record suitable for the specified domainset(s).",
)
show_caa_parser.set_defaults(method="show_caa")

# "version" command
subparsers.add_parser(
"version", help='The "version" command just outputs the version of Certgrinder'
Expand All @@ -2015,6 +2062,13 @@ def get_parser() -> argparse.ArgumentParser:
help="The command to reach the certgrinderd server, will get the input (CSR or cert chain) on stdin. Usually something like 'ssh certgrinderd@server -T'",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--caa-validation-methods",
required=False,
help="The ACME validation methods to include when outputting CAA records. Default: dns-01,http-01",
dest="caa-validation-methods",
default=argparse.SUPPRESS,
)
parser.add_argument(
"--cert-renew-threshold-days",
dest="cert-renew-threshold-days",
Expand Down
89 changes: 88 additions & 1 deletion client/certgrinder/tests/test_certgrinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,65 @@ def test_get_certificate(
assert "OCSP response not found" not in caplog.text
assert "was produced_at more than" not in caplog.text

# we only need to test CAA once
if certgrinderd_configfile[0] == "dns":
certpath = str(tmpdir_factory.mktemp("certificates"))
# make sure the "show caa" subcommand works,
# this requires pebble running with a registered account,
# so it is placed here instead of a seperate test
caplog.clear()
mockargs = [
"--path",
certpath,
"--domain-list",
"example.com,www.example.com",
"--domain-list",
"*.example.org",
"--certgrinderd",
f"server/certgrinderd/certgrinderd.py --config-file {certgrinderd_configfile[1]} --acme-server-url https://127.0.0.1:14000/dir",
"--debug",
]
with pytest.raises(SystemExit) as E:
main(mockargs + ["show", "caa"])
assert E.type == SystemExit, f"Exit was not as expected, it was {E.type}"
captured = capsys.readouterr()
assert (
'example.com IN CAA 128 issue "letsencrypt.org; validationmethods=dns-01,http-01; accounturi=https://127.0.0.1:14000/my-account/1"'
in captured.out
)
assert 'example.com IN CAA 128 issuewild ";"' in captured.out
assert (
'www.example.com IN CAA 128 issue "letsencrypt.org; validationmethods=dns-01,http-01; accounturi=https://127.0.0.1:14000/my-account/1"'
in captured.out
)
assert 'www.example.com IN CAA 128 issuewild ";"' in captured.out
assert (
'example.org IN CAA 128 issuewild "letsencrypt.org; validationmethods=dns-01; accounturi=https://127.0.0.1:14000/my-account/1"'
in captured.out
)
assert 'example.org IN CAA 128 issue ";"' in captured.out

# make sure the --caa-validation-methods arg works
mockargs = [
"--path",
certpath,
"--domain-list",
"example.com",
"--certgrinderd",
f"server/certgrinderd/certgrinderd.py --config-file {certgrinderd_configfile[1]} --acme-server-url https://127.0.0.1:14000/dir",
"--caa-validation-methods",
"dns-01",
"--debug",
]
with pytest.raises(SystemExit) as E:
main(mockargs + ["show", "caa"])
assert E.type == SystemExit, f"Exit was not as expected, it was {E.type}"
captured = capsys.readouterr()
assert (
'example.com IN CAA 128 issue "letsencrypt.org; validationmethods=dns-01; accounturi=https://127.0.0.1:14000/my-account/1"'
in captured.out
)


def test_show_spki(caplog, tmpdir_factory):
"""Test the 'show spki' subcommand."""
Expand Down Expand Up @@ -1232,7 +1291,7 @@ def test_help(capsys):
main(["help"])
assert E.type == SystemExit
captured = capsys.readouterr()
assert "See the manpage or ReadTheDocs for more" in captured.out
assert "ReadTheDocs" in captured.out


def test_show_configuration(capsys, tmpdir_factory):
Expand Down Expand Up @@ -1574,3 +1633,31 @@ def test_load_certificates_broken_input(tmpdir_factory):
)

certgrinder.load_certificates(path)


def test_certgrinderd_show_acmeaccount_command(
certgrinderd_env, certgrinderd_configfile
):
"""Test calling certgrinderd with the 'show acmeaccount' subcommand."""
if certgrinderd_configfile[0] != "dns":
# we only need to test this once
return

p = subprocess.Popen(
[
"server/certgrinderd/certgrinderd.py",
"--debug",
"--config-file",
certgrinderd_configfile[1],
"show",
"acmeaccount",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
certgrinderd_stdout, certgrinderd_stderr = p.communicate()
assert p.returncode == 1
assert (
"Could not find an existing account for server https://acme-v02.api.letsencrypt.org/directory"
in certgrinderd_stderr.decode()
)
36 changes: 32 additions & 4 deletions client/man/certgrinder.8
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "CERTGRINDER" "8" "Oct 02, 2023" "" "Certgrinder"
.TH "CERTGRINDER" "8" "Oct 24, 2023" "" "Certgrinder"
.SH NAME
certgrinder \- Manpage for certgrinder
.sp
Expand Down Expand Up @@ -380,6 +380,7 @@ usage: certgrinder
[\-h]
[\-a]
[\-\-certgrinderd CERTGRINDERD]
[\-\-caa\-validation\-methods CAA\-VALIDATION\-METHODS]
[\-\-cert\-renew\-threshold\-days CERT\-RENEW\-THRESHOLD\-DAYS]
[\-c CONFIG\-FILE]
[\-d]
Expand Down Expand Up @@ -427,6 +428,9 @@ Use alternate chain. For production this means using the short chain with 1 inte
.B \-\-certgrinderd
The command to reach the certgrinderd server, will get the input (CSR or cert chain) on stdin. Usually something like \(aqssh \fI\%certgrinderd@server\fP \-T\(aq
.TP
.B \-\-caa\-validation\-methods
The ACME validation methods to include when outputting CAA records. Default: dns\-01,http\-01
.TP
.B \-\-cert\-renew\-threshold\-days
A certificate is renewed when it has less than this many days of lifetime left. Default: \fI30\fP
.TP
Expand Down Expand Up @@ -691,7 +695,7 @@ Use the \(dqshow\(dq command to show certificates, TLSA records, SPKI pins or co
.ft C
certgrinder show
[\-h]
{certificate,configuration,paths,ocsp,spki,tlsa}
{certificate,configuration,paths,ocsp,spki,tlsa,caa}
\&...
.ft P
.fi
Expand All @@ -701,7 +705,7 @@ certgrinder show
.INDENT 0.0
.TP
.B subcommand
Possible choices: certificate, configuration, paths, ocsp, spki, tlsa
Possible choices: certificate, configuration, paths, ocsp, spki, tlsa, caa
.sp
Specify what to show using one of the available show sub\-commands
.UNINDENT
Expand Down Expand Up @@ -801,6 +805,20 @@ The port of the service, for example 443
.B tlsa\-protocol
The protocol of the service, for example tcp
.UNINDENT
.SS caa
.sp
Use the \(aqshow caa\(aq sub\-command to tell certgrinder to output a CAA record suitable for the specified domainset(s).
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
certgrinder show caa
[\-h]
.ft P
.fi
.UNINDENT
.UNINDENT
.SS version
.sp
The \(dqversion\(dq command just outputs the version of Certgrinder
Expand Down Expand Up @@ -1384,7 +1402,7 @@ It starts out by sleeping for a random period and then checks certificates and r
.UNINDENT
.INDENT 7.0
.TP
.B run_certgrinderd(stdin: bytes, command: List[str], certgrinderd_stdout: bytes | None = None, certgrinderd_stderr: bytes | None = None) -> bytes | None
.B run_certgrinderd(stdin: bytes, command: List[str], certgrinderd_stdout: bytes = b\(aq\(aq, certgrinderd_stderr: bytes = b\(aq\(aq) -> bytes
Run the configured \fBself.conf[\(dqcertgrinderd\(dq]\fP command.
.sp
The stdin argument will be passed to stdin of the command. A CSR is needed for
Expand Down Expand Up @@ -1533,6 +1551,16 @@ None
.UNINDENT
.INDENT 7.0
.TP
.B show_caa() -> None
The \fBshow caa\fP subcommand method, called for each domainset by \fBself.grind()\fP\&.
.INDENT 7.0
.TP
.B Returns
None
.UNINDENT
.UNINDENT
.INDENT 7.0
.TP
.B show_certificate() -> None
The \fBshow certificate\fP subcommand method, called for each domainset by \fBself.grind()\fP\&.
.INDENT 7.0
Expand Down
14 changes: 13 additions & 1 deletion docs/certgrinder-changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,23 @@ All notable changes to ``certgrinder`` will be documented in this file.
This project adheres to `Semantic Versioning <http://semver.org/>`__.

(unreleased)
------------
~~~~~~~~~~~~

Added
~~~~~
- `show caa` command to print suggested CAA records including method and ACME account pinning. The ACME account URI is retrieved using the new certgrinderd `show acmeaccount` command.
- New command-line argument `--caa-validation-methods` which defaults to `dns-01,http-01`. This can be used to control which validation methods are included in the CAA records output by the `show caa` command.

Fixed
~~~~~
- Tox docs build: Switch from `whitelist_external` to `allowlist_external`
- Tox docs build: Switch from requirements files to using the `docs` extra from `pyproject.toml`

v0.18.1 (11-oct-2023)
---------------------

Fixed
~~~~~
- Add missing development dependency `build` to dev extras in `pyproject.toml`
- Stop including unit tests in built packages. Tests are still included in the source `.tar.gz` distribution.

Expand Down
13 changes: 13 additions & 0 deletions docs/certgrinderd-changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ This project adheres to `Semantic Versioning <http://semver.org/>`__.
(unreleased)
------------

Added
~~~~~
- `show acmeaccount` command which runs `certbot show_account` and returns the output, including the ACME account URI for use in CAA records.

Fixed
~~~~~
- Tox docs build: Switch from `whitelist_external` to `allowlist_external`
- Tox docs build: Switch from requirements files to using the `docs` extra from `pyproject.toml`


v0.18.1 (11-oct-2023)
---------------------

Fixed
~~~~~

Expand Down
Loading

0 comments on commit 3588cf3

Please sign in to comment.