Compare commits

...

323 Commits

Author SHA1 Message Date
Alexis
792a3a75a4 Change parameter scope in workflows
Signed-off-by: Alexis <lxshancock@gmail.com>
2025-10-15 14:11:14 +09:00
ohemorange
4d5d5f7ae8 Merge pull request #10474 from certbot/util-openssl
Simplify loading code only used for PEM files
2025-10-09 13:43:01 -07:00
Brad Warren
d7ae612841 Release 5.1.0 (#10473) 2025-10-07 11:00:17 -07:00
Erica Portnoy
3683eb6cf4 Bump version to 5.2.0 2025-10-07 09:42:29 -07:00
Erica Portnoy
068203b1bd Remove built packages from git 2025-10-07 09:42:29 -07:00
Erica Portnoy
d97f2aefc3 Release 5.1.0 2025-10-07 09:42:28 -07:00
Erica Portnoy
1d32888ac4 Update changelog for 5.1.0 release 2025-10-07 09:42:00 -07:00
Brad Warren
33890ab7f3 deprecate make_self_signed_cert (#10466)
with https://github.com/certbot/certbot/pull/10465,
make_self_signed_cert is no longer used outside of our tests so let's
move it there and remove it from our public API
2025-09-30 07:16:03 -07:00
Brad Warren
513190afe0 don't use snakeoil certs in nginx (#10465)
this PR finally removes all uses of self-signed certificates from
certbot-nginx

i plan to create one last PR related to this deprecating
`acme.crypto_util.make_self_signed_cert` and moving the function to
certbot-compatibility-test which is the only place it's currently used

i think we could also do additional refactoring in certbot-nginx by
moving the _make_server_ssl call out of choose_or_make_vhost and make
deploy_cert responsible for calling it if the returned vhosts aren't
ssl. in this case, we could then skip updating cert and key directives a
second time as this is duplicate work if we just made the server ssl

i considered doing this, but it's a bigger refactor, breaks more tests,
and i'm not sure it really buys us much so i skipped it. i could take
this on or create an issue for it if you think it's important for us to
do for some reason tho ohemorange
2025-09-25 12:41:47 -07:00
Brad Warren
57d2429e4f remove create_if_no_match (#10464)
this is another tiny piece of my nginx refactoring. with
https://github.com/certbot/certbot/pull/10455, this function is now
never called outside of tests with `create_if_no_match=False` so this PR
removes the unnecessary parameter

luckily, tests still Just Work™ with this change
2025-09-25 08:43:23 -07:00
ohemorange
dc2f3b9eb0 Show --deploy-hook help info in certonly and run info, and add note that it will be run again later. (#10463)
Fixes https://github.com/certbot/certbot/issues/6180.

New output:

```
  --deploy-hook DEPLOY_HOOK
                        Command to be run in a shell once for each successfully issued certificate, including on subsequent renewals. Unless --disable-hook-validation is
                        used, the command’s first word must be the absolute pathname of an executable or one found via the PATH environment variable. For this command, the
                        shell variable $RENEWED_LINEAGE will point to the config live subdirectory (for example, "/etc/letsencrypt/live/example.com") containing the new
                        certificates and keys; the shell variable $RENEWED_DOMAINS will contain a space-delimited list of renewed certificate domains (for example,
                        "example.com www.example.com") (default: None)
```

Pre and post hooks are still only shown in `renew` and `reconfigure`
help, though perhaps there is less confusion over those so it's not
necessary.
2025-09-23 12:23:11 -07:00
ohemorange
f8838fc949 Remove unnecessary time.sleep from certbot-ci (#10461)
Fixes https://github.com/certbot/certbot/issues/10450

Discussion when originally added is
[here](https://github.com/certbot/certbot/pull/6989/files#r283050165).
Further notes are "The problem is transient, only observed on Travis so
far, not locally. I not know how to recreate a reliable pattern and find
precisely what needs to be done." So it's possible we weren't even
hitting this anymore anyway. Regardless, I ran the test a few times in
CI just to make sure it's not breaking.

Runs:

https://dev.azure.com/certbot/certbot/_build/results?buildId=9686&view=results
(pass)

https://dev.azure.com/certbot/certbot/_build/results?buildId=9688&view=results
(pass)

https://dev.azure.com/certbot/certbot/_build/results?buildId=9689&view=results
(pass)

https://dev.azure.com/certbot/certbot/_build/results?buildId=9690&view=results
(pass)

https://dev.azure.com/certbot/certbot/_build/results?buildId=9691&view=results
(pass)
2025-09-18 12:40:53 -07:00
Alexis
90905de18f [DOCS] Update documentation: Remove decommissioned server (#10460)
Deleted decommissioned server language

Fixes: #10459

Signed-off-by: zoracon <zoracon@pm.me>
2025-09-18 08:04:24 -07:00
Brad Warren
deb3d035e3 require ssl vhosts for HSTS and OCSP stapling (#10455)
this is the first part of the nginx refactoring work i wanted to do.
ohemorange, if this conflicts with your work on updating our nginx ssl
config, please feel free to either ignore this for now or point me to
your branch after merging this and i can fix up any merge conflicts
myself like i previously offered

the main thing this PR does is create a new choose_or_make_vhosts
function with the current choose_vhosts behavior and makes choose_vhosts
only return existing ssl vhosts which i think is the behavior we want
when setting up HSTS or OCSP stapling. [this is what we do in
apache](867b499f9b/certbot-apache/src/certbot_apache/_internal/configurator.py (L1795-L1829)),
enabling HSTS or OCSP stapling on an HTTP vhost seems wrong/dangerous,
and since we don't have cert and key information in these [enhance
calls](867b499f9b/certbot/src/certbot/interfaces.py (L255)),
any SSL vhost we create will be left with snakeoil certs which also
seems very wrong

of course, this simple change to certbot-nginx's prod code required many
changes to its tests. the config file for headers.com was introduced
[here](https://github.com/certbot/certbot/pull/6068) specifically for
testing HSTS so i added the SSL configuration it needs to make work with
the choose_vhost changes. that then broke the IP tests for headers.com
that were added in https://github.com/certbot/certbot/pull/10145/ so i
created a new no-listens.com vhost for testing that

if this is merged, my plan in the next PR or two is to:

1. since choose_or_make_vhosts is now always called with
create_if_no_match=True in prod code, i plan to remove that variable,
make that the default behavior of the function, and fix up tests
2. then, since choose_or_make_vhosts is only called in deploy_cert, i
plan to pass the cert and key to it so it can be used in
_make_server_ssl instead of the dummy certs currently being used there

i could do more of this in this PR if you want ohemorange, but i figured
it rarely hurts to break things up especially when the code is kind of
tricky like it is (to me) here
2025-09-17 14:23:32 -07:00
ohemorange
867b499f9b Merge pull request #10451 from certbot/candidate-5.0.0
release 5.0.0
2025-09-02 12:09:12 -07:00
Brad Warren
8ecd7fc23d Bump version to 5.1.0 2025-09-02 08:47:05 -07:00
Brad Warren
95e65ac3a5 Remove built packages from git 2025-09-02 08:47:05 -07:00
Brad Warren
c307f62114 Release 5.0.0 2025-09-02 08:47:04 -07:00
Brad Warren
6f8a357147 Update changelog for 5.0.0 release 2025-09-02 08:46:14 -07:00
ldlb
8556a9c427 fix: Remove pyOpenSSL dependency with custom certificate text formatting (#10439)
fixed: #10434

---------

Co-authored-by: ohemorange <ebportnoy@gmail.com>
2025-08-29 14:39:48 -07:00
ohemorange
1c67e990f7 Merge pull request #10448 from certbot/no-openssl-ocsp
remove openssl-based ocsp checking
2025-08-28 10:22:17 -07:00
Brad Warren
9c1685638b fixup pytest.ini, out var, and add changelog 2025-08-28 09:33:37 -07:00
Brad Warren
a06fa7643d Merge branch 'main' into no-openssl-ocsp 2025-08-28 09:27:06 -07:00
ohemorange
5c30a180a2 Apache config update (#10443)
Part of https://github.com/certbot/certbot/issues/10183

There are two changes made here:

1. Add `DHE-RSA-CHACHA20-POLY1305` to `SSLCipherSuite` list. Nice to
have for compliance reasons. See
https://github.com/mozilla/server-side-tls/issues/285 DHE ciphers in
general are the topic of
[some](https://github.com/mozilla/server-side-tls/issues/268)
[debate](https://github.com/mozilla/server-side-tls/issues/299) over on
the generator repo; my opinion is that certbot should match whatever
they currently recommend, and if they do decide to change it, we can
also update to match them again later.
2. Configure curves with `SSLOpenSSLConfCmd`. Needed for OpenSSL 3.0
support, so FFDH won't be used. This option requires apache `>=2.4.8`
but we already require `>=2.4.11` to use this conf file in the first
place, so that's fine. When the file in their repo was converted from
`.hbs` to `.js` the version requirement was changed to `>=2.4.11` but I
suspect that's a bug, and either way it's still fine. See
https://github.com/mozilla/ssl-config-generator/issues/270 and
https://github.com/mozilla/ssl-config-generator/pull/297

You can see a generated Apache config file here:
https://ssl-config.mozilla.org/#server=apache&version=2.4.60&config=intermediate&openssl=3.4.0&guideline=5.7

Originally, I had planned to switch `SSLProtocol` list to allowlist
format. It's a little nicer, though it did the same thing, technically,
and would let us match mozilla, who does this because it makes their
code cleaner. See
https://github.com/mozilla/ssl-config-generator/pull/286/. But TLSv1.3
is only
[supported](https://github.com/mozilla/ssl-config-generator/blob/master/src/js/configs.js#L12C5-L12C21)
in `apache >= 2.4.36`, so we on the other hand would have to include it
conditionally. Whereas despite `SSLv2` [not being
available](https://github.com/mozilla/ssl-config-generator/pull/286/files#diff-3d067ee1b10453909ca9c86397a6b235fcb961ed3ca1cfbdda4daa2cb4b30b97L41)
in `apache >= 2.3.16`, it still recognizes it just fine in a config.
We're always `>= 2.3.16` now so we could remove it proactively, I
suppose, in case apache stops recognizing it. I left it as-is here but
could change it.
2025-08-27 16:38:08 -07:00
Brad Warren
4c5e575531 use a static string when creating self-signed certs (#10447)
this is the quickest somewhat sane fix for the current CI problem that i
could think of

the domain being used in the self-signed cert really seems irrelevant.
on my main test VPS, socket.gethostname returns "brad-certbot-dev". on
my laptop, it's "MacBookPro". i manually tested this branch a bit on my
VPS and nginx seems content

using a simple static string like this seems unlikely to break anything
to me and i think helps clearly identify where the self-signed cert is
coming from if ever causes a problem for anyone in the future
2025-08-26 17:04:08 -07:00
Brad Warren
aabc5a0dd6 update centos ami (#10445)
it looks like the one previously being used was deleted so i grabbed a
new one from https://centos.org/download/aws-images/

you can see this passing at
https://dev.azure.com/certbot/certbot/_build/results?buildId=9627&view=results
after also reconfiguring CI to only run test farm tests
2025-08-25 10:21:33 -07:00
Brad Warren
444e3251bf make apache tls warning conditional (#10444)
this is in response to the thread at
https://opensource.eff.org/eff-open-source/pl/49gnrcner7gptrnsi339bqu1yo
2025-08-25 10:19:18 -07:00
Brad Warren
e9dd4eb03b stop running setup.py and switch to python -m build (#10442)
fixes #10404

unfortunately, exactly what `python setup.py clean` did doesn't seem
well documented so i dug into the code with a debugger. executing the
`clean` subcommand is done by [this
code](9cc2f5c05c/setuptools/_distutils/command/clean.py (L54-L77))
where the relevant build variables are set by the `build` subcommand
[here](9cc2f5c05c/setuptools/_distutils/command/build.py (L52))
and
[here](9cc2f5c05c/setuptools/_distutils/command/build.py (L112)).
it turns out us running `python setup.py clean` was already redundant
with `rm -rf build` on the next line

i built two releases, one on the latest commit in this PR and another on
44f1dd677b
before the switch to `python -m build`. a simple diff of the resulting
tarballs and wheels fails, presumably because of metadata differences,
but after untaring or unzipping the files, the contents are identical
for all of our built packages
2025-08-22 11:03:00 -07:00
ohemorange
d75cb60eeb Clean up warning catching in pytest.ini (#10438)
Several of these have been fixed, so let's update the requirement if
necessary and remove the warning catching.

`python-dateutil 2.9.0` was released Feb 29, 2024, so it's not widely
packaged in non-EOL major distros yet.
`pytest-cov 4.1.0` was released May 24, 2023.
Our pinned versions were already higher than these requirements.

Alternatively, we could just remove the warnings and not update the
minimum requirement, but I think it's nicer to note it in requirements
for anyone running our tests, like packagers.

We already require `poetry-plugin-export>=1.9.0`. `1.7.0` updated its
`requests-toolbelt` requirement to `>=1.0.0`, which is greater than the
minimum version needed to remove the warning.
2025-08-21 11:33:57 -07:00
ohemorange
4f12c29ef3 Remove unused code for renaming certificates (#10437)
Fixes https://github.com/certbot/certbot/issues/10397

This code was not being called, so let's remove it.
2025-08-19 09:29:33 -07:00
ohemorange
3c1bd657b5 Add instructions for renaming certificate to docs (#10436)
Fixes https://github.com/certbot/certbot/issues/10431
2025-08-19 09:27:41 -07:00
ohemorange
00a51ab22b Drop in uv for pip (#10428)
It's a [drop-in
replacement](https://docs.astral.sh/uv/pip/compatibility/) that speeds
things up. I don't see any reason why not.

`--use-pep517` is [set by default](
https://docs.astral.sh/uv/pip/compatibility/#pep-517-build-isolation),
so we don't need it.

`--disable-pip-version-check` also does nothing on uv.

`uv` [uses
separate](https://docs.astral.sh/uv/pip/compatibility/#build-constraints)
`UV_BUILD_CONSTRAINT` and `UV_CONSTRAINT`. I just added it to both to do
the simplest thing here. We could split them.

We probably don't actually need to pipstrap pip anymore, I could take
that out.

What's happening with `parsedatetime` and `python-digitalocean` is that
they were always secretly wrong. Since `pip` compiles bytecode by
default, it was suppressing the errors. If you add the
`--compile-bytecode` flag to `uv`, it passes, but I don't think we
should do that. You can see the failure happen on main by passing
`--no-compile` to the pip args and running `certbot -r -e oldest`.

Now what I don't understand is that some places seem to say the `'\/'`
error from `parsedatetime` only started in python 3.12, whereas others
see it on earlier python. Perhaps pytest is vendorizing python or
something. Not too worried about that, needed to get updated anyway, and
it's an accurate oldest version based on our oldest OSes.
`python-digitalocean` is techincally newer than debian 11, but we've
made that decision before so it seems fine to me.
2025-08-18 13:17:02 -07:00
Brad Warren
cad2c61db1 add changelog for #10377 (#10429)
it feels changelog entry worthy to me
2025-08-18 11:14:46 -07:00
Jacob Hoffman-Andrews
a58d9e96d7 ari: store Retry-After in lineage config (#10377)
This uses the new `storage.atomic_rewrite` function to write only the
ARI-related value to the config file, leaving other values the same.

Updates `storage.atomic_rewrite` to handle the case where the
`"renewalparams"` section might be empty (which occurs in the new
unittests), and adds some comments.

Note: `atomic_rewrite` is mocked out as a no-op in
`test_resilient_ari_directory_fetches` and `test_renew_before_expiry`
because those tests have a simplistic config object on their
mock_renewable_cert that doesn't include a filename. If we try to write
the config in those tests we'd get an error (trying to write to `None`).
Since those tests aren't intended to test the "store and obey
Retry-After" behavior, I figured it's reasonable to skip it in the name
of testing; but of course, open to idea about the best way to navigate
this.

Part of #10355
2025-08-18 11:05:29 -07:00
ohemorange
56da12207b Mock time.sleep for more modules to speed up tests (#10427)
The extra 8 seconds isn't really relevant in the test farm, but is nice
to have for local runs. This is based on what was done in
https://github.com/certbot/certbot/pull/10419/.
2025-08-18 09:59:55 -07:00
ohemorange
1816e56557 Set up unreleased packages minus dns plugins to use pyproject.toml (#10424)
Part of https://github.com/certbot/certbot/issues/10403

We were never actually updating the versions in certbot-ci and letstest.
Not that it really matters, but let's do that there as well.
2025-08-18 09:57:14 -07:00
ohemorange
407dc158f6 Set up dns plugins to use pyproject.toml (#10425)
Final part of https://github.com/certbot/certbot/issues/10403

I tested running `tools/snap/generate_dnsplugins_snapcraft.sh
certbot-dns-dnsimple` and it put the correct description in to the
`snapcraft.yaml` file.
2025-08-15 09:17:15 -07:00
ohemorange
d5dd8e47e1 Set up released packages minus dns plugins to use pyproject.toml (#10417)
Part of https://github.com/certbot/certbot/issues/10403.

As far as I can tell, "stick it in setup.py" is the official way of
handling complex dependencies. But since the version is static, we have
a little more choice here than we had with `certbot/pyproject.toml`.

We could put the version in the respective `pyproject.toml`s and read it
directly from the toml file with something like
[this](https://stackoverflow.com/a/78082561). Or otherwise load and
parse that file. The benefit of doing it that way is that all
non-certbot versions would be canonically in the `pyproject.toml`, and
also if we wanted we could use that same toml parsing to change the
version at release time instead of `sed`. I actually suspect `acme`,
`certbot-ci`, and `certbot-compatibility-test` will be the only ones
where we can completely delete `setup.py`, as the others all have
lockstep dependencies. (side note - we just never update `certbot-ci`
version. it's still set at `0.32.0.dev0`. there's no way this matters
but just noting.) I chose to do it this way instead because it seems
cleaner since we have to keep `setup.py` around anyway, but I don't have
a strong preference.

Based on what I've read, there's not actually a clean way to grab and
insert the version number within the toml file. This is due to [design
decisions](https://github.com/toml-lang/toml/issues/77) by the toml
authors. The clean `all` extras specification that we used in
`certbot/pyproject.toml` [seems to be an
outlier](https://github.com/pypa/setuptools/discussions/3627#discussioncomment-6476654)
because it's pip handling the self-reference, not toml.
2025-08-15 08:33:16 -07:00
ohemorange
80fa8ad738 Remove unnecessary code from release script (#10426)
This code [never did
anything](https://github.com/certbot/certbot/pull/10417#issuecomment-3190266418),
and it is not now. Let's remove it so we don't have to keep taking it
into account. I did a test release and it removed the `dev0`s just fine.
2025-08-15 02:01:34 +00:00
ohemorange
454048f2f6 Fix memory leak in apache unit tests (#10421)
This was causing oldest tests to fail on my mac, which has an open file
limit of 256. Locks were being released at exit, but there were more
than 256 tests being run at once. Holding onto the file descriptor for
temporary files was making us keep the files open.

I also removed unnecessary setUps and tearDowns in subclasses so that
this could be fixed in only one spot.

If you wanted to do any testing locally, I was throwing this in places:
```
import errno, os, resource
open_file_handles = []
for fd in range(resource.getrlimit(resource.RLIMIT_NOFILE)[0]):
    try: os.fstat(fd)
    except OSError as e:
        if e.errno == errno.EBADF: continue
    open_file_handles.append(fd)
print(f'location description: {len(open_file_handles)}')
```
2025-08-14 17:13:33 -07:00
ohemorange
49900b27d3 Update pinned oldest dependencies (#10420)
This is not necessarily the absolute minimum versions/pins we could use,
but it does get tests working. Fixes
https://github.com/certbot/certbot/issues/10418.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-08-14 16:37:10 -07:00
ohemorange
7201159d32 Have oldest tests inherit environment from python tests (#10422) 2025-08-14 16:36:34 -07:00
ohemorange
801894c7da Speed up mac tests using various strategies (#10419)
Alternative to https://github.com/certbot/certbot/pull/10408/ and
https://github.com/certbot/certbot/pull/10415/ that fixes production
code for account meta and puts autouse fixtures in certbot and acme
tests. Overrides all `time.sleep` calls while we're at it.

Fixes the production code where it's simple/clean, and fixes the tests
for HTTPServer-based code because we just don't have that many mac users
using standalone.
2025-08-14 09:58:06 -07:00
Jacob Hoffman-Andrews
bd796deaa3 storage: factor out atomic update operation (#10409)
The responsibility for atomic updates to a config file was previously
spread across different functions. This moves the atomic update to the
lowest level of the call graph.

Also, factor out the code that creates a ConfigObj based on various
inputs (archive dir, cert locations, and renewal params). This allows
cleanly reusing it across the "update" and "create new" paths.

Now the "create new config" code path doesn't have to do any renaming,
and the "update config" code path can assume the input file exists (and
error if it doesn't).

This will make it easier and cleaner to reuse the config-writing code in
#10377.

Part of #10355
2025-08-13 18:53:37 +00:00
Brad Warren
34a128ae88 update pinned dependencies (#10416)
this fixes the alert those with access can see at
https://github.com/certbot/certbot/security/dependabot
2025-08-13 17:59:55 +00:00
Brad Warren
d5a2e9227c use pep585 types everywhere and add a test (#10414)
this is the final part of
https://github.com/certbot/certbot/issues/10195. this fixes
https://github.com/certbot/certbot/issues/10195

the changes in the first commit were done automatically with the
command:
```
ruff check --fix --extend-select UP006 --unsafe-fixes
```
the second commit configures ruff to check for this to avoid regressions

thanks for bearing with me thru these somewhat large automatically
generated PRs ohemorange 🙏
2025-08-12 16:56:45 -07:00
Brad Warren
27b344c8d8 use pep585 types in certbot-apache (#10413)
this is another part of https://github.com/certbot/certbot/issues/10195

these changes were done automatically with the command:

```
ruff check --fix --extend-select UP006 --unsafe-fixes certbot-apache
```
2025-08-12 15:36:02 -07:00
ohemorange
92c8aa9b6c Add note and warning about old nginx ssl options not receiving updates (#10410)
This PR is modeled on https://github.com/certbot/certbot/pull/10373/,
and is part of https://github.com/certbot/certbot/issues/10183.

relevant requirements:
use_tls13 >= 1.13.0
session_tix_off implemented: nginx >= 1.5.9 and openssl_version >=
1.0.2l
session tix off by default: >= 1.23.2

oldest non-deprecated major distros nginx versions:
debian 11 1.18.0
epel 8 1.23.1
ubuntu 22.04 1.18.0

Therefore, we can stop testing for use tls 13 and session tix off
allowed, with the same caveat as [in this
comment](https://github.com/certbot/certbot/pull/10373#issuecomment-3134101604).
While we could add a new split for configs that don't require session
tickets off to be set explicitly since it's the default, I don't think
it's worth doing now. I added a note in the comments about this.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-08-12 17:54:46 +00:00
ohemorange
5d05984dd9 remove python 3.9 support (#10406)
Fixes https://github.com/certbot/certbot/issues/10389. you can compare
this to the PR that did this for python 3.8 at
https://github.com/certbot/certbot/pull/10077

additional changes:
- linux-py310 test is removed from extended tests, since it's now run in
standard tests. additionally, openssl will never be < 1.1.1 now, due to
https://peps.python.org/pep-0644/.
- `letstest/scripts/test_openssl_version.py` was testing functionality
that was removed in https://github.com/certbot/certbot/pull/10373 so it
was deleted

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-08-12 17:49:02 +00:00
Brad Warren
c519307569 use pep585 types in private certbot files (#10412)
this is another part of https://github.com/certbot/certbot/issues/10195

the first commit was done automatically with the command:

```
ruff check --fix --extend-select UP006 --unsafe-fixes certbot/src/certbot/_internal
```

this was unfortunately insufficient as it left a line in webroot.py with
over 100 characters. i fixed this manually in my second commit
2025-08-11 23:51:23 +00:00
Brad Warren
1943889119 use pep585 types in public certbot files (#10411)
this is another part of https://github.com/certbot/certbot/issues/10195

these changes were done automatically with the commands:

```
ruff check --fix --extend-select UP006 --unsafe-fixes certbot
git checkout certbot/src/certbot/_internal
```

fixing up the files under certbot/src/certbot/_internal will be done in
another PR
2025-08-11 23:45:25 +00:00
Brad Warren
dd99dc30f2 use pep585 annotations in acme (#10407)
this is part of https://github.com/certbot/certbot/issues/10195 where i
posted [my general
plan](https://github.com/certbot/certbot/issues/10195#issuecomment-3176661347)
here

these changes were done automatically with the command:
```
ruff check --fix --extend-select UP006 --unsafe-fixes acme
```
2025-08-11 13:19:39 -07:00
Brad Warren
14a7a97f5b remove curses environment marker (#10405)
i noticed this when reviewing
https://github.com/certbot/certbot/pull/10402

we stopped using curses ages ago and should probably remove this. see
https://github.com/certbot/certbot/pull/3665
2025-08-11 18:09:52 +00:00
ohemorange
9b3db6d52c Add pyproject.toml for certbot (#10402)
This sets up a `pyproject.toml` file for certbot, initially generated
[using](https://hatch.pypa.io/latest/intro/#existing-project) `hatch new
--init` and modifying from there. Since we dynamically require acme of a
matching version, I kept that around in `setup.py` to do the simplest
thing in this PR.

Other possible (future) implementations:
- setuptools has a beta implementation to read from a
`requirements.txt`. we could generate one of those.
- we could just hardcode it and update at release time. I like not
having to keep the version up to date in various places but maybe it's
actually fine
- something something integration with poetry pinning?

I think the syntax for setting version dynamically in `pyproject.toml`
is much nicer than what we do in `setup.py`. It's a little silly to do
it there after we've bothered to calculate it, but I put it there in the
hopes of being able to remove it from `setup.py` someday/somehow.

It would be nice to access the version dynamically set in
`pyproject.toml` in `setup.py`, but I do not think it is likely to be
possible.

Here are some useful links regarding this migrations:
- [How to modernize a `setup.py` based
project?](https://packaging.python.org/en/latest/guides/modernize-setup-py-project/)
- [Writing your
`pyproject.toml`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/)
- [Configuring setuptools using `pyproject.toml` files](
https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html)
- [`pyproject.toml`
specification](https://packaging.python.org/en/latest/specifications/pyproject-toml/)
- [Platform compatibility
tags](https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/)
2025-08-11 10:50:41 -07:00
Brad Warren
f0f036a34c fixup pinning (#10400)
this is in response to
https://github.com/certbot/certbot/pull/10399#issuecomment-3166305086

this PR does two things:

1. it clarifies what is meant by "build dependencies" in DESIGN.md
2. fixes our workaround for
https://github.com/python-poetry/poetry/issues/4103 which broke when we
moved most of our code under `src` directories. i kept the previous `rm
-rf ${REPO_ROOT}/*/*.egg-info` line around for `letstest` and to
hopefully add some robustness for us if we ever move our code around
again
2025-08-08 10:08:44 -07:00
ohemorange
dea3e5f1c4 Set up ruff so that test files have at least some linting (#10399)
Alternative implementation for #7908.

In this PR:
- set up ruff in CI (add to `tox.ini`, mark dep in `certbot/setup.py`)
- add a `ruff.toml` that ignores particularly annoying errors. I think
line length isn't actually necessary to set with this workflow since
we're not checking it but putting it there for future usage.
- either fix or ignore the rest of the errors that come with the default
linting configuration. fixed errors are mostly unused variables. ignored
are usually where we're doing weird import things for a specific reason.
2025-08-08 08:48:43 -07:00
ohemorange
5859e50e44 Run ruff to fix test errors (#10398)
This is mostly removing unused imports, plus one unused `import as`. Had
to put back imports being used with `eval` -- see the second commit.
2025-08-07 22:10:02 +00:00
Jacob Hoffman-Andrews
13954a783a renewal: factor out AriClientPool (#10393)
Part of #10355.

This allows combining the NamespaceConfig object with a cache of ACME
clients, so we don't have to make the whole large NamespaceConfig object
available all the way down the renewal call stack.

The new AriClientPool is responsible for instantiating ACME clients for
the purpose of ARI fetching. It provides only a simple user agent,
listing the Certbot version. The only CLI flag it observes is
--no-verify-ssl.
2025-08-07 13:19:05 -07:00
ohemorange
f39a584db4 Remove SSLSocket and probe_sni (#10395)
Fixes https://github.com/certbot/certbot/issues/10381.

Also cleans out lingering unused OpenSSL references that I happened to
notice. I can move these to a different PR if requested.

Fixes https://github.com/certbot/certbot/issues/10079 as well.
```
$ git grep PKey
$ git grep "crypto.x509"
$ 
```

towncrier draft:
```
## 99.99.0 - 2025-08-05

### Changed

- Removed final instances of pyopenssl x509 and PKey objects
  * Removed `acme.crypto_util.SSLSocket`
  * Removed `acme.crypto_util.probe_sni`

  ([#10079](https://github.com/certbot/certbot/issues/10079),
  [#10381](https://github.com/certbot/certbot/issues/10381))
- Removed a number of deprecated classes/interfaces
  * Removed `acme.challenges.TLSALPN01Response`
  * Removed `acme.challenges.TLSALPN01`
  * Removed `acme.standalone.TLSServer`
  * Removed `acme.standalone.TLSALPN01Server`

  ([#10274](https://github.com/certbot/certbot/issues/10274))
```
2025-08-05 16:39:13 -07:00
Will Greenberg
15a145ac3f acme: remove deprecated TLS-ALPN challenge functionality (#10378)
Fixes #10274

---------

Co-authored-by: ohemorange <erica@eff.org>
Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
Co-authored-by: Brad Warren <bmw@eff.org>
2025-08-05 21:19:51 +00:00
Brad Warren
f55ea6e70f Release 4.2.0 (#10394) 2025-08-05 12:32:36 -07:00
Erica Portnoy
ffb2196e21 Bump version to 5.0.0 2025-08-05 08:58:17 -07:00
Erica Portnoy
ba9a10b866 Remove built packages from git 2025-08-05 08:58:17 -07:00
Erica Portnoy
832d11a63f Release 4.2.0 2025-08-05 08:58:16 -07:00
Erica Portnoy
1b209c9bd3 Update changelog for 4.2.0 release 2025-08-05 08:57:49 -07:00
Jacob Hoffman-Andrews
e9cf7e8a9b renewal: use early return for empty lineage server (#10392)
This allows outdenting most of the logic for _ari_renewal_time.

Split from #10377.
2025-08-05 08:16:54 -07:00
Brad Warren
3d8be0c3b2 deprecate python 3.9 (#10390)
i based this PR on https://github.com/certbot/certbot/pull/10034/

this is maybe a little early since [python 3.9's EOL is in
october](https://devguide.python.org/versions/#supported-versions), but
i thought this would be good to do now to ensure we didn't forget, help
keep us on schedule when we're not releasing every month, allow us to
fix the security alert those with access can see at
https://github.com/certbot/certbot/security/dependabot which is caused
by [us (transitively) needing an old version of urllib3 with python
3.9](d80b1d395a/tools/requirements.txt (L195)),
and simplify development ever so slightly as discussed at
https://github.com/certbot/certbot/pull/10179

i created https://github.com/certbot/certbot/issues/10389 to track
dropping support entirely
2025-08-04 14:14:09 -07:00
Brad Warren
581977c92d try and improve should_autorenew comments (#10391)
i wrote this in response to erica's thread at
https://opensource.eff.org/eff-open-source/pl/xtuemgdti78xfx1hn9jwbrfrjy

this hopefully helps some, but i think our logic here is unfortunately
fairly complicated which is reflected in the code comments. feel free to
suggest alternate wording or even just close this if you think our
comments currently in main are good enough
2025-08-04 13:48:12 -07:00
ohemorange
d80b1d395a Deprecate acme.crypto_util.probe_sni() (#10387)
Fixes #10386.

- Creates an internal version of `probe_sni` for
`certbot-compatibility-test` use
- Deprecates `acme.crypto_util.probe_sni()`
2025-08-04 08:08:45 -07:00
Brad Warren
106d21d21d modify pyvenv.cfg instead of PYTHONPATH (#10384)
this fixes https://github.com/certbot/certbot/issues/10176 and fixes
https://github.com/certbot/certbot/issues/10257. it is based on
https://github.com/certbot/certbot/pull/10017 and ohemorange said it's
fine for me to cherry-pick their commit here

this change accomplishes a few things:

* because PYTHONPATH is no longer set to
`"$SNAP/lib/python3.12/site-packages:${PYTHONPATH}"` which evaluates to
`"$SNAP/lib/python3.12/site-packages:"` if PYTHONPATH wasn't previously
set, Certbot no longer searches for Python modules in the current
working directory which was causing #10176. i was able to reproduce this
problem with our currently released snap and verify that this change
fixes that problem
* since we no longer set PYTHONPATH at all, it won't be set in user
hooks which was causing https://github.com/certbot/certbot/issues/10257
* as an added bonus, scripts that start with `#!/usr/bin/env
/snap/certbot/current/bin/python3` as suggested
[here](https://github.com/certbot/certbot/issues/10257#issuecomment-2809421320)
are still able to find and import certbot and its dependencies so those
scripts should continue to work. i verified this is the case with manual
testing

finally, i created two news fragments here based on the text at
https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
which says

> You can associate multiple issue numbers with a news fragment by
giving them the same contents.

when run on this PR `towncrier --draft` outputs:
```
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.

## 4.2.0.dev0 - 2025-07-31

### Changed

- Catches and ignores errors during the directory fetch for ARI checking so
  that these errors do not hinder the actual certificate issuance.
  ([#10342](https://github.com/certbot/certbot/issues/10342))
- Removed the dependency on `pytz`.
  ([#10350](https://github.com/certbot/certbot/issues/10350))

### Fixed

- The Certbot snap no longer sets the environment variable PYTHONPATH stopping
  it from picking up Python files in the current directory and polluting the
  environment for Certbot hooks written in Python.
  ([#10176](https://github.com/certbot/certbot/issues/10176),
  [#10257](https://github.com/certbot/certbot/issues/10257))
- Previously, we claimed to set FAILED_DOMAINS and RENEWED_DOMAINS env
  variables for use by post-hooks when certificate renewals fail, but we were
  not actually setting them. Now, we are.
  ([#10259](https://github.com/certbot/certbot/issues/10259))
- Added `--eab-hmac-alg` parameter to support custom HMAC algorithm for
  External Account Binding.
  ([#10281](https://github.com/certbot/certbot/issues/10281))
- Certbot now always uses the server value from the renewal configuration file
  for ARI checks instead of the server value from the current invocation of
  Certbot. This helps prevent ARI requests from going to the wrong server if
  the user changes CAs.
  ([#10339](https://github.com/certbot/certbot/issues/10339))



```

---------

Co-authored-by: Erica Portnoy <erica@eff.org>
2025-08-01 11:55:38 -07:00
ohemorange
b782c52ede Remove _snap_log_name and put back comments from #9956 (#10385)
https://github.com/certbot/certbot/pull/9956/ was accidentally merged,
so we rolled the thousand commits back, and put them back in
https://github.com/certbot/certbot/pull/9983/

In the process, commits
7d3ceb27b6
and
f62eab640e
were lost. The comment in `snapcraft.yaml` is superseded by
https://github.com/certbot/certbot/pull/10384, so let's just reapply the
remaining changes to `build_remote.py` and `snapcraft.yaml`.
2025-07-31 14:20:29 -07:00
Brad Warren
2b868708d5 move --eab-hmac-alg from fixed to added (#10383)
as i went to merge https://github.com/certbot/certbot/pull/10379, i
noticed that it moved the changelog entry from
https://github.com/certbot/certbot/pull/10319 from added to fixed which
i had missed in my prior reviews. i think added is more appropriate so
this moves it back

i personally don't think this PR requires two reviews
2025-07-31 11:38:17 -07:00
Will Greenberg
2ac7baa651 Add towncrier for automatic changelog generation (#10379)
blast from the past! resurrects
https://github.com/certbot/certbot/pull/9803 with all of @bmw's changes.
i figured instead of force-pushing a basically brand new branch and
obliterating the old review, i'd just start from a clean slate

fixes #8272

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
Co-authored-by: Brad Warren <bmw@eff.org>
2025-07-31 07:12:56 -07:00
Brad Warren
1f128b0e0a improve ARI error handling and logging (#10374)
fixes https://github.com/certbot/certbot/issues/10336 and fixes
https://github.com/certbot/certbot/issues/10357 using the plan at
https://github.com/certbot/certbot/issues/10336#issuecomment-3109192677

while this PR makes the renewal_time function slightly less nice, i
think us catching and handling the exceptions in certbot makes the most
sense so we can do exactly what we want around terminal and file logging

with this change, a output from a failed `sudo certbot renew` run looks
like
```
$ sudo certbot renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
Processing /etc/letsencrypt/renewal/example.org.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
An error occurred requesting ACME Renewal Information (ARI). If this problem persists and you think it's a bug in Certbot, please open an issue at https://github.com/certbot/certbot/issues/new/choose.
Certificate not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
The following certificates are not due for renewal yet:
  /etc/letsencrypt/live/example.org/fullchain.pem expires on 2025-10-23 (skipped)
No renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
```
`sudo certbot renew -q` produces no output and the relevant messages in
the log file look like:
```
2025-07-30 19:51:13,267:WARNING:certbot._internal.renewal:An error occurred requesting ACME Renewal Information (ARI). If this problem persists and you think it's a bug in Certbot, please open an issue at https://github.com/certbot/certbot/issues/new/choose.
2025-07-30 19:51:13,267:DEBUG:certbot._internal.renewal:Error while requesting ARI was:
Traceback (most recent call last):
  File "/home/brad/certbot/acme/src/acme/client.py", line 366, in renewal_time
    raise ValueError('im some error')
ValueError: im some error

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/brad/certbot/certbot/src/certbot/_internal/renewal.py", line 351, in _ari_renewal_time
    return acme.renewal_time(cert_pem)[0]
  File "/home/brad/certbot/acme/src/acme/client.py", line 370, in renewal_time
    raise errors.ARIError(error_msg, now + default_retry_after) from e
acme.errors.ARIError: ('failed to fetch renewal_info URL https://acme-staging-v02.api.letsencrypt.org/acme/renewal-info/oXQaBm1Qt4YtSizBfrSNiElszRY.LMjTHFS4HPbSRMOzLrA9OZId', datetime.datetime(2025, 7, 31, 1, 51, 13, 267088))

```
2025-07-30 16:31:59 -07:00
Brad Warren
ae2a00bbad prep for 4.2.0 and fix changelog (#10375)
something weird happened to the changelog in
https://github.com/certbot/certbot/pull/10319. a 4.2.0 entry was added
below the entry for `5.0.0 - main` despite 4.2.0 not having been
released. since it's sounding like we're expecting our next release to
be 4.2.0 and not 5.0, i merged these two changelog entries into one for
4.2.0

i also modified our setup.py files to use 4.2.0.dev0 instead of
5.0.0.dev0 altho this isn't strictly necessary because our release
script will automatically set all version numbers to whatever version we
give it on the command line before building the release
2025-07-29 14:44:39 -07:00
SATOH Fumiyasu
6ba8abe8d5 Remove the dependency on pytz (#10350)
The `pytz` is obsoleted by Python 3.9.
2025-07-28 08:00:16 -07:00
ohemorange
d8acf7cea0 Add note and warning about old-options-ssl-apache.conf not receiving updates (#10373)
Part of #10183

> Option 4. Stop updating old files with security improvements. If
people want to be on old software they can but then they're not getting
the nice new things. We can either warn or not warn if we see people
using them, either on certbot install (what, who's installing new
certbot on these machines), new cert, cert renewal, or certbot update.
The second two would require code changes, I'm pretty sure. I don't
think we should warn too often because that's how we get people to
silence all output. This is a little weird because we don't usually keep
around deprecated things. We could also warn loudly and see if people
complain. Or do some sort of brownout.

This PR warns every time certbot is run. We could make it run less often
(only when a new config file is installed, probably), but that's a more
extensive code change, and honestly I think it's probably fine? But I
can change it.
2025-07-25 12:05:58 -07:00
Nicholas Ciechanowski
33d3162c24 add 3rd party certbot-dns-synergy-wholesale to the docs (#10364) 2025-07-21 08:06:55 -07:00
Brad Warren
d2e1567d26 warn about missing server during ARI checks (#10369)
this is part of https://github.com/certbot/certbot/issues/10336

on my test server, the output of `certbot renew` after deleting the
server field looks like:
<img width="1815" height="398" alt="Screenshot 2025-07-17 at 12 06
07 PM"
src="https://github.com/user-attachments/assets/537bcc39-eb59-43b4-912f-d30ac22baae8"
/>
2025-07-17 13:17:50 -07:00
ohemorange
a020de1e50 Make ACME directory fetches for ARI checks resilient (#10358)
Fixes https://github.com/certbot/certbot/issues/10342

When doing ARI checks in acme.renewal_time, we catch RequestException
and return a default value. That's so an unavailable ARI server doesn't
cause issues.

Before we get to acme.renewal_time, we have to create an ACME client,
and in the process fetch a directory. We should make the directory fetch
similarly resilient.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-07-17 12:18:25 -07:00
Brad Warren
57cb40ee9a simplify request exception handling (#10363)
my desire to do this came from the discussion at
https://github.com/certbot/certbot/pull/10358#discussion_r2198273164

the code i'm deleting here came from
https://github.com/certbot/certbot/pull/4733

i think doing string parsing on the exception like this is convoluted
and overkill. i also agree with erica from the linked thread above that
we shouldn't be raising a ValueError here, especially when the docstring
for this function says `:raises requests.exceptions.RequestException: in
case of any problems` and doesn't mention ValueError

i prefer we do the simple thing and just delete the code. in the my
opinion unlikely event we decide polishing this important, i think we
can reconsider more complex approaches here
2025-07-17 12:07:00 -07:00
Brad Warren
eb563ccc1d update dependencies (#10368)
this fixes the security alerts those with access can see at
https://github.com/certbot/certbot/security/dependabot

setting `PIP_USE_PEP517: "true"` is needed with modern versions of
setuptools as described in the issue linked in the code comment. we're
already setting the equivalent flag in builds outside of snaps. see
https://github.com/certbot/certbot/pull/10249
2025-07-17 11:56:03 -07:00
ldlb
295d8c3c49 Add --eab-hmac-alg parameter to support custom HMAC algorithm for EAB (#10319)
fixed: #10281
2025-06-27 14:16:38 -07:00
Will Greenberg
372813175d Fix #10339 (#10353)
When making an ARI request, use the server listed in the cert's renewal
conf rather than the one passed in certbot's config.

Fixes #10339

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-06-27 10:51:55 -07:00
ohemorange
b70a9f0987 Restore le-auto, which should not be modified (#10351)
https://github.com/certbot/certbot/pull/10297 modified
`letsencrypt-auto-source/letsencrypt-auto`. It should never be modified
except during a release, and we have a test to make sure of it. Old
scripts pull directly from that file on github, so let's put that back
asap.
2025-06-24 21:07:55 +00:00
Yaroslav Halchenko
86f76cd3df Add codespell support (CI to check, not to fix) and make it fix a few typos (#10297)
Another token of gratitude for a super useful tool and service.

More about codespell: https://github.com/codespell-project/codespell .

I personally introduced it to dozens if not hundreds of projects already
and so far only positive feedback.

CI workflow has 'permissions' set only to 'read' so also should be safe.

---------

Signed-off-by: Yaroslav O. Halchenko <debian@onerussian.com>
2025-06-24 13:14:31 +09:00
ohemorange
6ee19bac55 Allow notification of two reviewers being assigned to a PR and two issue assignees (#10345)
Fixes https://github.com/certbot/certbot/issues/10344

You can see this working in the mattermost "Test" channel, where I ran
this code from my test repo.

The documentation for the PR reviewer syntax is here:
https://docs.github.com/en/webhooks/webhook-events-and-payloads?actionType=review_requested#pull_request

We now no longer notify on PR assignees. But I think that is the correct
behavior.

This PR also fixes a bug in the issue assigned notification code, and
now lets you see when two different people were assigned. That
documentation is here:
https://docs.github.com/en/webhooks/webhook-events-and-payloads#

After this is in, I'll make the same changes to the josepy repo.

You can see the `if` syntax here:
https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows

```
on:
  pull_request:
    types: [review_requested]
jobs:
  specific_review_requested:
    runs-on: ubuntu-latest
    if: ${{ github.event.requested_team.name == 'octo-team'}}
    steps:
      - run: echo 'A review from octo-team was requested'
```

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-06-23 17:08:21 +00:00
ohemorange
bc0b54950a Run changelog generation stage on ubuntu-latest, nightly (#10349)
Fixes https://github.com/certbot/certbot/issues/10328

This PR:
1) Moves changelog generation to ubuntu-latest instead of deprecated
windows, and
2) Sets it to run nightly so we catch breakages before release day
3) Modifies `update_changelog.py` to also allow `.dev0` version numbers
and headings with `main` instead of the date in them, for testing.

I could have been more specific about only matching `main` or a date,
but that seemed honestly unnecessary.

Here is a manually triggered nightly test; the test branch just
[removes](https://github.com/certbot/certbot/compare/changelog-gen...nightly-changelog-gen?expand=1)
all the other tests for speed:
https://dev.azure.com/certbot/certbot/_build/results?buildId=9250&view=results

You can download the created changelog artifact here:
https://dev.azure.com/certbot/certbot/_build/results?buildId=9250&view=artifacts&pathAsName=false&type=publishedArtifacts
2025-06-20 09:38:34 -07:00
ohemorange
035a6dcc39 Actually set FAILED_DOMAINS and RENEWED_DOMAINS variables when renewals fail (#10347)
Fixes https://github.com/certbot/certbot/issues/10259

This PR moves post-hook execution from `main.renew` to
`renewal.handle_renewal_request` so that failed and renewed domains
actually get passed into post-hook execution as promised, even when
failures happened.

I suspect the original PR was being overly cautious by putting the whole
thing into a try/finally so that post-hooks definitely happen, but
`handle_renewal_request` is already full of exception catching. I
understand the worry about executing a pre-hook and then failing to
execute its matching post-hook, but the code really is already
structured to make sure that that won't happen. And then when we added
`FAILED_DOMAINS` and `RENEWED_DOMAINS`, we both kept that
overly-cautious hooks execution location, but also kept the error so we
have a summary at the end...which meant that if failures happened, the
env vars were never set.

If we really want to keep the `hooks.run_saved_post_hooks` call on the
outside of everything in main, we can, but then we will have to do one
of the following:
- pass in the output lists to be filled out during execution. not my
favorite pattern
- throw the output lists in the error object or make a wrapper error,
not sure, haven't looked at `errors.py` too closely
- stop raising that final error where we report failures at the very
bottom. it's a little outdated maybe but I do like it and I think people
are used to it
- raise that error in main, returning the number of parse and renewal
failures. this is my favorite of the options, but I still like it less
than what I've implemented here.

Here's the integration/regression test failing on main:

https://dev.azure.com/certbot/certbot/_build/results?buildId=9237&view=logs&j=fca58cec-e7ce-563a-f36f-5c233894d750
You can see here that that branch just has the integration test without
the fix (and removing other tests for efficiency):
https://github.com/certbot/certbot/compare/main...test-fail-env-on-main

It's the default, but just to be clear, this should definitely have two
reviewers.
2025-06-20 07:42:20 -07:00
Brad Warren
a7e4ffb13b update acmev1 url comment (#10343)
this was the wrong/misleading comment i remember erica mentioning in our
discussions yesterday. the problem here is modern versions of certbot
also always save the server url. see
31599bad83/certbot/src/certbot/_internal/storage.py (L288-L291)

i personally don't think this requires two reviews and if whoever gets
to this first agrees, i think you should feel free to merge this
2025-06-13 10:31:49 -07:00
ohemorange
779ebe9085 Merge pull request #10341 from certbot/candidate-4.1.1
update files from 4.1.1 release
2025-06-12 13:40:50 -07:00
Brad Warren
f24cacf496 fixup changelog 2025-06-12 13:21:39 -07:00
ohemorange
b531a302ee Remove manual mattermost notification on azure failure code (#10330)
I added the exact same service hook we use for nightly failures for
release failures.
<img width="1347" alt="Screenshot 2025-06-11 at 10 32 18 AM"
src="https://github.com/user-attachments/assets/b4728d0b-212b-4ecb-84c6-0ed62715f0ff"
/>
Service hooks can be viewed here:
https://dev.azure.com/certbot/certbot/_settings/serviceHooks

Now there's no reason to keep around the manual notification stage, it
wasn't working in either case anyway. Since it's literally the same as
the nightly hook, I don't personally feel the need to test the release
branch but I can if the reviewer would like.
2025-06-12 12:40:07 -07:00
Brad Warren
4aaf9ccf59 Bump version to 5.0.0 2025-06-12 11:09:58 -07:00
Brad Warren
679f831cdd Add contents to certbot/CHANGELOG.md for next version 2025-06-12 11:09:58 -07:00
Brad Warren
2929d8072a Remove built packages from git 2025-06-12 11:09:58 -07:00
Brad Warren
c93a261aad Release 4.1.1 2025-06-12 11:09:57 -07:00
Brad Warren
9afb6415b8 fix up acme & certbot standalone code (#10293)
certbot's standalone code contains confusing references to things like
`SSLSocket` which we were hoping to deprecate in
https://github.com/certbot/certbot/issues/10284. are they relevant?
they're sure not!

certbot's standalone plugin only supports HTTP-01 so comments about
things like `ACMETLSServer` and the completely unused `certs` variable
can be deleted

furthermore, the type of the different variables named things like
`http_01_resources` were wrong in multiple places. as can be seen in
certbot's standalone code, the type is
`Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource]`. this is also
[the type used in acme.standalone's
tests](723fe64d4d/acme/src/acme/_internal/tests/standalone_test.py (L78-L81))
despite the file's type annotations saying it takes a different type. i
think the incorrect type annotations were never caught because mypy
can't fully make sense of our overly complex server classes here

finally, `from __future__ import annotations` was added to make [forward
references in type
annotations](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html#forward-references)
easier
2025-06-12 11:09:12 -07:00
Brad Warren
ee2bdafc56 Update changelog for 4.1.1 release 2025-06-12 11:08:34 -07:00
ohemorange
680d998597 Pass in dict of acme clients instead of acme so we can wait to initialize in some cases (#10337)
Regression test fails on main with commit "add regression test"
cherry-picked onto it

```
$ pytest   certbot/src/certbot/_internal/tests/renewal_test.py 
======================================================================= test session starts =======================================================================
platform darwin -- Python 3.12.8, pytest-8.3.5, pluggy-1.5.0
rootdir: /Users/erica/certbot
configfile: pytest.ini
plugins: anyio-4.9.0, cov-6.1.1, xdist-3.6.1
collected 27 items                                                                                                                                                

certbot/src/certbot/_internal/tests/renewal_test.py .....F.....................                                                                             [100%]

============================================================================ FAILURES =============================================================================
___________________________________________________________ RenewalTest.test_no_network_if_no_autorenew ___________________________________________________________

self = <certbot._internal.tests.renewal_test.RenewalTest testMethod=test_no_network_if_no_autorenew>
mock_autorenewal_enabled = <MagicMock name='autorenewal_is_enabled' id='4378096224'>, mock_client_network_get = <MagicMock name='get' id='4378087008'>
unused_mock_display = <certbot.tests.util.FreezableMock object at 0x104eb4f50>

>   ???
E   AssertionError: assert 1 == 0
E    +  where 1 = <MagicMock name='get' id='4378087008'>.call_count

certbot/src/certbot/_internal/tests/renewal_test.py:260: AssertionError
===================================================================== short test summary info =====================================================================
FAILED certbot/src/certbot/_internal/tests/renewal_test.py::RenewalTest::test_no_network_if_no_autorenew - AssertionError: assert 1 == 0
================================================================== 1 failed, 26 passed in 0.30s ===================================================================

```
2025-06-12 11:02:22 -07:00
ohemorange
31599bad83 Reduce logging level of ARI failure to info (#10335)
This is a feature people didn't have before and won't miss if it fails.
We can always raise it later, but let's reduce it for now to stop people
worrying about the big red warning.
2025-06-12 16:16:57 +00:00
ohemorange
b682687449 Avoid ari mismatch problem during dry-run (#10332)
This is one solution to https://github.com/certbot/certbot/issues/10327.
It won't test an ARI check during a dry run, since it will just avoid
the mismatch problem by checking for dry run first and returning before
checking ARI. This PR will make the big error (actually a warning, but
red and scary) go away though.
2025-06-12 08:05:57 -07:00
ohemorange
2e827c5da6 Improve changelog entry (#10331)
I thought https://github.com/certbot/certbot/pull/9804/ was abandoned
but the author just missed my comment. I would like to accept that PR to
get it in, but in the process of updating the PR I wrote a nicer
changelog entry, so I would like to add that.
2025-06-11 16:37:43 -07:00
✨ Q (it/its) ✨
8e9d867447 Print error details when an IssuanceError is thrown (#9804)
When a CA fails to issue a certificate after finalisation Certbot
currently prints the following unhelpful message:

```
An unexpected error occurred:
acme.errors.IssuanceError
```

This PR makes Certbot print the ACME error object from the order, as
such

```
An unexpected error occurred:
CAA error :: Invalid CAA: CAA prohibits issuance
```

## Pull Request Checklist

- [ ] The Certbot team has recently expressed interest in reviewing a PR
for this. If not, this PR may be closed due our limited resources and
need to prioritize how we spend them.
- [x] If the change being made is to a [distributed
component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout),
edit the `master` section of `certbot/CHANGELOG.md` to include a
description of the change being made.
- [x] Add or update any documentation as needed to support the changes
in this PR.
- [x] Include your name in `AUTHORS.md` if you like.
2025-06-11 15:15:45 -07:00
Brad Warren
1e8c09c05f Release 4.1.0 (#10326) 2025-06-11 07:32:03 -07:00
Erica Portnoy
4a1a136fcb Bump version to 5.0.0 2025-06-10 14:43:36 -07:00
Erica Portnoy
42789114b3 Add contents to certbot/CHANGELOG.md for next version 2025-06-10 14:43:36 -07:00
Erica Portnoy
9a08102f43 Remove built packages from git 2025-06-10 14:43:36 -07:00
Erica Portnoy
6a72811a39 Release 4.1.0 2025-06-10 14:43:35 -07:00
Erica Portnoy
f417f24998 Update changelog for 4.1.0 release 2025-06-10 14:43:05 -07:00
ohemorange
10b019b3b8 moving to src layout means we need to cd into src as well to grab version number for changelog (#10325) 2025-06-10 21:40:53 +00:00
ohemorange
47b44a6751 Add a changelog entry describing the impacts of ARI on short renew_before_expiry (#10323)
Fixes #10312. This is perhaps overly detailed, but I was hoping that by
giving a viable path forward it would forestall requests to change it
back, add a flag to ignore ari, or otherwise change the behavior. Very
open to suggestions on wording/content/length/etc.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-06-09 20:11:19 +00:00
ohemorange
4c5492fbec Use ubuntu-latest for mattermost-notify on azure (#10324)
There's no reason to be using a specific vmImage here; set it to
`ubuntu-latest` so we don't have to regularly update this. Fixes
https://github.com/certbot/certbot/issues/10322.
2025-06-09 12:45:53 -07:00
Jacob Hoffman-Andrews
1d9fc8dccf renewal: use lineage-specific server for ARI (#10307)
Previously, we were constructing an ACME client for ARI checking that
used the global value for `server`, not the one recorded in a lineage's
renewal file.

This resulted in errors in the logs and failure to observe ARI for
lineages that used a non-default `--server` (e.g. staging or non-Let's
Encrypt CAs).

---------

Co-authored-by: ohemorange <ebportnoy@gmail.com>
2025-06-09 11:44:04 -07:00
Jacob Hoffman-Andrews
a75057042f integration: add test for early renewal from ARI (#10311)
This depends on a pending Pebble pull request and so will fail
integration tests until/unless that lands:
https://github.com/letsencrypt/pebble/pull/501

However, I'd appreciate some eyes on this PR in this regard: is the
interface we're using in Pebble useful and appropriate? If not, we can
adjust the Pebble PR.

Inspired based on conversation on
https://github.com/certbot/certbot/pull/10307, but note that this just
tests the general case; it does not test the "default server differs
from lineage server" case yet; when I try adding that I get some bugs
that may reflect a problem in #10307 I need to fix (or may reflect that
I need to inhibit the `--server` flag rather than trying to override it
late in the command line).
2025-06-06 14:39:10 -07:00
Brad Warren
95a70e98c2 don't check ARI for expired certs (#10317)
fixes https://github.com/certbot/certbot/issues/10308

my thinking here was if the spec forbids checking ARI for expired certs,
this check should happen directly in the renewal_time function. if we do
that, what's its most useful response? error? return None? return a
datetime in the past?

i feel the latter is most helpful. tell the caller to renew now rather
than erroring out or giving it no suggestion about when it should renew

it probably doesn't matter much, but i think this would be nice to have
for 4.1.0 as it fixes a (minor) spec compliance issue in our ARI
implementation that is being released
2025-06-06 10:52:54 -07:00
ohemorange
48f34938c6 Change acme.renewal_time to only check ARI, and not also a default time. Separate out default check and use that in should_autorenew instead. (#10309)
Fixes https://github.com/certbot/certbot/issues/10298. Replacement for
#10301.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-06-04 14:48:44 -07:00
ohemorange
3cbe1288c9 Clean up renew_before_expiry default behavior (#10306)
[Recent changes](https://github.com/certbot/certbot/pull/10272/) to
`renewal.should_autorenew` assumed that if
`RenewableCert.configuration.renew_before_expiry` was set, that means
the user set it. That's wasn't true; we were throwing in a default value
if the user didn't set it. But there's no reason for that, especially
since we now set the default renewal time dynamically. Also, we were
writing out a commented `# renew_before_expiry = 30 days` without any
further documentation, in a file that we tell users they [shouldn't
really be
editing](https://eff-certbot.readthedocs.io/en/latest/using.html#modifying-the-renewal-configuration-file).
We now do neither of those things.
2025-06-02 14:19:31 -07:00
Brad Warren
e873874752 update developer OS dependency list (#10304)
this is a follow up from https://github.com/certbot/certbot/pull/10286
and related to https://github.com/certbot/certbot/issues/10302

sorry i initially missed this! in #10286 our tests were just yelling at
me about the different augeas package needed, but python headers and a
compiler are also needed for things to work with an updated version of
python-augeas

i don't believe we need this change in our macOS instructions because:

1. homebrew doesn't split up python packages the way many linux distros
do. there is no equivalent python-dev package
2. if you're using homebrew, you already have a compiler because
[homebrew requires command line tools for
xcode](https://docs.brew.sh/Installation#macos-requirements)
3. "it works on my machine"
2025-06-02 12:54:33 -07:00
Jacob Hoffman-Andrews
dbd0c6fce8 Deprecate parameter enforce_openssl_binary_usage (#10300)
Part of https://github.com/certbot/certbot/issues/10291
2025-05-29 13:28:48 -07:00
ohemorange
7a27a67cdb Respect Retry-After header when polling for order finalization (#10288)
Fixes #10273.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-05-15 16:24:52 +00:00
Brad Warren
5d03191493 deprecate SSLSocket and TLSServer (#10294)
fixes https://github.com/certbot/certbot/issues/10284
2025-05-15 09:06:18 -07:00
Jacob Hoffman-Andrews
723fe64d4d Add ARI support to acme module and to Certbot (#10272)
Follow-up to #10241. The acme module code is mostly the same, except the
switch to return a tuple containing Retry-After.

This includes the CLI-side work to call out to the new `renewal_time`
method when checking for renewal.

I moved `should_autorenew` from `storage.py` into `renewal.py`, where it
fits better (and also this solves an import cycle problem). To make the
edits more visible I split this into one commit for the move and [one
commit for the subsequent
edits](4e137d9b00 (diff-fad906e31304c767d620bfd243f4c7adf1e63a3420fd634ee57a0f6651c182cf)).

This does not yet attempt to store the Retry-After info, or failure
retries, in renewal configs. I figured since that's a pretty big chunk
of work and design on its own, I wanted to get interim feedback as is. I
think this PR would be okay to land with the current default crons /
systemd timers that run twice a day. I think we should implement storage
of retry information before increasing the frequency of runs. And if the
team would like to hold off on landing any ARI until that storage is
done, I'm good with that too. 👍🏻
2025-05-13 10:34:19 -07:00
Jacob Hoffman-Andrews
3a1f6782b6 Remove openssl-based OCSP checking
Use exclusively the `cryptography` package for OCSP checking.
2025-05-13 10:26:44 -07:00
Brad Warren
c5686e6653 fix mac dev augeas setup (#10287)
it appears these changes are also needed to work with python-augeas
1.2.0. i didn't catch this in
https://github.com/certbot/certbot/pull/10286 because the problem only
affects ARM macs and it appears [our CI only offers intel
macs](https://learn.microsoft.com/en-us/azure/devops/release-notes/roadmap/macos-agents-apple-silicon)

the issue here is described in homebrew issues like
https://github.com/Homebrew/brew/issues/13481 and
https://github.com/orgs/Homebrew/discussions/868. essentially, homebrew
on intel macs puts files in /usr/local which is then found by other
software by default while on arm macs it uses /opt/homebrew meaning we
have to set additional flags for things like C compilers to find headers
and libraries installed through homebrew. there was a little discussion
in https://github.com/Homebrew/brew/issues/13481 of having homebrew
fixup environment variables like `CFLAGS` by default on ARM systems, but
the issue was closed ☹️

in the meantime, this PR should fix things for certbot devs and removes
the need for the ~/lib symlinks with both new and old versions of
python-augeas
2025-05-12 11:26:40 -07:00
Brad Warren
fde359f4da fixup http01_example.py (#10285)
it looks like https://github.com/certbot/certbot/pull/10098 introduced a
couple bugs into this file:

1.
[RSAPrivateKeys](https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey)
don't have a `public_bytes` method
2. `cryptography.x509` wasn't imported and
[load_pem_x509_certificate](https://cryptography.io/en/latest/x509/reference/#cryptography.x509.load_pem_x509_certificate)
takes bytes, not a string

i think avoiding this is unfortunately difficult as this file has no
tests, but it was useful for me just now when testing
https://github.com/certbot/certbot/pull/10283 so i wanted to fix it up

i also changed the script to initially create the account without an
email address as the fake@example.com email causes registration with
LE's staging server to fail early in execution

with the changes in this PR changes, if you:

1. change the value of
[DOMAIN](0075104805/acme/examples/http01_example.py (L57))
to a domain pointing at your machine
2. as root, activate your certbot dev environment, and run `python
acme/examples/http01_example.py `

it will fail late in the script with:
```
Traceback (most recent call last):
  File "/home/brad/certbot/acme/examples/http01_example.py", line 237, in <module>
    example_http()
    ~~~~~~~~~~~~^^
  File "/home/brad/certbot/acme/examples/http01_example.py", line 223, in example_http
    regr = client_acme.update_registration(
        regr.update(
    ...<3 lines>...
        )
    )
  File "/home/brad/certbot/acme/src/acme/client.py", line 101, in update_registration
    updated_regr = self._send_recv_regr(regr, body=body)
  File "/home/brad/certbot/acme/src/acme/client.py", line 373, in _send_recv_regr
    response = self._post(regr.uri, body)
  File "/home/brad/certbot/acme/src/acme/client.py", line 392, in _post
    return self.net.post(*args, **kwargs)
           ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/home/brad/certbot/acme/src/acme/client.py", line 766, in post
    return self._post_once(*args, **kwargs)
           ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/home/brad/certbot/acme/src/acme/client.py", line 781, in _post_once
    response = self._check_response(response, content_type=content_type)
  File "/home/brad/certbot/acme/src/acme/client.py", line 630, in _check_response
    raise messages.Error.from_json(jobj)
acme.messages.Error: urn:ietf:params:acme:error:invalidContact :: The provided contact URI was invalid :: Unable to update account :: invalid contact: contact email has forbidden domain "example.org"
```
if you also change [this email
variable](0075104805/acme/examples/http01_example.py (L223))
to a valid email address, the script will run successfully
2025-05-08 15:43:37 -07:00
Brad Warren
10747555ae upgrade python-augeas (#10286)
a couple weeks ago, [python-augeas
1.2.0](https://pypi.org/project/python-augeas/#history) was uploaded to
pypi. unfortunately, this broke things for us

the first major change was from
https://github.com/hercules-team/python-augeas/pull/49 where
python-augeas now needs the new OS packages described in the initial
comment there

the second change was from
https://github.com/hercules-team/python-augeas/pull/51 which added a
python interface to augeas functions that weren't introduced until
[augeas
1.13.0](af2aa88ab3/NEWS (L65-L66)).
this isn't ideal, but i don't think it's a big deal for us. augeas
1.13.0 is over three years old and [ubuntu
20.04](https://ubuntu.com/blog/ubuntu-20-04-eol-for-devicesional) and
[debian bullseye](https://www.debian.org/releases/) which have older
versions than that are technically EOL'd

regardless of how we feel about these changes, our tests don't currently
work with an updated version of python-augeas and this PR fixes it. i'm
also tracking https://github.com/certbot/certbot/issues/10282 to update
certbot.eff.org to list the newly required OS packages
2025-05-08 13:03:31 -07:00
Will Greenberg
0fc755fe08 Fix 10260 (#10283)
Builds off of https://github.com/certbot/certbot/pull/7066 to stringify
these validation errors

Fixes #10260

---------

Co-authored-by: George Daramouskas <gdaramouskas@therp.nl>
Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-05-08 09:13:04 -07:00
Jacob Hoffman-Andrews
dcdfdacf75 store preferred/required_profile in renewal config (#10280)
This ensures that renewals of certificates will use the same profile
settings.

Fixes #10271
2025-05-07 16:32:48 -07:00
Jacob Hoffman-Andrews
0075104805 acme.ClientNetwork: JWK becomes optional (#10275)
This results in a ClientNetwork that can .get() but not .post(). Useful
for fetching ARI, which does not require authentication.
2025-05-06 12:34:50 -07:00
Alexis
2cf6cda1fa [REPO] Update SECURITY.md (#10253)
Add supported versions

---------

Signed-off-by: zoracon <zoracon@pm.me>
2025-05-06 10:57:50 -07:00
Brad Warren
6418ee32e5 upgrade certbot compatibility test images (#10277)
i need this for some other upgrades i'm working on. using these debian
buster images which were [EOL'd ages
ago](https://www.debian.org/releases/) is giving me problems

while i was at it, i fixed up up the following warnings docker was
printing at me:

*
https://docs.docker.com/reference/build-checks/legacy-key-value-format/
* https://docs.docker.com/reference/build-checks/maintainer-deprecated/
2025-05-06 10:57:10 -07:00
Brad Warren
cc08242abc update pinned dependencies (#10278)
this fixes the security alerts those with access can see at
https://github.com/certbot/certbot/security/dependabot

i based what needed to be done to drop python < 3.9.2 support on
https://github.com/certbot/certbot/pull/10077 and concluded we only
really needed to update `python_requires`. we could do a deprecation
period for this, but i think it's not necessary. cryptography didn't
(it's not even in mentioned in [their
changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst))
and none of the major LTS distros use python 3.9.0 or 3.9.1
2025-05-06 10:56:19 -07:00
ohemorange
62361dac44 Catch and ignore orderNotReady response when posting a request to begin finalization and poll until ready (#10239)
Fixes #9766.
2025-05-02 12:08:46 -07:00
Brad Warren
5dcfd32a11 remove unneeded cryptography req (#10276)
since https://github.com/certbot/certbot/pull/10130, we depend on much
newer versions of cryptography so this restraint is no longer needed
2025-04-30 11:47:35 -07:00
ohemorange
6ac951e146 Deprecate TLS-ALPN support in ACME (#10268)
Fixes #10266.

See example deprecation in
https://github.com/certbot/josepy/pull/207/files

I can add stacklevel=2, though I find that usually I just look at the
whole stack anyway when debugging, myself, so it doesn't really matter.
2025-04-28 15:09:15 -07:00
ohemorange
508fba1da6 Why do we have this? It means warnings aren't filtered in acme tests (#10267)
This was added in https://github.com/certbot/certbot/pull/6091 to make
tests pass in EPEL and older ubuntus, 7 years ago. It is probably no
longer needed.

| pytest version | min. Python version |
|---------------|---------------------|
|8.0+ | 3.8+|
| 7.1+ | 3.7+ |
| 6.2 - 7.0 | 3.6+ |
| 5.0 - 6.1 | 3.5+ |
| 3.3 - 4.6 | 2.7, 3.4+ |

That version is [no longer
supported](https://docs.pytest.org/en/stable/backwards-compatibility.html).

Probably therefore we can just get rid of this.
2025-04-25 20:53:31 +00:00
ohemorange
2da39317b2 Replace pyparsing error that usually misdirects people with a more helpful message (#10265)
Addresses #10264, though I could not actually find a way to fix that
particular issue. So, fixes #10264 is not actually accurate, but I would
like github to link them.
2025-04-25 13:11:28 -07:00
ohemorange
16f858547f Add --use-pep517 flag to pip to silence warning in tools/venv.py, and switch codebase to src-layout (#10249)
Fixes #10252.

See further discussion here: https://github.com/pypa/pip/issues/11457

We are doing option:

> Alternatively, enable the --use-pep517 pip option, possibly with
--no-build-isolation. The --use-pip517 flag will force pip to use the
modern mechanism for editable installs. --no-build-isolation may be
needed if your project has build-time requirements beyond setuptools and
wheel. By passing this flag, you are responsible for making sure your
environment already has the required dependencies to build your package.
Once the legacy mechanism is removed, --use-pep517 will have no effect
and will essentially be enabled by default in this context.

Major changes made here include:
- Add `--use-pep517` to use the modern mechanism, which will be the only
mechanism in future pip releases
- Change to `/src` layout to appease mypy, and because for editable
installs that really is the normal way these days.
  - `cd acme && mkdir src && mv acme src/` etc.
- add `where='src'` argument to `find_packages` and add
`package_dir={'': 'src'},` in `setup.py`s
  - update `MANIFEST.in` files with new path locations 
- Update our many hardcoded filepaths
- Update `importlib-metadata` requirement to fix
double-plugin-entry-point problem in oldest tests
2025-04-11 19:30:33 +00:00
mirchicap
6de7570af0 Add certbot-dns-cdmon to third-party plugins list (#10258)
This PR adds `certbot-dns-cdmon` to the list of third-party plugins in
the documentation.

`certbot-dns-cdmon` enables DNS-01 challenge automation for domains
managed with cdmon's DNS.

PyPI: https://pypi.org/project/certbot-dns-cdmon/
2025-04-10 14:38:02 -07:00
Brad Warren
9bc9e3412e Merge pull request #10261 from certbot/candidate-4.0.0 2025-04-08 10:25:34 -07:00
Will Greenberg
f822602fff Bump version to 4.1.0 2025-04-07 15:04:01 -07:00
Will Greenberg
52ad3e80bd Add contents to certbot/CHANGELOG.md for next version 2025-04-07 15:04:01 -07:00
Will Greenberg
d9e3d7b2d2 Remove built packages from git 2025-04-07 15:04:01 -07:00
Will Greenberg
d95a389c3f Release 4.0.0 2025-04-07 15:04:00 -07:00
Will Greenberg
f7a0df3461 Update changelog for 4.0.0 release 2025-04-07 15:03:33 -07:00
Jonathan Vanasco
cb5d579a84 cloudtest some warnings (#10255) 2025-04-03 14:38:54 -07:00
Jacob Hoffman-Andrews
45626e88e2 Add --preferred-profile and --required-profile (#10230)
Fixes #10194
2025-04-03 06:48:52 +09:00
Will Greenberg
15024aabd3 Repin dependencies for josepy 2.0 (#10254) 2025-04-02 18:17:44 +00:00
Jonathan Vanasco
dd876a40ed Feature acme cryptography 2 (#10245)
redoing https://github.com/certbot/certbot/pull/10174 but lots of
mergecommits and ff wanted; so test in a clean environment
2025-04-02 10:53:47 -07:00
ohemorange
7a90cdd231 Remove custom version of CertificateIssuerPrivateKeyTypes now that cryptography version is minimum 43 (#10236)
Fixes https://github.com/certbot/certbot/issues/10233

This was a stand-in for
`cryptography.hazmat.primitives.asymmetric.types.CertificateIssuerPrivateKeyTypes`,
which we can now use directly.
2025-03-28 11:56:02 -07:00
ohemorange
7d461a8dfc Add template for code maintenance task (#10251)
We need this to create issues to track work like "update venv.py to
address upcoming pip build system deprecation" since we no longer have a
blank issue template.
2025-03-28 16:41:50 +00:00
Brad Warren
83510c1da5 use type feature instead of label (#10247)
not doing this breaks our triage link which currently looks for
unlabeled issues
2025-03-27 21:36:16 +00:00
Alexis
0b51653def [REPO] Add New Feature Template (#10238)
Adding for cases when it's not a bug, but a feature request. Helping
those and us frame the initial request better.
2025-03-17 18:07:32 -07:00
ohemorange
aa005f20fe Add RewriteEngine on directive also in post (#10232)
Fixes
https://github.com/certbot/certbot/issues/9835#issuecomment-2717096178,
where our `RewriteEngine on` directive inserted at the beginning of a
virtualhost was overridden a `RewriteEngine Off` directive later. This
PR does the easy thing of placing `RewriteEngine on` in our
post-insert.
2025-03-17 20:02:10 +00:00
ohemorange
8a6138856f Escape <TAG> in docker readme file (#10235)
Fixes https://github.com/certbot/certbot/issues/10229
2025-03-17 08:48:25 -07:00
ohemorange
7322e56cc7 Use updated name TLS_METHOD instead of SSLv23_METHOD (#10237)
Fixes #10231
2025-03-14 10:38:02 -07:00
Artur Corrêa Souza
df9075e023 fix(route53): explanation on credentials file (#9907)
The credentials configuration file is at ~/.aws/credentials.

Also, when running on root it uses the root home (so /root/.aws). This
was from my test at an ubuntu server.

## Pull Request Checklist

- [ ] The Certbot team has recently expressed interest in reviewing a PR
for this. If not, this PR may be closed due our limited resources and
need to prioritize how we spend them.
- [ ] If the change being made is to a [distributed
component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout),
edit the `master` section of `certbot/CHANGELOG.md` to include a
description of the change being made.
- [ ] Add or update any documentation as needed to support the changes
in this PR.
- [ ] Include your name in `AUTHORS.md` if you like.
2025-03-13 13:23:57 -07:00
Jacob Hoffman-Andrews
d91e552491 renewal: by default, use a fraction of lifetime (#10207)
Previously we defaulted to renewing at 30 days before expiry, and
allowed users to customize the config file to set a different value.

Instead, we should renew when 1/3 of the lifetime is left, or for
shorter certificates (<10 days), when 1/2 of the lifetime is left.

This still allows explicitly configured values to take precedence.

---------

Co-authored-by: Will Greenberg <ifnspifn@gmail.com>
2025-03-12 10:35:59 -07:00
Will Greenberg
b3bd4304f4 Merge pull request #10228 from certbot/candidate-3.3.0
Candidate 3.3.0
2025-03-11 09:54:07 -07:00
Brad Warren
37f6f8a12c Bump version to 4.0.0 2025-03-11 08:04:08 -07:00
Brad Warren
be60ad5131 Add contents to certbot/CHANGELOG.md for next version 2025-03-11 08:04:08 -07:00
Brad Warren
16cbd6c00a Remove built packages from git 2025-03-11 08:04:08 -07:00
Brad Warren
3b19e18641 Release 3.3.0 2025-03-11 08:04:07 -07:00
Brad Warren
259afd7458 Update changelog for 3.3.0 release 2025-03-11 08:03:28 -07:00
Brad Warren
b7e09dd652 change changelog to 3.3.0 (#10226)
i tested it and the version numbers in setup.py files will be changed
automatically by the release script
2025-03-10 12:08:29 -07:00
Jacob Hoffman-Andrews
8487bfeaa5 Update contributing.rst (#10203)
Give better instructions on running all unittests, and on running
specific test cases.

Replace `python` with `python3` in venv setup invocations because some
systems don't have a plain `python` command.

---------

Co-authored-by: ohemorange <ebportnoy@gmail.com>
2025-03-08 00:21:44 +00:00
Jacob Hoffman-Andrews
c323af7be9 Remove warnings about empty email. (#10214)
We strongly encouraged providing an email address because we wanted
people to get expiration notices to ensure that even if their Certbot
install broke, they could fix it before their site goes down.

Now that Let's Encrypt is getting rid of expiration notices
(https://letsencrypt.org/2025/01/22/ending-expiration-emails/), we can
remove some of the encouragement, providing a smoother user experience.
2025-03-06 14:38:52 -08:00
Brad Warren
2a92e22332 make integration tests crossplatform (#10217)
i wanted this for testing
https://github.com/certbot/certbot/issues/10190

alex started working on this in
https://github.com/certbot/certbot/pull/9207 years ago, but pebble
didn't end up doing a release containing his work while he was still
regularly contributing to certbot. this has now changed though

before this PR, our integration tests only worked on amd64 linux
systems. with this PR, i've successfully run our integration tests on
all combinations of the architectures amd64 and arm64 and the OSes linux
and macos

---------

Co-authored-by: ohemorange <erica@eff.org>
2025-03-06 14:13:26 -08:00
Brad Warren
48ffe02ce3 fix openssl env vars (#10223)
this should fix https://github.com/certbot/certbot/issues/10190

i unfortunately wasn't able to reproduce the nginx problem, but i did
audit all subprocess calls in certbot and all of its plugins and
confirmed that they use this `env_no_snap_for_external_calls` function.
with `OPENSSL_MODULES` no longer pointing inside the snap, this problem
should go away

i also cleared `OPENSSL_FORCE_FIPS_MODE` because of the report
[here](https://community.letsencrypt.org/t/certbot-snap-error-while-renewing-openssl-init-ssl/232145/16)
2025-03-06 13:21:33 -08:00
Will Greenberg
a43fdedd12 Merge pull request #10224 from certbot/bmw-patch-1
use pull_request_target
2025-03-05 11:13:47 -08:00
Brad Warren
4cffcbffaa use-pr-target 2025-03-05 08:19:10 -08:00
Alexis
65c33488dd [DOCS] Update CONTRIBUTING.md (#10220)
Just adding consistent language for the contributing guide.
2025-03-04 19:01:39 -08:00
Jacob Hoffman-Andrews
487dd53103 tests: remove RSA-256 key. (#10208)
This is ludicrously short and was only used by some key-mismatch tests.
We have plenty of other mismatched keys.
2025-02-25 18:03:16 -08:00
Brad Warren
d3d0b76f9f update centos9 ami (#10210)
tests on this passed at
https://dev.azure.com/certbot/certbot/_build/results?buildId=8790&view=results
2025-02-25 15:46:28 -08:00
ohemorange
52ee2a5e8b Improve instructions for updating github releases token (#10201)
The token is now owned by the team account and can simply be
regenerated, so we don't need the info about perhaps doing that. Plus,
there are now more clear instructions on the wiki. And the date was
updated.
2025-02-25 14:27:40 -08:00
ohemorange
a6bed18f0b Use ubuntu 24 to build docker (#10202)
Maybe addresses https://github.com/certbot/certbot/issues/10020

#10020 claims that without the verbose settings, build fail almost every
time. In my one (1) test removing verbosity, it passed, so idk. BUT! It
took [56
minutes](https://dev.azure.com/certbot/certbot/_build/results?buildId=8766&view=logs&j=fdd3565a-f3c6-5154-eca9-9ae03666f7bd&t=5dbd9851-46a4-524f-73a8-4028241afcde)
instead of [37 on ubuntu
24](https://dev.azure.com/certbot/certbot/_build/results?buildId=8768&view=logs&j=fdd3565a-f3c6-5154-eca9-9ae03666f7bd&t=5dbd9851-46a4-524f-73a8-4028241afcde).
Whether or not this actually fixes the underlying problem (still looking
into that), that seems worthwhile to speed up.
2025-02-24 19:22:07 -08:00
Jacob Hoffman-Andrews
792a76569d acme: add support for profiles (#10196)
Recognizes the profiles map in the "meta" section of directory.

Allows sending a "profile" field in order objects.

Adds an optional "profile" parameter to new_order in client.py.

Related to #10194.
2025-02-24 19:18:51 -08:00
ohemorange
9105cd21ba Update url for moved image to fix readthedocs build (#10200)
https://readthedocs.org/projects/eff-certbot/builds/27298576/ was
failing with `WARNING: Could not fetch remote image:
https://raw.githubusercontent.com/EFForg/design/master/logos/eff-certbot-lockup.png
[404]`

This is because the file was moved to
https://raw.githubusercontent.com/EFForg/design/master/logos/certbot/eff-certbot-lockup.png
as you can see here:
https://github.com/EFForg/design/tree/master/logos/certbot
2025-02-24 11:17:03 -08:00
ohemorange
6fd6a541d4 Remove internal comments from server_names directive (#10147)
Fixes https://github.com/certbot/certbot/issues/7090
2025-02-24 10:54:58 -08:00
Jonathan Vanasco
cda56361ad Fix deprecate pyopenssl (#10186)
Uploading for tests;

These deprecations are a precursor to #10174 

In addition to the previously discussed `acme` functions, the `certbot`
functions were deprecated as they are primarily used for testing and
support. Marking them deprecated now will allow them to be removed in
the next major release, as they will no-longer be used.
2025-02-24 10:54:17 -08:00
Alexis
d3aceba188 [TOOLING] Add Automation for When a PR or Issue is Assigned (#10191)
Adding automation for team triage meetings for when PRs or Issues are
assigned. You can see an example in the "Test" channel.

---------

Co-authored-by: ohemorange <erica@eff.org>
2025-02-14 14:58:00 -08:00
Alexis
8524255a2e [REPO] Create New Issue Template with Updated Strcuture (#10172)
Playing around with the new [issue template
structure](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms).

If you'd like to test it out:
https://github.com/zoracon/zoracon/issues/new?template=bug.yml

---------

Co-authored-by: ohemorange <ebportnoy@gmail.com>
Co-authored-by: ohemorange <erica@eff.org>
2025-02-14 14:50:20 -08:00
Osiris Inferi
a32fc2d6f0 Example explanation for standalone authenticator in cli.ini mentions incorrect port (#9951)
The standalone authenticator doesn't use port 443, so the comment in the
`cli.ini` example also shouldn't say so.
2025-02-13 14:26:06 -08:00
ohemorange
a24f14d48f Be consistent with checking capitalization in rfc2136 plugin (#10188)
Fixes https://github.com/certbot/certbot/issues/10177.

We were using `.upper()` when validating the config but not when
actually creating the object. Now we call it in both places. I updated a
test to work as a regression test here.
2025-02-12 21:42:00 -08:00
ohemorange
3ab9bf9f39 Release 3.2.0 (#10187)
This should be merged, not squashed!
2025-02-11 15:32:51 -08:00
Will Greenberg
4708012b2d bump changelog to 4.0.0 2025-02-11 15:18:54 -08:00
Will Greenberg
8a2ded2d0e psych, actually bump version to 4.0.0 2025-02-11 15:17:56 -08:00
Will Greenberg
77594d7300 Bump version to 3.3.0 2025-02-11 11:58:46 -08:00
Will Greenberg
fa9cd61066 Add contents to certbot/CHANGELOG.md for next version 2025-02-11 11:58:46 -08:00
Will Greenberg
a6d8c467f0 Remove built packages from git 2025-02-11 11:58:45 -08:00
Will Greenberg
a0e8b49057 Release 3.2.0 2025-02-11 11:58:44 -08:00
Will Greenberg
3272dcbebd Update changelog for 3.2.0 release 2025-02-11 11:57:39 -08:00
Ted Strzalkowski
3395dce319 Add ClouDNS to documentation (#9879)
There is a ClouDNS plugin actively maintained that should be included in
the documentation for discoverability by users.
2025-02-11 10:56:58 -08:00
Trinopoty Biswas
ad29d5fbcf Remove support for Linode API v3 (#10062)
The v3 api was sunset at the end of July 2023. This PR cleans up code
related to api v3.

## Pull Request Checklist

- [ ] The Certbot team has recently expressed interest in reviewing a PR
for this. If not, this PR may be closed due our limited resources and
need to prioritize how we spend them.
- [ ] If the change being made is to a [distributed
component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout),
edit the `main` section of `certbot/CHANGELOG.md` to include a
description of the change being made.
- [ ] Add or update any documentation as needed to support the changes
in this PR.
- [ ] Include your name in `AUTHORS.md` if you like.
2025-02-11 10:25:28 -08:00
Will Greenberg
de48847af4 Require v2.19 of cloudflare's python library (#10182)
This is a stopgap measure until we upgrade to the newer (but
backwards-incompatible) versions of cloudflare's python library (see
#9938)

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-02-06 19:29:07 +00:00
ohemorange
a57b29a276 [apache] Add type hints to apache configurator for mypy --strict (#10155)
I don't love the `Any` in that `Callable`, but I can't find a way to fix
it. In practice, it's either going to be `str` or `None`, but we pass an
`options` that's typed as `List[str] | str | None`, and one of the
functions has a header with a strict `str`. I tried various unions of
things and it wasn't working and I decided it's not worth it.

```
$ mypy --strict certbot-apache/certbot_apache/_internal/configurator.py 
Success: no issues found in 1 source file
```
2025-02-06 10:54:10 -08:00
ohemorange
9989e7b82f Remove unneeded casts now that Addr.fromstring can't be None (#10167)
Now that https://github.com/certbot/certbot/pull/10162 is in
2025-02-06 10:53:22 -08:00
ohemorange
aafe7ba2f9 Update to latest poetry-plugin-export to fix urllib3 multiple versions issue and unpin urllib3 (#10169)
We can use modern urllib3 now! They
[fixed](https://github.com/python-poetry/poetry-plugin-export/pull/286)
the poetry
[issue](https://github.com/python-poetry/poetry-plugin-export/issues/183)
and shipped it!

Since this PR updates the requirements file, it pulls in the new
`josepy` release, so I've silenced those warnings here. If I should do
that in a separate PR lmk.
2025-02-05 17:14:25 -08:00
Brad Warren
87e5dcbc83 update ocsp api (#10181)
this does the simple and more urgent fix described in
https://github.com/certbot/certbot/issues/10053. i created
https://github.com/certbot/certbot/issues/10180 to track fixing up our
tests to generally help prevent this kind of problem in the future
2025-02-05 14:08:14 -08:00
Brad Warren
2535a7bb29 fix mypy on 3.9 (#10179)
in
https://github.com/certbot/certbot/pull/10160/files#diff-f14c5058a01d9c9f8e0215ae6378c1d99fcfe2dd71d5cea207a7d610b31c149bR123
we used the nice & clean `|` syntax for unions but this caused tests to
fail for me locally where i was using python 3.9. this fixes that using
the same approach as was used in
https://github.com/certbot/certbot/pull/10178

i think we want this regardless, but by the same logic as
a00e343459,
i think we could consider deprecating & dropping python 3.9 support
early
2025-02-05 12:15:27 -08:00
ohemorange
e32f4fc5fb Allow http block inclusion at top-level nginx.conf (#10178)
Fixes #9928.

Thanks to @vanviethieuanh for tracking down the source of the issue!
2025-02-04 11:15:17 -08:00
ohemorange
b5f7fe179e Avoid trailing dot on link problem (#10173)
Fixes https://github.com/certbot/certbot/issues/9964.

Decided not to be fancy and just throw a newline in there.

```
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at:
https://letsencrypt.org/documents/LE-SA-v1.4-April-3-2024.pdf
You must agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: 
```
2025-02-03 08:50:18 -08:00
Alexis
2ae7f83e2a [REPO] Modify Stalebot Labels for Better Filters (#10171)
- Better labels upon an issue going stale will help triage better. There
other PRs with "needs update" that are manually put and therefore we
can't explicitly filter for stalebot.
- For management purposes, being able to view how many issues are
auto-closed helps as well.
2025-01-31 15:23:10 -08:00
ohemorange
935855b751 Enable strict mypy for certbot-ci/certbot_integration_tests (#10168)
Co-authored-by: Brad Warren <bmw@eff.org>
2025-01-31 02:32:27 +00:00
ohemorange
0dedef801a Run mypy with --strict on modules that are ready for it (#10166) 2025-01-30 23:11:31 +00:00
ohemorange
d5cb89ba13 [apache] Add type hints to apache test utils (to enable mypy --strict) (#10151)
We could also leave the `jose.JWKRSA` call as-is and add
`--implicit-reexport`, or explicitly export `JWKRSA` in `josepy`, but I
think just cleaning the calls up is nice.

```
$ mypy --strict certbot-apache/certbot_apache/_internal/tests/util.py
Success: no issues found in 1 source file
```
2025-01-29 15:10:31 -08:00
ohemorange
04f3072399 Ignore sublime project files (#10165) 2025-01-29 13:26:44 -08:00
Brad Warren
392467609f add python 3.13 support (#10164)
fixes https://github.com/certbot/certbot/issues/10045. this is based on
the PR that did this for 3.12 at
https://github.com/certbot/certbot/pull/9852

this PR also removes python 3.8 from our tox config which should have
been done as part of https://github.com/certbot/certbot/pull/10077
2025-01-29 12:53:56 -08:00
ohemorange
7661dbaf82 Enable strict typing in certbot-dns-google (#10160)
```
$ mypy --strict certbot-dns-google/certbot_dns_google
Success: no issues found in 5 source files
```


I can change the `setattr`s to `# type: ignore [method-assign,
unused-ignore]` if that's preferred.
2025-01-28 16:53:38 -08:00
ohemorange
014e554f70 Enable strict typing in certbot-dns-cloudflare (#10157)
```
$ mypy --strict certbot-dns-cloudflare/certbot_dns_cloudflare
Success: no issues found in 5 source files
```

If we do `type: ignore` but don't set `--strict`, mypy gets mad. Flake8
doesn't like this but luckily we don't use that here (yet?). The other
option is to add `# type: ignore [method-assign, unused-ignore]`; I can
change it to that if that's preferred.
2025-01-28 16:46:18 -08:00
ohemorange
f5bfe543ff Enable strict typing in certbot-dns-rfc2136 (#10159)
```
$ mypy --strict certbot-dns-rfc2136/certbot_dns_rfc2136
Success: no issues found in 5 source files
```

`dnspython` would be perfectly happy to accept a string once the
algorithm is passed through, but our `_RFC2136Client` object will only
accept `dns.name.Name` objects, so let's make it happy.
2025-01-28 16:43:28 -08:00
ohemorange
e8cc2df316 [nginx] Add type hints to nginx configurator for mypy --strict (#10156)
Decided that imports should be in ascii, not caseless, order. If we've
done otherwise elsewhere I'll change it.

The cast isn't the best but that's what we get for using a dict holding
various objects -- the thing marked by `vhost` is going to be a
virtualhost so 🤷

`re.findall` can
[return](https://docs.python.org/3/library/re.html#re.findall) either a
list of strings or a list of tuples of strings, depending what you
specify in the regex. In our case it's going to be a list of strings.
2025-01-28 16:42:08 -08:00
ohemorange
f94c981dfd [apache] Add type hints to apache obj and http_01 for mypy --strict (#10154)
```
$ mypy --strict certbot-apache/certbot_apache/_internal/http_01.py 
Success: no issues found in 1 source file
$ mypy --strict certbot-apache/certbot_apache/_internal/obj.py 
Success: no issues found in 1 source file
```

PEP 526 says to declare types of unpacked tuples beforehand:
https://peps.python.org/pep-0526/#global-and-local-variable-annotations.
Could have just declared it in apache, but improved the acme return type
while I was at it.

Once again, `typing.Pattern` is deprecated in favor of `re.Pattern` so
changing that while parametrizing the type
2025-01-28 16:33:12 -08:00
Brad Warren
f0f3cdad9c fixup fromstring return types (#10162)
in https://github.com/certbot/certbot/pull/9124 we had the problem of
certbot-nginx's `Addr.fromstring` method possibly returning None which
is not possible in the `Addr` method in the certbot base class or in
certbot-apache. we fixed this by telling mypy the common
`Addr.fromstring` method returns an `Optional[Addr]` (despite it
actually always returning an `Addr`) and then unnecessarily complicating
certbot-apache's code a bit. the need for extra complexity with this
approach is going even further in
https://github.com/certbot/certbot/pull/10151 where we have to use
`cast` to assure mypy that the type isn't actually `Optional`. i
personally don't like all this
2025-01-28 16:29:52 -08:00
ohemorange
6f46e1be15 Improve help output for default-None constants (#10149)
Fixes #10000.

To create this PR, I looked through `constants.py` for defaults set to
`None`. If the action for the cli flag was `store_true` and there wasn't
other custom manual default specification, I changed it to report
`False`, and added a comment in `constants.py`. Adding `(default:` in
the help text suppresses listing of the actual default (done by
`cli_utils.py:CustomHelpFormatter`). Also added a comment for `redirect`
which is described manually since I noticed it while I was going
through.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-01-28 23:41:02 +00:00
ohemorange
ec3330ee0c Enable strict typing in certbot-dns-digitalocean (#10158)
If we do `type: ignore` but don't set `--strict`, mypy gets mad. Flake8
doesn't like this but luckily we don't use that here (yet?). The other
option is to add `# type: ignore [method-assign, unused-ignore]`; I can
change it to that if that's preferred.

```
$ mypy --strict certbot-dns-digitalocean/certbot_dns_digitalocean
Success: no issues found in 5 source files
```
2025-01-28 11:04:08 -08:00
ohemorange
e657cc3a8d [apache] Add type hints to apache parser.py (to enable mypy --strict) (#10152)
```
$ mypy --strict certbot-apache/certbot_apache/_internal/parser.py 
Success: no issues found in 1 source file
```

`typing.Pattern` is deprecated in python 3.9 in favor of using
`re.Pattern` directly, and also wants to be subscripted with its type.

`python-augeas` types can be found in
a1e84a7e58/augeas/__init__.py
2025-01-28 10:54:11 -08:00
ohemorange
a46db66371 Enable strict typing in certbot-dns-route53 (#10161)
Using the `ignore` syntax this time!

```
$ mypy --strict certbot-dns-route53/certbot_dns_route53
Success: no issues found in 5 source files
```
2025-01-28 10:53:27 -08:00
ohemorange
d98edd97ad Preserve IP addresses when adding ssl listen directives in nginx server blocks (#10145)
Fixes #10011

When we take a server block with no ssl addresses in and and enable ssl,
if it has any listens on the http port, use those host addresses when
creating a directive to listen on ssl. Addresses with no port and on
other ports will be ignored.

---------

Co-authored-by: Will Greenberg <willg@eff.org>
2025-01-28 10:52:06 -08:00
ohemorange
70ba4f2438 Run directory hooks during commands other than renew (#10146)
Partially fixes #9869. Fixes #9978.
2025-01-28 10:20:17 -08:00
ohemorange
9d049723c2 Enable mypy strict equality checking (#10150)
Fixes one impossible check, but that's it! Closes
https://github.com/certbot/certbot/issues/5649.
2025-01-27 14:55:57 -08:00
ohemorange
60b88a3b83 [apache] Add type hints to apache dualparser.py (to enable mypy --strict) (#10153)
`typing.Type` is deprecated in favor of built-in `type`. In strict
mode,`find_ancestors` needs to be more specific about what it actually
returns, due to covariance and generics and such.

```
$ mypy --strict certbot-apache/certbot_apache/_internal/dualparser.py 
Success: no issues found in 1 source file
```
2025-01-27 14:54:40 -08:00
ohemorange
e0e81a97f2 Add new style of issue template (#10143)
Pasted from the old one. Maybe we can just rename it but this is what
github's web interface led me to create.

I want to make sure that they at least create the template so that they
read it. If they then choose to ignore it that's fine, but it should
always pop up. Basically I want to keep the old behavior. Open to
alternatives.

We could also play around with the new issue forms:
https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms

Or label this one the "bug" template, and create a second one that is
blank but has the header text paragraph. I haven't seen a way to make
something appear in all templates, including the "blank" one, other than
just turning off blank templates.
2025-01-22 16:27:03 -08:00
Alex Gaynor
e050fe91a3 Allow using cryptography certs and keys in the standalone plugin (#10133)
Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-01-16 22:16:45 +00:00
Will Greenberg
ed972a130f Add gen_ss_cert deprecation to changelog (#10142)
Forgot a changelog note for this
2025-01-16 19:23:41 +00:00
Brad Warren
b411cddc8a fix private key format (#10134)
fixes https://github.com/certbot/certbot/issues/10131

this seems simple enough, but i also requested alex's review as a quick
sanity check if he doesn't mind providing one

i've verified this fixes the problem and that PKCS#8 was used in certbot
3.0.1
2025-01-16 11:04:55 -08:00
Brad Warren
40f0b91512 Fix readlink on windows (#10140)
fixes https://github.com/certbot/certbot/issues/10135

i did this by first reverting the bad changes from
https://github.com/certbot/certbot/pull/10077 and then fixing up
comments/documentation

it seems that the code comments
[here](https://github.com/certbot/certbot/blob/v3.0.1/certbot/certbot/compat/filesystem.py#L410-L411)
and in the unit tests that os.readlink always returns the extended form
in python 3.8+ was incorrect

significant credit for this work goes to
https://github.com/certbot/certbot/pull/10136 and
https://github.com/mbs-c for identifying the problem in the code here
2025-01-16 10:09:44 -08:00
Will Greenberg
7e87acee3c acme: deprecate gen_ss_cert in favor of make_self_signed_cert (#10097)
gen_ss_cert()'s signature contains deprecated pyOpenSSL API, so here we
deprecate it in favor of a new function that does the same thing, except
with only cryptography types: make_self_signed_cert
2025-01-16 11:38:10 +09:00
ohemorange
680729655e Honor --reuse-key when --allow-subset-of-names is set (#10138)
Fixes #10109. We were not previously doing so, and that was an
oversight. Adds regression tests in unit tests and integration tests.

Integration regression test failing without the fix is here:
https://dev.azure.com/certbot/certbot/_build/results?buildId=8463&view=logs&j=fca58cec-e7ce-563a-f36f-5c233894d750&t=8c19ffdb-5db1-573e-d81e-907ba1b3cfee
2025-01-16 01:41:57 +00:00
Brad Warren
94dcf25f6e notify about PRs from forks (#10101) 2025-01-15 17:19:25 -08:00
Brad Warren
96c4bcd9a8 double failing timeout (#10141)
our macOS tests recently started failing (more?) often hitting the
timeout modified in this PR. an example of this can be seen at
https://dev.azure.com/certbot/certbot/_build/results?buildId=8459&view=logs&j=1ae398a1-7dc9-5ade-0f59-912b32975b53&t=0ec28dfb-4593-5e04-05b6-bb502ec0a017&s=96ac2280-8cb4-5df5-99de-dd2da759617d.
this has affected at least
https://github.com/certbot/certbot/pull/10138,
https://github.com/certbot/certbot/pull/10130, and
https://github.com/certbot/certbot/pull/10136

i'm not sure whether the failing tests are actually getting stuck or
just hitting the timeout i bumped here, but i suspect it may be the
latter. our tests on macOS in CI are unreasonably slow for some reason.
i do not have this problem on my macbook locally

this PR does the simple/lazy thing of bumping the timeout which may help
avoid the now regularly occuring problem and/or help us get more
certainty whether the tests are actually getting stuck or not
2025-01-15 13:51:02 -08:00
Brad Warren
a00e343459 update policy on minimum dependency versions (#10130)
fixes #10105

this PR updates our minimally required cryptography and pyopenssl
versions as well as updating our policy for choosing minimum dependency
versions generally

before this PR, we were trying to keep compatibility with packages
available in EPEL 9 using the python 3 version available in RHEL 9.
after the discussion in #10105 we decided not to do this anymore
because:

* EPEL 9 may not want to update to certbot 3.0+ anyway because of our
backwards incompatible changes from certbot 2.x
* RHEL 9 appstream repos now contain newer versions of many of our
dependencies for newer versions of python
* alternate installation methods for RHEL 9 based users including our
snaps and pip are available

on a call we then discussed what distro repositories we should track
instead of EPEL 9. our docs previously said Debian sid/unstable, but we
felt this as unnecessary because Debian sid can and does change very
quickly. if we wanted a new dependency there, Debian could probably
accommodate it

we also considered RHEL 10 + EPEL 10, however, these repos are not even
stable yet and certbot and many of its dependencies are not yet packaged
there at all

for these reasons, plus many of the reasons we decided to upgrade past
EPEL 9 with the default python 3 version there, we decided that at least
for now, we will remove any linux distro considerations when choosing
minimal dependency versions of certbot

as i wrote in the contributing docs, we may choose to reconsider this
plan if there are requests for us to do so, but based on the information
above, we are not sure this will ever even happen and removing this
constraint significantly simplifies development of certbot
2025-01-15 09:47:40 -08:00
Alexis
86694397a6 Update notify_weekly.yaml (#10118)
Making the weekly message a little more useful.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2025-01-13 07:46:12 -08:00
ohemorange
b18c074088 Allow non-breaking spaces in nginx config files (#10126)
Fixes @josevavia's issue in #9942.
2025-01-10 15:25:05 -08:00
Brad Warren
f59a639ec4 improve repin experience on macOS (#10128)
this hopefully at least helps the problem hit at
https://github.com/certbot/certbot/pull/10126#discussion_r1909714276

i took this approach because in my experience, linux specific shell
commands have crept into our scripts repeatedly over the years so i
think just having macOS devs use the linux versions is much more
reliable. it's what i've personally been doing for years now
2025-01-10 12:54:54 -08:00
Brad Warren
5411e4c86a silence poetry warning (#10127)
when reviewing https://github.com/certbot/certbot/pull/10126 and running
`tools/pinning/oldest/repin.sh` using a freshly created dev environment,
i was repeatedly given the message

> The "poetry.dev-dependencies" section is deprecated and will be
removed in a future version. Use "poetry.group.dev.dependencies"
instead.

i believe this section was generated automatically by poetry's tooling
when it created the initial boilerplate file for us, but we don't use
it, so i just deleted the section which makes the warnings disappear
2025-01-10 12:52:24 -08:00
Brad Warren
0425b87b78 Merge pull request #10123 from certbot/candidate-3.1.0
Release Certbot 3.1.0
2025-01-07 15:53:09 -08:00
Erica Portnoy
1de966d637 Bump version to 3.2.0 2025-01-07 12:54:01 -08:00
Erica Portnoy
ba2e4aecb7 Add contents to certbot/CHANGELOG.md for next version 2025-01-07 12:54:01 -08:00
Erica Portnoy
7d2b1996d9 Remove built packages from git 2025-01-07 12:54:01 -08:00
Erica Portnoy
dcd52b0711 Release 3.1.0 2025-01-07 12:54:00 -08:00
Erica Portnoy
8074858620 Update changelog for 3.1.0 release 2025-01-07 12:53:36 -08:00
Brad Warren
d3d293299a minor acme doc & comment fixes (#10122)
this fixes two tiny things i noticed when reviewing
https://github.com/certbot/certbot/pull/10120

1. not all of our `acme` modules were generating API documentation
2. the deleted commend about a "type ignore" should have been deleted in
https://github.com/certbot/certbot/pull/9197 but will and i missed it
2025-01-07 18:17:00 +00:00
Alex Gaynor
9148acd332 Migrate verify_cert to take cryptography certificates (#10120) 2025-01-07 17:46:31 +00:00
Brad Warren
9f9a1df85e upgrade pylint (#10121)
we need this for https://github.com/certbot/certbot/issues/10045
2025-01-07 09:43:14 -08:00
ohemorange
985457e57b Add docstring for acme.crypto_util.get_names_from_subject_and_extensions (#10115)
It was my oversight to not request this when this function was made
public in https://github.com/certbot/certbot/pull/10111.
2025-01-06 14:37:29 -08:00
Alex Gaynor
4004589cbf Migrate certbot-compatibility-test to cryptography (as much as possible (#10117)
Also fixed a typing error.
2025-01-06 13:39:16 -08:00
Alex Gaynor
8f7c3756b3 Migrate get_serial_from_cert and valid_privkey to cryptography (#10116) 2025-01-06 13:34:57 -08:00
Alex Gaynor
6ea5da51e0 Simplify typing for a local variable (#10113)
`_DefaultCertSelection` _is_ a `Callable` of the appropriate signature.

Also fixed a mypy error I see locally, `TOKEN_SIZE` should be an
integer.
2025-01-06 13:18:28 -08:00
Alex Gaynor
1ac05ae891 Remove _pyopenssl_cert_or_req_san_ip which is unused, and migrate _pyopenssl_cert_or_req_all_names to cryptography (#10112)
Unfortunately the other helpers from this family are directly called by
(historic) versions of certbot, and so cannot be easily removed.
2025-01-06 12:46:23 -08:00
Manuel Baldassarri
a441debdaa Add Nginx Unit plugin to documentation (#10110)
## Pull Request Checklist

- [ ] The Certbot team has recently expressed interest in reviewing a PR
for this. If not, this PR may be closed due our limited resources and
need to prioritize how we spend them.
- [ ] If the change being made is to a [distributed
component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout),
edit the `main` section of `certbot/CHANGELOG.md` to include a
description of the change being made.
- [x] Add or update any documentation as needed to support the changes
in this PR.
- [ ] Include your name in `AUTHORS.md` if you like.
2025-01-06 12:37:31 -08:00
Alex Gaynor
5dd898f56b Move _get_names_from_subject_and_extensions to acme's crypto_utils (#10111)
Make use of it in more places
2025-01-03 16:21:31 -08:00
Alex Gaynor
a1fce6b398 Convert notBefore and notAfter to use cryptography's APIs (#10103) 2025-01-03 13:50:33 -08:00
Will Greenberg
635d9c3ec3 Merge pull request #10090 from alex/san-cryptography
Convert several SAN handling functions to use cryptography's APIs
2025-01-02 11:42:49 -08:00
Alex Gaynor
0f36d0c1ba Convert several SAN handling functions to use cryptography's APIs 2025-01-02 14:25:17 -05:00
Alex Gaynor
619da0432a Introduce a Format enum to help us migrate away from pyOpenSSL's constants
Begin using it in `dump_pyopenssl_chain`
2024-12-21 11:06:43 -05:00
Alex Gaynor
314838eb81 Convert some certbot-ci utilities to use cryptography's APIs (#10102) 2024-12-19 19:37:09 +00:00
Will Greenberg
25a1933e01 snap: disable FIPS detection (#10067)
This is needed because the Python + OpenSSL bundled in core24 don't
include an OpenSSL FIPS provider, which causes crashes on host systems
with OpenSSL 1.1.1f (e.g. Ubuntu Pro 20.04). For some reason, core24's
OpenSSL also looks in a non-standard location for the provider, which
also causes crashes on systems with OpenSSL 3.x (e.g. RHEL 9). If you
need FIPS functionality in certbot, install via pip.
2024-12-19 10:55:53 -08:00
Alex Gaynor
0f500e8010 Convert crypto_util_test.py to use cryptography's APIs (#10100) 2024-12-19 10:24:16 -08:00
Alex Gaynor
1afae838bb Convert validate_key_csr to use cryptography's APIs (#10099) 2024-12-19 07:11:47 -08:00
Alex Gaynor
724be8848a Convert http01_example.py to use cryptography's APIs (#10098)
Co-authored-by: ohemorange <ebportnoy@gmail.com>
2024-12-18 23:54:44 +00:00
Alex Gaynor
06ea141ca9 Convert make_key to use cryptography's APIs (#10091) 2024-12-18 15:10:20 -08:00
Mads Jensen
23245c07b2 Replace assert False with pytest.fail (#10094)
This seems to be better style. The assert False statements are
automatically removed by Python when running in the optimized mode,
which could hide test failures.

## Pull Request Checklist

- [ ] The Certbot team has recently expressed interest in reviewing a PR
for this. If not, this PR may be closed due our limited resources and
need to prioritize how we spend them.
- [ ] If the change being made is to a [distributed
component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout),
edit the `main` section of `certbot/CHANGELOG.md` to include a
description of the change being made.
- [ ] Add or update any documentation as needed to support the changes
in this PR.
- [ ] Include your name in `AUTHORS.md` if you like.

Co-authored-by: Mads Jensen <atombrella@users.noreply.github.com>
2024-12-18 14:44:05 -08:00
Will Greenberg
2d1d1cd534 Merge pull request #10089 from jvanasco/fix-migrate_to_cryptography
switch `cert_and_chain_from_fullchain` to cryptography APIs
2024-12-17 14:37:02 -08:00
Will Greenberg
5240e3cbf2 Merge pull request #10085 from atombrella/pyupgrade/up020_open_alias
Replace io.open with the built-in.
2024-12-17 14:35:08 -08:00
Will Greenberg
5fca4a14ab Merge pull request #10084 from atombrella/pyupgrade/up024_oserror
Replace aliased OSError.
2024-12-17 14:34:10 -08:00
Alex Gaynor
9be070414f Convert valid_csr and csr_matches_pubkey to use cryptography's APIs (#10088) 2024-12-17 09:22:22 -08:00
jonathan vanasco
761c268934 missed import level in port 2024-12-16 15:42:33 -05:00
jonathan vanasco
1fa110c9d7 added to authors 2024-12-16 15:30:42 -05:00
jonathan vanasco
9d1fccf53a switch cert_and_chain_from_fullchain to cryptography 2024-12-16 15:24:38 -05:00
Alex Gaynor
b16c64a05b Convert make_csr to use cryptography instead of pyOpenSSL (#10086)
These pyOpenSSL APIs are deprecated and we'd like to remove them.
2024-12-16 11:00:52 -08:00
Mads Jensen
88932da859 lint 2024-12-14 11:33:59 +01:00
Mads Jensen
8a69b2f1d9 Replace io.open with the built-in.
As of Python 3, io.open is an alias for the built-in open function.
2024-12-14 11:29:40 +01:00
Mads Jensen
57b5942fc3 Replace aliased OSError.
As of Python 3.3, various errors were merged into OSError.
https://docs.python.org/3/library/exceptions.html#OSError
2024-12-14 11:15:26 +01:00
Brad Warren
0f0000298b improve repinning (#10082)
this PR hopefully improves two things that i hit while working on #10035

1) i found that repinning our dependencies took ~6 minutes!

digging into it a bit, the biggest culprit i found was the inclusion of
`--no-cache-dir` here which seemed to cause poetry to redownload the
same packages over and over in a single run. this comes from
https://github.com/certbot/certbot/pull/9453 which fixed a problem i
(but not alex) was having with a major performance penalty. i removed
the flag here and instead included instructions on clearing poetry's
caches in case anyone ever hits this in the future. with this change,
the script now takes about 40 seconds on my laptop

2) every run of this script ended with the output:

    ```
Warning: poetry-plugin-export will not be installed by default in a
future version of Poetry.
In order to avoid a breaking change and make your automation
forward-compatible, please install poetry-plugin-export.
explicitly. See https://python-poetry.org/docs/plugins/#using-plugins
for details on how to install a plugin.
To disable this warning run 'poetry config warnings.export false'.
    ```

setting `POETRY_WARNINGS_EXPORT=false` fixes this which i believe is
safe to do because of
2c8609464c/certbot/setup.py (L53-L56)
2024-12-12 12:00:11 -08:00
Will Greenberg
c39fbe388c Merge pull request #10081 from certbot/no-windows-installer
remove the windows installer
2024-12-12 10:40:44 -08:00
Brad Warren
fc07f5f935 update pinnings 2024-12-12 08:57:10 -08:00
Brad Warren
9c8cdd05da remove the windows installer 2024-12-12 08:57:10 -08:00
Brad Warren
2c8609464c fix upgrading pyopenssl (#10080)
i hit this when working on https://github.com/certbot/certbot/pull/10076
where i found that updating all our dependencies no longer worked
because of new deprecations in pyopenssl. this pr fixes that
2024-12-11 15:15:55 -08:00
Brad Warren
7a48c235a9 remove importlib_resources (#10076)
this is part of my work on
https://github.com/certbot/certbot/issues/10035 based on erica's comment
at
https://github.com/certbot/certbot/issues/10035#issuecomment-2452212686
2024-12-06 12:37:17 -08:00
Mads Jensen
3f9387bd15 Fix F541 and E711 (#10071)
There are a quite a lot of imports that are unused.

F541 is Unnecessary f-interpolation without placeholders
E711 is incorrect use of == for boolean and None comparisons

## Pull Request Checklist

- [x] The Certbot team has recently expressed interest in reviewing a PR
for this. If not, this PR may be closed due our limited resources and
need to prioritize how we spend them.
- [ ] If the change being made is to a [distributed
component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout),
edit the `main` section of `certbot/CHANGELOG.md` to include a
description of the change being made.
- [ ] Add or update any documentation as needed to support the changes
in this PR.
- [x] Include your name in `AUTHORS.md` if you like.

---------

Co-authored-by: Mads Jensen <atombrella@users.noreply.github.com>
2024-12-05 11:33:09 -08:00
Brad Warren
087cb4d1f4 remove python 3.8 support (#10077)
fixes https://github.com/certbot/certbot/issues/10035. you can compare
this to the PR that did this for python 3.7 at
https://github.com/certbot/certbot/pull/9792

i agree with erica's comment at
https://github.com/certbot/certbot/issues/10035#issuecomment-2452212686,
but felt this PR was already getting pretty large so i did that in a
second PR at https://github.com/certbot/certbot/pull/10076
2024-12-04 14:55:20 -08:00
Brad Warren
bcbc3dd484 Merge pull request #10075 from certbot/test-no-setuptools
remove setuptools dependency
2024-12-03 13:58:03 -08:00
Brad Warren
89737718c1 update documentation and pinnings 2024-12-03 11:25:02 -08:00
Harlan Lieberman-Berg
b0e389aad7 Drop setuptools as a runtime dependency
Because of the change from using setuptools.pkg_resources to using
importlib, we no longer need a runtime dependency on setuptools. It is
still required, however, for running setup.py.
2024-12-03 11:17:27 -08:00
Brad Warren
9f5451d16b update intersphinx mapping (#10074)
this hopefully fixes our nightly failures

readthedocs seems to redirect users to its .io site so
https://acme-python.readthedocs.org/en/latest/objects.inv is supposed to
redirect people to
https://acme-python.readthedocs.io/en/latest/objects.inv, but that
doesn't always seem to work and instead [sometimes serves a
403](https://dev.azure.com/certbot/certbot/_build/results?buildId=8237&view=logs&j=d74e04fe-9740-597d-e9fa-1d0400037dfd&t=dde413a4-f24c-59a0-9684-e33d79f9aa02&l=800)

removing the need for this redirect seems to fix things based on some
quick testing and certainly shouldn't hurt
2024-12-03 11:16:13 -08:00
Will Greenberg
5ada20cb74 Merge pull request #10068 from certbot/test-include-ssl-provider
stage SSL lib and set OPENSSL_MODULES
2024-11-22 13:22:09 -08:00
Brad Warren
ba256adcdb add changelog entry 2024-11-22 11:23:00 -08:00
Brad Warren
94adff7247 stage ssl lib and set OPENSSL_MODULES 2024-11-22 11:00:56 -08:00
Will Greenberg
06d6231d6d Merge pull request #10060 from certbot/candidate-3.0.1
update main from 3.0.1 release
2024-11-18 13:38:23 -08:00
Brad Warren
61da18cc47 Merge branch 'main' into candidate-3.0.1 2024-11-18 13:25:09 -08:00
Brad Warren
7ab421233e remove old python macos cover tests (#10063)
Co-authored-by: Will Greenberg <willg@eff.org>
2024-11-18 19:18:46 +00:00
Brad Warren
59f32c9d11 update docker image (#10057) 2024-11-14 13:37:08 -08:00
Brad Warren
2b57c5f03c Bump version to 3.1.0 2024-11-14 09:22:05 -08:00
Brad Warren
8f75af1e84 Add contents to certbot/CHANGELOG.md for next version 2024-11-14 09:22:05 -08:00
Brad Warren
3c9b936168 Remove built packages from git 2024-11-14 09:22:04 -08:00
Brad Warren
93294fc989 Release 3.0.1 2024-11-14 09:22:03 -08:00
Brad Warren
58a07ddd79 Update changelog for 3.0.1 release 2024-11-14 09:21:24 -08:00
Will Greenberg
933c1703b6 Remove built packages from git
(cherry picked from commit 990352e371)
2024-11-14 09:21:04 -08:00
Brad Warren
a44d739dd7 silence warning and add test (#10054) (#10058)
fixes https://github.com/certbot/certbot/issues/9967

actually fixing the underlying issue is being tracked by https://github.com/certbot/certbot/issues/10053

in addition to the unit test, i tested this manually. if you obtain any cert and run `certbot renew` in an up-to-date dev environment, there's 3 warnings when on `main` and none on this branch

(cherry picked from commit aa6ea3b513)
2024-11-14 09:07:06 -08:00
Brad Warren
58374867c8 Merge pull request #10056 from certbot/prep-for-3.0.1
prep for 3.0.1
2024-11-13 20:40:33 -08:00
Brad Warren
aa6ea3b513 silence warning and add test (#10054)
fixes https://github.com/certbot/certbot/issues/9967

actually fixing the underlying issue is being tracked by https://github.com/certbot/certbot/issues/10053

in addition to the unit test, i tested this manually. if you obtain any cert and run `certbot renew` in an up-to-date dev environment, there's 3 warnings when on `main` and none on this branch
2024-11-13 20:39:50 -08:00
ohemorange
a25ef72c4f escape backslashes in format string in finish_release.py (#10043)
(cherry picked from commit 38fc7fcc48)
2024-11-13 10:34:32 -08:00
ohemorange
396b6cce02 Fix release script main replacement (#10042)
* restore incorrect regex changes to CHANGELOG.md

* Update _release.sh regex to switch only first instance of main in changelog

(cherry picked from commit 0e225dcba2)
2024-11-13 10:34:26 -08:00
ohemorange
38fc7fcc48 escape backslashes in format string in finish_release.py (#10043) 2024-11-05 23:42:59 +00:00
ohemorange
0e225dcba2 Fix release script main replacement (#10042)
* restore incorrect regex changes to CHANGELOG.md

* Update _release.sh regex to switch only first instance of main in changelog
2024-11-05 14:55:23 -08:00
Brad Warren
4ff5719a65 Merge pull request #10039 from certbot/candidate-3.0.0
Candidate 3.0.0
2024-11-05 12:52:47 -08:00
Will Greenberg
798a61622c Bump version to 3.1.0 2024-11-05 10:55:20 -08:00
Will Greenberg
b20d01e032 Add contents to certbot/CHANGELOG.md for next version 2024-11-05 10:55:20 -08:00
Will Greenberg
990352e371 Remove built packages from git 2024-11-05 10:55:20 -08:00
Will Greenberg
c5a5d6f9a1 Release 3.0.0 2024-11-05 10:55:19 -08:00
Will Greenberg
d4850399c5 Update changelog for 3.0.0 release 2024-11-05 10:54:15 -08:00
Brad Warren
c4be440853 update dependencies (#10036)
this fixes the current [dependabot alert](https://github.com/certbot/certbot/security/dependabot)
2024-11-01 10:04:10 -07:00
Will Greenberg
165c3e32b0 snap: fix generated postrefreshhook script (#9994)
Fixes #9990

If the python oneliner to check certbot's version succeeded, exit_code
would never be set, which would cause our exit_code check to fail. Use
a check that handles unset exit_code
2024-11-01 08:03:57 -07:00
Will Greenberg
2660a2017b Certbot 3.0 outdated plugin warning (#10031)
* Print an error if outdated snap plugins detected

With Certbot 3.0 comes a bump to Python 3.12, so if any snap plugins
are still located in a python3.8 directory, print an error informing
the user.

* tox nitpicks

* personal nitpick

* review fixups

* Update certbot/certbot/_internal/snap_config.py

Co-authored-by: ohemorange <ebportnoy@gmail.com>

* Use LOGGER.warn instead of error

* warn-->warning

* warn-->warning

---------

Co-authored-by: ohemorange <ebportnoy@gmail.com>
2024-11-01 07:52:48 -07:00
ohemorange
6a6544fd90 Update azure standard tests to use macOS-15 and python3.12 (#10032)
macOS-12 is [being deprecated](https://github.com/actions/runner-images/issues/10721) on Azure, so update to the latest available version.

* Upgrade macOS azure tests to use macOS-15

* switch standard azure tests to using python 3.12

* restore mac and linux cover tests to oldest and newest version style, and add explanation that that's what we're doing.
2024-11-01 07:34:16 -07:00
Brad Warren
320cf92944 depecate py38 support (#10034) 2024-10-31 15:48:57 -07:00
Brad Warren
3078c2f3db remove reference to "good first issue" label (#10018) 2024-10-25 11:43:44 -07:00
Brad Warren
c54f99e35b mattermost/action-mattermost-notify still uses master (#10021) 2024-10-04 14:08:25 -07:00
Brad Warren
c81dbb2582 Make Docker builds more verbose (#10022)
* use consistent casing to fix warnings

* don't truncate docker build logs

* make docker build output verbose
2024-10-04 13:54:56 -07:00
Will Greenberg
742f97e11a docs: fix logo url (#10019) 2024-09-26 15:10:06 -07:00
Will Greenberg
84c8dbc52a Migrate master branch to main
We're a few years behind the curve on this one, but using "master" as a
programming term is a callous practice that explicitly uses the
historical institution of slavery as a cheap, racist metaphor. Switch to
using "main", as it's the new default in git and GitHub.
2024-09-26 14:48:10 -07:00
851 changed files with 7392 additions and 7439 deletions

View File

@@ -1,8 +1,8 @@
# Configuring Azure Pipelines with Certbot
Let's begin. All pipelines are defined in `.azure-pipelines`. Currently there are two:
* `.azure-pipelines/main.yml` is the main one, executed on PRs for master, and pushes to master,
* `.azure-pipelines/advanced.yml` add installer testing on top of the main pipeline, and is executed for `test-*` branches, release branches, and nightly run for master.
* `.azure-pipelines/main.yml` is the main one, executed on PRs for main, and pushes to main,
* `.azure-pipelines/advanced.yml` add installer testing on top of the main pipeline, and is executed for `test-*` branches, release branches, and nightly run for main.
Several templates are defined in `.azure-pipelines/templates`. These YAML files aggregate common jobs configuration that can be reused in several pipelines.
@@ -64,7 +64,7 @@ Azure Pipeline needs RW on code, RO on metadata, RW on checks, commit statuses,
RW access here is required to allow update of the pipelines YAML files from Azure DevOps interface, and to
update the status of builds and PRs on GitHub side when Azure Pipelines are triggered.
Note however that no admin access is defined here: this means that Azure Pipelines cannot do anything with
protected branches, like master, and cannot modify the security context around this on GitHub.
protected branches, like main, and cannot modify the security context around this on GitHub.
Access can be defined for all or only selected repositories, which is nice.
```
@@ -91,11 +91,11 @@ grant permissions from Azure Pipelines to GitHub in order to setup a GitHub OAut
then are way too large (admin level on almost everything), while the classic approach does not add any more
permissions, and works perfectly well.__
- Select GitHub in "Select your repository section", choose certbot/certbot in Repository, master in default branch.
- Select GitHub in "Select your repository section", choose certbot/certbot in Repository, main in default branch.
- Click on YAML option for "Select a template"
- Choose a name for the pipeline (eg. test-pipeline), and browse to the actual pipeline YAML definition in the
"YAML file path" input (eg. `.azure-pipelines/test-pipeline.yml`)
- Click "Save & queue", choose the master branch to build the first pipeline, and click "Save and run" button.
- Click "Save & queue", choose the main branch to build the first pipeline, and click "Save and run" button.
_Done. Pipeline is operational. Repeat to add more pipelines from existing YAML files in `.azure-pipelines`._

View File

@@ -1,9 +1,9 @@
# We run the test suite on commits to master so codecov gets coverage data
# about the master branch and can use it to track coverage changes.
# We run the test suite on commits to main so codecov gets coverage data
# about the main branch and can use it to track coverage changes.
trigger:
- master
- main
pr:
- master
- main
- '*.x'
variables:

View File

@@ -1,4 +1,4 @@
# Nightly pipeline running each day for master.
# Nightly pipeline running each day for main.
trigger: none
pr: none
schedules:
@@ -6,7 +6,7 @@ schedules:
displayName: Nightly build
branches:
include:
- master
- main
always: true
variables:
@@ -15,5 +15,5 @@ variables:
stages:
- template: templates/stages/test-and-package-stage.yml
- template: templates/stages/changelog-stage.yml
- template: templates/stages/nightly-deploy-stage.yml
- template: templates/stages/notify-failure-stage.yml

View File

@@ -14,4 +14,3 @@ stages:
- template: templates/stages/test-and-package-stage.yml
- template: templates/stages/changelog-stage.yml
- template: templates/stages/release-deploy-stage.yml
- template: templates/stages/notify-failure-stage.yml

View File

@@ -4,32 +4,24 @@ jobs:
- name: IMAGE_NAME
value: ubuntu-22.04
- name: PYTHON_VERSION
value: 3.12
value: 3.13
- group: certbot-common
strategy:
matrix:
linux-py39:
PYTHON_VERSION: 3.9
TOXENV: py39
linux-py310:
PYTHON_VERSION: 3.10
TOXENV: py310
linux-py311:
PYTHON_VERSION: 3.11
TOXENV: py311
linux-py312:
PYTHON_VERSION: 3.12
TOXENV: py312
linux-isolated:
TOXENV: 'isolated-acme,isolated-certbot,isolated-apache,isolated-cloudflare,isolated-digitalocean,isolated-dnsimple,isolated-dnsmadeeasy,isolated-gehirn,isolated-google,isolated-linode,isolated-luadns,isolated-nsone,isolated-ovh,isolated-rfc2136,isolated-route53,isolated-sakuracloud,isolated-nginx'
linux-integration-certbot-oldest:
PYTHON_VERSION: 3.8
PYTHON_VERSION: 3.10
TOXENV: integration-certbot-oldest
linux-integration-nginx-oldest:
PYTHON_VERSION: 3.8
PYTHON_VERSION: 3.10
TOXENV: integration-nginx-oldest
# python 3.8 integration tests are not run here because they're run as
# part of the standard test suite
linux-py39-integration:
PYTHON_VERSION: 3.9
TOXENV: integration
linux-py310-integration:
PYTHON_VERSION: 3.10
TOXENV: integration
@@ -39,17 +31,19 @@ jobs:
linux-py312-integration:
PYTHON_VERSION: 3.12
TOXENV: integration
# python 3.13 integration tests are not run here because they're run as
# part of the standard test suite
nginx-compat:
TOXENV: nginx_compat
linux-integration-rfc2136:
IMAGE_NAME: ubuntu-22.04
PYTHON_VERSION: 3.8
PYTHON_VERSION: 3.12
TOXENV: integration-dns-rfc2136
le-modification:
IMAGE_NAME: ubuntu-22.04
TOXENV: modification
farmtest-apache2:
PYTHON_VERSION: 3.8
PYTHON_VERSION: 3.12
TOXENV: test-farm-apache2
pool:
vmImage: $(IMAGE_NAME)

View File

@@ -1,7 +1,7 @@
jobs:
- job: docker_build
pool:
vmImage: ubuntu-22.04
vmImage: ubuntu-24.04
strategy:
matrix:
arm32v6:
@@ -155,5 +155,5 @@ jobs:
displayName: Prepare Certbot-CI
- script: |
set -e
sudo -E venv/bin/pytest certbot-ci/snap_integration_tests/dns_tests --allow-persistent-changes --snap-folder $(Build.SourcesDirectory)/snap --snap-arch amd64
sudo -E venv/bin/pytest certbot-ci/src/snap_integration_tests/dns_tests --allow-persistent-changes --snap-folder $(Build.SourcesDirectory)/snap --snap-arch amd64
displayName: Test DNS plugins snaps

View File

@@ -1,30 +1,26 @@
jobs:
- job: test
variables:
PYTHON_VERSION: 3.12
PYTHON_VERSION: 3.13
strategy:
matrix:
macos-py38-cover:
IMAGE_NAME: macOS-12
PYTHON_VERSION: 3.8
macos-cover:
IMAGE_NAME: macOS-15
TOXENV: cover
# As of pip 23.1.0, builds started failing on macOS unless this flag was set.
# See https://github.com/certbot/certbot/pull/9717#issuecomment-1610861794.
PIP_USE_PEP517: "true"
macos-cover:
IMAGE_NAME: macOS-13
TOXENV: cover
# See explanation under macos-py38-cover.
PIP_USE_PEP517: "true"
linux-oldest:
IMAGE_NAME: ubuntu-22.04
PYTHON_VERSION: 3.8
PYTHON_VERSION: 3.10
TOXENV: oldest
linux-py38:
linux-py310:
# linux unit tests with the oldest python we support
IMAGE_NAME: ubuntu-22.04
PYTHON_VERSION: 3.8
TOXENV: py38
PYTHON_VERSION: 3.10
TOXENV: py310
linux-cover:
# linux unit+cover tests with the newest python we support
IMAGE_NAME: ubuntu-22.04
TOXENV: cover
linux-lint:
@@ -35,7 +31,6 @@ jobs:
TOXENV: mypy
linux-integration:
IMAGE_NAME: ubuntu-22.04
PYTHON_VERSION: 3.8
TOXENV: integration
apache-compat:
IMAGE_NAME: ubuntu-22.04
@@ -46,12 +41,15 @@ jobs:
nginxroundtrip:
IMAGE_NAME: ubuntu-22.04
TOXENV: nginxroundtrip
validate-changelog:
IMAGE_NAME: ubuntu-22.04
TOXENV: validate-changelog
pool:
vmImage: $(IMAGE_NAME)
steps:
- template: ../steps/tox-steps.yml
- job: test_sphinx_builds
pool:
vmImage: ubuntu-20.04
vmImage: ubuntu-22.04
steps:
- template: ../steps/sphinx-steps.yml

View File

@@ -3,13 +3,13 @@ stages:
jobs:
- job: prepare
pool:
vmImage: windows-2019
vmImage: ubuntu-latest
steps:
# If we change the output filename from `release_notes.md`, it should also be changed in tools/create_github_release.py
- bash: |
set -e
CERTBOT_VERSION="$(cd certbot && python -c "import certbot; print(certbot.__version__)" && cd ~-)"
"${BUILD_REPOSITORY_LOCALPATH}\tools\extract_changelog.py" "${CERTBOT_VERSION}" >> "${BUILD_ARTIFACTSTAGINGDIRECTORY}/release_notes.md"
CERTBOT_VERSION="$(cd certbot/src && python -c "import certbot; print(certbot.__version__)" && cd ~-)"
"${BUILD_REPOSITORY_LOCALPATH}/tools/extract_changelog.py" "${CERTBOT_VERSION}" >> "${BUILD_ARTIFACTSTAGINGDIRECTORY}/release_notes.md"
displayName: Prepare changelog
- task: PublishPipelineArtifact@1
inputs:

View File

@@ -1,19 +0,0 @@
stages:
- stage: On_Failure
jobs:
- job: notify_mattermost
variables:
- group: certbot-common
pool:
vmImage: ubuntu-20.04
steps:
- bash: |
set -e
MESSAGE="\
---\n\
##### Azure Pipeline
*Repo* $(Build.Repository.ID) - *Pipeline* $(Build.DefinitionName) #$(Build.BuildNumber) - *Branch/PR* $(Build.SourceBranchName)\n\
:warning: __Pipeline has failed__: [Link to the build](https://dev.azure.com/$(Build.Repository.ID)/_build/results?buildId=$(Build.BuildId)&view=results)\n\n\
---"
curl -i -X POST --data-urlencode "payload={\"text\":\"${MESSAGE}\"}" "$(MATTERMOST_URL)"
condition: failed()

View File

@@ -15,22 +15,11 @@ stages:
- task: GitHubRelease@1
inputs:
# this "github-releases" credential is what azure pipelines calls a
# "service connection". it was created using the instructions at
# https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#github-service-connection
# with a fine-grained personal access token from github to limit
# the permissions given to azure pipelines. the connection on azure
# needs permissions for the "release" pipeline (and maybe the
# "full-test-suite" pipeline to simplify testing it). information
# on how to set up these permissions can be found at
# https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#secure-a-service-connection.
# the github token that is used needs "contents:write" and
# "workflows:write" permissions for the certbot repo
# "service connection". it needs to be recreated annually. instructions
# to do so and further information about the token are available at
# https://github.com/EFForg/certbot-misc/wiki/Azure-Pipelines-setup#regenerating-github-release-credentials-for-use-on-azure
#
# as of writing this, the current token will expire on 3/15/2025.
# when recreating it, you may also want to create it using the
# shared "certbotbot" github account so the credentials aren't tied
# to any one dev's github account and their access to the certbot
# repo
# as of writing this, the current token will expire on Wed, Feb 25 2026.
gitHubConnection: github-releases
title: ${{ format('Certbot {0}', replace(variables['Build.SourceBranchName'], 'v', '')) }}
releaseNotesFilePath: '$(Pipeline.Workspace)/release_notes.md'

View File

@@ -2,7 +2,7 @@ steps:
- bash: |
set -e
sudo apt-get update
sudo apt-get install -y --no-install-recommends libaugeas0
sudo apt-get install -y --no-install-recommends libaugeas-dev
FINAL_STATUS=0
declare -a FAILED_BUILDS
tools/venv.py

View File

@@ -20,7 +20,7 @@ steps:
set -e
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libaugeas0 \
libaugeas-dev \
nginx-light
sudo systemctl stop nginx
sudo sysctl net.ipv4.ip_unprivileged_port_start=0

69
.github/ISSUE_TEMPLATE/bug.yaml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: Bug Report
description: File a bug report.
title: "[Bug]: "
type: Bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! If you're
having trouble using Certbot and aren't sure you've found a bug,
please first try asking for help at https://community.letsencrypt.org/.
There is a much larger community there of people familiar with the
project who will be able to more quickly answer your questions.
- type: input
attributes:
label: OS
description: |
Describe your Operating System. Examples: Ubuntu 18.04, CentOS 8 Stream
placeholder: Ubuntu 24.04
validations:
required: true
- type: input
attributes:
label: Installation method
description: |
How did you install Certbot? Examples: snap, pip, apt, yum
placeholder: snap
validations:
required: true
- type: input
attributes:
label: Certbot Version
description: |
If you're not sure, you can find this by running `certbot --version`.
placeholder: 1.0.0
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: |
I ran this command and it produced this output. Example:
```
$ sudo certbot certonly -d adfsfasdf.asdfasdf --staging
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator nginx, Installer nginx
Requesting a certificate for example.com
An unexpected error occurred:
Invalid identifiers requested :: Cannot issue for "adfsfasdf.asdfasdf": Domain name does not end with a valid public suffix (TLD)
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.
```
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: Certbot's behavior differed from what I expected because.
placeholder: "What was expected?"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Here is a Certbot log showing the issue (if available). Logs are stored in `/var/log/letsencrypt` by default. Feel free to redact domains, e-mail and IP addresses as you see fit.
render: shell

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Let's Encrypt Community Support
url: https://community.letsencrypt.org/
about: If you're having trouble using Certbot and aren't sure you've found a bug or request for a new feature, please first try asking for help here. There is a much larger community there of people familiar with the project who will be able to more quickly answer your questions.
- name: Certbot Security Policy
url: https://github.com/certbot/certbot/security/advisories/new
about: Please report security vulnerabilities here.

27
.github/ISSUE_TEMPLATE/feature.yaml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Feature Request
description: Suggest a new feature or improvement to Certbot
title: "[Feature Request]: "
type: Feature
body:
- type: textarea
id: problem
attributes:
label: What problem does this feature solve or what does it enhance?
description: Explain what this feature addresses, or the benefit it provides.
placeholder: For example, "Currently, users have to manually do X, which is time-consuming."
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the solution you'd like to see implemented.
placeholder: For example, "Implement a new button that automatically does X."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions?
placeholder: For example, "We considered Y, but Z is a better approach because..."

15
.github/ISSUE_TEMPLATE/task.yaml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Task
description: A codebase upkeep task such as managing deprecations and refactoring
title: "[Task]: "
type: Task
body:
- type: textarea
id: problem
attributes:
label: Task description
description: Describe the work that needs to happen, and why.
placeholder: >
For example, "In issue [link here], we noted that we cannot update [dependency] until
[something happens]. That thing has happened, so now we should update [dependency]."
validations:
required: true

View File

@@ -1,22 +0,0 @@
If you're having trouble using Certbot and aren't sure you've found a bug or
request for a new feature, please first try asking for help at
https://community.letsencrypt.org/. There is a much larger community there of
people familiar with the project who will be able to more quickly answer your
questions.
## My operating system is (include version):
## I installed Certbot with (snap, OS package manager, pip, certbot-auto, etc):
## I ran this command and it produced this output:
## Certbot's behavior differed from what I expected because:
## Here is a Certbot log showing the issue (if available):
###### Logs are stored in `/var/log/letsencrypt` by default. Feel free to redact domains, e-mail and IP addresses as you see fit.
## Here is the relevant nginx server block or Apache virtualhost for the domain I am configuring:

View File

@@ -1,6 +1,7 @@
## Pull Request Checklist
- [ ] The Certbot team has recently expressed interest in reviewing a PR for this. If not, this PR may be closed due our limited resources and need to prioritize how we spend them.
- [ ] If the change being made is to a [distributed component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout), edit the `master` section of `certbot/CHANGELOG.md` to include a description of the change being made.
- [ ] If the change being made is to a [distributed component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout), add a description of your change to the `newsfragments` directory. This should be a file called `<title>.<type>`, where `<title>` is either a GitHub issue number or some other unique name starting with `+`, and `<type>` is either `changed`, `fixed`, or `added`.
* For example, if you fixed a bug for issue number 42, create a file called `42.fixed` and put a description of your change in that file.
- [ ] Add or update any documentation as needed to support the changes in this PR.
- [ ] Include your name in `AUTHORS.md` if you like.

14
.github/workflows/assigned.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: Issue Assigned
on:
issues:
types: [assigned]
jobs:
send-mattermost-message:
runs-on: ubuntu-latest
steps:
- uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_ASSIGN_WEBHOOK }}
TEXT: >
${{ github.event.assignee.login }} assigned to "${{ github.event.issue.title }}": ${{ github.event.issue.html_url }}

View File

@@ -7,8 +7,7 @@ on:
jobs:
if_merged:
# Forked repos can not access Mattermost secret.
if: github.event.pull_request.merged == true && !github.event.pull_request.head.repo.fork
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: mattermost/action-mattermost-notify@master
@@ -18,4 +17,4 @@ jobs:
[${{ github.repository }}] |
[${{ github.event.pull_request.title }}
#${{ github.event.number }}](https://github.com/${{ github.repository }}/pull/${{ github.event.number }})
was merged into master by ${{ github.actor }}
was merged into main by ${{ github.actor }}

View File

@@ -2,8 +2,8 @@ name: Weekly Github Update
on:
schedule:
# Every week on Thursday @ 13:00
- cron: "0 13 * * 4"
# Every week on Thursday @ 10:00
- cron: "0 10 * * 4"
workflow_dispatch:
jobs:
send-mattermost-message:
@@ -13,15 +13,13 @@ jobs:
- name: Create Mattermost Message
run: |
DATE=$(date --date="7 days ago" +"%Y-%m-%d")
echo "MERGED_URL=https://github.com/pulls?q=merged%3A%3E${DATE}+org%3Acertbot" >> $GITHUB_ENV
echo "UPDATED_URL=https://github.com/pulls?q=updated%3A%3E${DATE}+org%3Acertbot" >> $GITHUB_ENV
echo "ASSIGNED_PRS=https://github.com/pulls?q=is%3Apr+is%3Aopen+updated%3A%3E%3D${DATE}+assignee%3A*+user%3Acertbot" >> $GITHUB_ENV
echo "UPDATED_URL=https://github.com/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-desc+updated%3A%3E%3D${DATE}+user%3Acertbot" >> $GITHUB_ENV
- uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }}
MATTERMOST_CHANNEL: private-certbot
TEXT: |
## Updates Across Certbot Repos
- Certbot team members SHOULD look at: [link](${{ env.MERGED_URL }})
- Certbot team members MAY also want to look at: [link](${{ env.UPDATED_URL }})
- Want to Discuss something today? Place it [here](https://docs.google.com/document/d/17YMUbtC1yg6MfiTMwT8zVm9LmO-cuGVBom0qFn8XJBM/edit?usp=sharing) and we can meet today on Zoom.
- The key words SHOULD and MAY in this message are to be interpreted as described in [RFC 8147](https://www.rfc-editor.org/rfc/rfc8174).
## Updates In the Past Week
- Most commented in the last week: [link](${{ env.UPDATED_URL }})
- Updated (assigned) PRs in the last week: [link](${{ env.ASSIGNED_PRS }})

16
.github/workflows/review_requested.yaml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Review Requested
on:
pull_request:
types: [review_requested]
jobs:
send-mattermost-message:
# Don't notify for the interim step of certbot/eff-devs being assigned
if: ${{ github.event.requested_reviewer.login != ''}}
runs-on: ubuntu-latest
steps:
- uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_ASSIGN_WEBHOOK }}
TEXT: >
Review requested from ${{ github.event.requested_reviewer.login }} for "${{ github.event.pull_request.title }}": ${{ github.event.pull_request.html_url }}

View File

@@ -28,7 +28,10 @@ jobs:
exempt-all-issue-assignees: true
# Label to use when marking as stale
stale-issue-label: needs-update
stale-issue-label: stale-needs-update
# Label to use when issue is automatically closed
close-issue-label: auto-closed
stale-issue-message: >
We've made a lot of changes to Certbot since this issue was opened. If you

5
.gitignore vendored
View File

@@ -27,6 +27,8 @@ tags
.idea
.ropeproject
.vscode
*.sublime-project
*.sublime-workspace
# auth --cert-path --chain-path
/*.pem
@@ -60,3 +62,6 @@ certbot-dns*/certbot-dns*_arm*.txt
/certbot_arm*.txt
certbot-dns*/snap
snapcraft.cfg
# pyenv files
.python-version

View File

@@ -4,4 +4,4 @@ force_sort_within_sections=True
force_single_line=True
order_by_type=False
line_length=400
src_paths=acme/acme,acme/tests,certbot*/certbot*,certbot*/tests
src_paths=acme/src,acme/tests,certbot*/tests,certbot/src,certbot*/src/certbot*

View File

@@ -139,6 +139,7 @@ Authors
* [John Reed](https://github.com/leerspace)
* [Jonas Berlin](https://github.com/xkr47)
* [Jonathan Herlin](https://github.com/Jonher937)
* [Jonathan Vanasco](https://github.com/jvanasco)
* [Jon Walsh](https://github.com/code-tree)
* [Joona Hoikkala](https://github.com/joohoi)
* [Josh McCullough](https://github.com/JoshMcCullough)
@@ -245,6 +246,7 @@ Authors
* [Sagi Kedmi](https://github.com/sagi)
* [Sam Lanning](https://github.com/samlanning)
* [sapics](https://github.com/sapics)
* [SATOH Fumiyasu](https://github.com/fumiyas)
* [Scott Barr](https://github.com/scottjbarr)
* [Scott Merrill](https://github.com/skpy)
* [Sebastian Bögl](https://github.com/TheBoegl)

View File

@@ -1,5 +1,8 @@
<!---
zoracon: (This is an old comment below, not sure how accurate this is anymore.
Since Github seems to lean more towards Markdown these days, it's still probably accurate)
This file serves as an entry point for GitHub's Contributing
Guidelines [1] only.
@@ -19,19 +22,15 @@ to the Sphinx generated docs is provided below.
Hi! Welcome to the Certbot project. We look forward to collaborating with you.
If you're reporting a bug in Certbot, please make sure to include:
- The version of Certbot you're running.
- The operating system you're running it on.
- The commands you ran.
- What you expected to happen, and
- What actually happened.
If you're reporting a bug in Certbot. Please open an issue: https://github.com/certbot/certbot/issues/new/choose.
If you're having trouble using Certbot and aren't sure you've found a bug, please first try asking for help at https://community.letsencrypt.org/. There is a much larger community there of people familiar with the project who will be able to more quickly answer your questions.
If you're a developer, we have some helpful information in our
[Developer's Guide](https://certbot.eff.org/docs/contributing.html) to get you
started. In particular, we recommend you read these sections
started. In particular, we recommend you read these sections:
- [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode)
- [Finding issues to work on](https://certbot.eff.org/docs/contributing.html#find-issues-to-work-on)
- [Coding style](https://certbot.eff.org/docs/contributing.html#coding-style)
- [Submitting a pull request](https://certbot.eff.org/docs/contributing.html#submitting-a-pull-request)
- [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode)

View File

@@ -1,5 +1,16 @@
# Security Policy
## Supported Versions
Explanation on supported versions [here](https://github.com/certbot/certbot/wiki/Architectural-Decision-Records#-update-to-certbots-version-policy-and-end-of-life-support-on-previous-major-versions)
| Major Version | Support Level |
| ------- | ------------------ |
| >= 4.0 | Full Support |
| 3.x | Discretionary Backports |
| <=2.x | None |
## Reporting a Vulnerability
Security vulnerabilities can be reported using GitHub's [private vulnerability reporting tool](https://github.com/certbot/certbot/security/advisories/new).

View File

@@ -1,9 +1,8 @@
include LICENSE.txt
include README.rst
include pytest.ini
recursive-include docs *
recursive-include examples *
recursive-include acme/_internal/tests/testdata *
include acme/py.typed
recursive-include src/acme/_internal/tests/testdata *
include src/acme/py.typed
global-exclude __pycache__
global-exclude *.py[cod]

View File

@@ -1,356 +0,0 @@
"""Tests for acme.crypto_util."""
import ipaddress
import itertools
import socket
import socketserver
import sys
import threading
import time
from typing import List
import unittest
import josepy as jose
import OpenSSL
import pytest
from acme import errors
from acme._internal.tests import test_util
class SSLSocketAndProbeSNITest(unittest.TestCase):
"""Tests for acme.crypto_util.SSLSocket/probe_sni."""
def setUp(self):
self.cert = test_util.load_comparable_cert('rsa2048_cert.pem')
key = test_util.load_pyopenssl_private_key('rsa2048_key.pem')
# pylint: disable=protected-access
certs = {b'foo': (key, self.cert.wrapped)}
from acme.crypto_util import SSLSocket
class _TestServer(socketserver.TCPServer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.socket = SSLSocket(self.socket, certs)
self.server = _TestServer(('', 0), socketserver.BaseRequestHandler)
self.port = self.server.socket.getsockname()[1]
self.server_thread = threading.Thread(
target=self.server.handle_request)
def tearDown(self):
if self.server_thread.is_alive():
# The thread may have already terminated.
self.server_thread.join() # pragma: no cover
self.server.server_close()
def _probe(self, name):
from acme.crypto_util import probe_sni
return jose.ComparableX509(probe_sni(
name, host='127.0.0.1', port=self.port))
def _start_server(self):
self.server_thread.start()
time.sleep(1) # TODO: avoid race conditions in other way
def test_probe_ok(self):
self._start_server()
assert self.cert == self._probe(b'foo')
def test_probe_not_recognized_name(self):
self._start_server()
with pytest.raises(errors.Error):
self._probe(b'bar')
def test_probe_connection_error(self):
self.server.server_close()
original_timeout = socket.getdefaulttimeout()
try:
socket.setdefaulttimeout(1)
with pytest.raises(errors.Error):
self._probe(b'bar')
finally:
socket.setdefaulttimeout(original_timeout)
class SSLSocketTest(unittest.TestCase):
"""Tests for acme.crypto_util.SSLSocket."""
def test_ssl_socket_invalid_arguments(self):
from acme.crypto_util import SSLSocket
with pytest.raises(ValueError):
_ = SSLSocket(None, {'sni': ('key', 'cert')},
cert_selection=lambda _: None)
with pytest.raises(ValueError):
_ = SSLSocket(None)
class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase):
"""Test for acme.crypto_util._pyopenssl_cert_or_req_all_names."""
@classmethod
def _call(cls, loader, name):
# pylint: disable=protected-access
from acme.crypto_util import _pyopenssl_cert_or_req_all_names
return _pyopenssl_cert_or_req_all_names(loader(name))
def _call_cert(self, name):
return self._call(test_util.load_cert, name)
def test_cert_one_san_no_common(self):
assert self._call_cert('cert-nocn.der') == \
['no-common-name.badssl.com']
def test_cert_no_sans_yes_common(self):
assert self._call_cert('cert.pem') == ['example.com']
def test_cert_two_sans_yes_common(self):
assert self._call_cert('cert-san.pem') == \
['example.com', 'www.example.com']
class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
"""Test for acme.crypto_util._pyopenssl_cert_or_req_san."""
@classmethod
def _call(cls, loader, name):
# pylint: disable=protected-access
from acme.crypto_util import _pyopenssl_cert_or_req_san
return _pyopenssl_cert_or_req_san(loader(name))
@classmethod
def _get_idn_names(cls):
"""Returns expected names from '{cert,csr}-idnsans.pem'."""
chars = [chr(i) for i in itertools.chain(range(0x3c3, 0x400),
range(0x641, 0x6fc),
range(0x1820, 0x1877))]
return [''.join(chars[i: i + 45]) + '.invalid'
for i in range(0, len(chars), 45)]
def _call_cert(self, name):
return self._call(test_util.load_cert, name)
def _call_csr(self, name):
return self._call(test_util.load_csr, name)
def test_cert_no_sans(self):
assert self._call_cert('cert.pem') == []
def test_cert_two_sans(self):
assert self._call_cert('cert-san.pem') == \
['example.com', 'www.example.com']
def test_cert_hundred_sans(self):
assert self._call_cert('cert-100sans.pem') == \
['example{0}.com'.format(i) for i in range(1, 101)]
def test_cert_idn_sans(self):
assert self._call_cert('cert-idnsans.pem') == \
self._get_idn_names()
def test_csr_no_sans(self):
assert self._call_csr('csr-nosans.pem') == []
def test_csr_one_san(self):
assert self._call_csr('csr.pem') == ['example.com']
def test_csr_two_sans(self):
assert self._call_csr('csr-san.pem') == \
['example.com', 'www.example.com']
def test_csr_six_sans(self):
assert self._call_csr('csr-6sans.pem') == \
['example.com', 'example.org', 'example.net',
'example.info', 'subdomain.example.com',
'other.subdomain.example.com']
def test_csr_hundred_sans(self):
assert self._call_csr('csr-100sans.pem') == \
['example{0}.com'.format(i) for i in range(1, 101)]
def test_csr_idn_sans(self):
assert self._call_csr('csr-idnsans.pem') == \
self._get_idn_names()
def test_critical_san(self):
assert self._call_cert('critical-san.pem') == \
['chicago-cubs.venafi.example', 'cubs.venafi.example']
class PyOpenSSLCertOrReqSANIPTest(unittest.TestCase):
"""Test for acme.crypto_util._pyopenssl_cert_or_req_san_ip."""
@classmethod
def _call(cls, loader, name):
# pylint: disable=protected-access
from acme.crypto_util import _pyopenssl_cert_or_req_san_ip
return _pyopenssl_cert_or_req_san_ip(loader(name))
def _call_cert(self, name):
return self._call(test_util.load_cert, name)
def _call_csr(self, name):
return self._call(test_util.load_csr, name)
def test_cert_no_sans(self):
assert self._call_cert('cert.pem') == []
def test_csr_no_sans(self):
assert self._call_csr('csr-nosans.pem') == []
def test_cert_domain_sans(self):
assert self._call_cert('cert-san.pem') == []
def test_csr_domain_sans(self):
assert self._call_csr('csr-san.pem') == []
def test_cert_ip_two_sans(self):
assert self._call_cert('cert-ipsans.pem') == ['192.0.2.145', '203.0.113.1']
def test_csr_ip_two_sans(self):
assert self._call_csr('csr-ipsans.pem') == ['192.0.2.145', '203.0.113.1']
def test_csr_ipv6_sans(self):
assert self._call_csr('csr-ipv6sans.pem') == \
['0:0:0:0:0:0:0:1', 'A3BE:32F3:206E:C75D:956:CEE:9858:5EC5']
def test_cert_ipv6_sans(self):
assert self._call_cert('cert-ipv6sans.pem') == \
['0:0:0:0:0:0:0:1', 'A3BE:32F3:206E:C75D:956:CEE:9858:5EC5']
class GenSsCertTest(unittest.TestCase):
"""Test for gen_ss_cert (generation of self-signed cert)."""
def setUp(self):
self.cert_count = 5
self.serial_num: List[int] = []
self.key = OpenSSL.crypto.PKey()
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
def test_sn_collisions(self):
from acme.crypto_util import gen_ss_cert
for _ in range(self.cert_count):
cert = gen_ss_cert(self.key, ['dummy'], force_san=True,
ips=[ipaddress.ip_address("10.10.10.10")])
self.serial_num.append(cert.get_serial_number())
assert len(set(self.serial_num)) >= self.cert_count
def test_no_name(self):
from acme.crypto_util import gen_ss_cert
with pytest.raises(AssertionError):
gen_ss_cert(self.key, ips=[ipaddress.ip_address("1.1.1.1")])
gen_ss_cert(self.key)
class MakeCSRTest(unittest.TestCase):
"""Test for standalone functions."""
@classmethod
def _call_with_key(cls, *args, **kwargs):
privkey = OpenSSL.crypto.PKey()
privkey.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
privkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey)
from acme.crypto_util import make_csr
return make_csr(privkey_pem, *args, **kwargs)
def test_make_csr(self):
csr_pem = self._call_with_key(["a.example", "b.example"])
assert b'--BEGIN CERTIFICATE REQUEST--' in csr_pem
assert b'--END CERTIFICATE REQUEST--' in csr_pem
csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't
# have a get_extensions() method, so we skip this test if the method
# isn't available.
if hasattr(csr, 'get_extensions'):
assert len(csr.get_extensions()) == 1
assert csr.get_extensions()[0].get_data() == \
OpenSSL.crypto.X509Extension(
b'subjectAltName',
critical=False,
value=b'DNS:a.example, DNS:b.example',
).get_data()
def test_make_csr_ip(self):
csr_pem = self._call_with_key(["a.example"], False, [ipaddress.ip_address('127.0.0.1'), ipaddress.ip_address('::1')])
assert b'--BEGIN CERTIFICATE REQUEST--' in csr_pem
assert b'--END CERTIFICATE REQUEST--' in csr_pem
csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't
# have a get_extensions() method, so we skip this test if the method
# isn't available.
if hasattr(csr, 'get_extensions'):
assert len(csr.get_extensions()) == 1
assert csr.get_extensions()[0].get_data() == \
OpenSSL.crypto.X509Extension(
b'subjectAltName',
critical=False,
value=b'DNS:a.example, IP:127.0.0.1, IP:::1',
).get_data()
# for IP san it's actually need to be octet-string,
# but somewhere downstream thankfully handle it for us
def test_make_csr_must_staple(self):
csr_pem = self._call_with_key(["a.example"], must_staple=True)
csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't
# have a get_extensions() method, so we skip this test if the method
# isn't available.
if hasattr(csr, 'get_extensions'):
assert len(csr.get_extensions()) == 2
# NOTE: Ideally we would filter by the TLS Feature OID, but
# OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID,
# and the shortname field is just "UNDEF"
must_staple_exts = [e for e in csr.get_extensions()
if e.get_data() == b"0\x03\x02\x01\x05"]
assert len(must_staple_exts) == 1, \
"Expected exactly one Must Staple extension"
def test_make_csr_without_hostname(self):
with pytest.raises(ValueError):
self._call_with_key()
def test_make_csr_correct_version(self):
csr_pem = self._call_with_key(["a.example"])
csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, csr_pem)
assert csr.get_version() == 0, \
"Expected CSR version to be v1 (encoded as 0), per RFC 2986, section 4"
class DumpPyopensslChainTest(unittest.TestCase):
"""Test for dump_pyopenssl_chain."""
@classmethod
def _call(cls, loaded):
# pylint: disable=protected-access
from acme.crypto_util import dump_pyopenssl_chain
return dump_pyopenssl_chain(loaded)
def test_dump_pyopenssl_chain(self):
names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem']
loaded = [test_util.load_cert(name) for name in names]
length = sum(
len(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
for cert in loaded)
assert len(self._call(loaded)) == length
def test_dump_pyopenssl_chain_wrapped(self):
names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem']
loaded = [test_util.load_cert(name) for name in names]
wrap_func = jose.ComparableX509
wrapped = [wrap_func(cert) for cert in loaded]
dump_func = OpenSSL.crypto.dump_certificate
length = sum(len(dump_func(OpenSSL.crypto.FILETYPE_PEM, cert)) for cert in loaded)
assert len(self._call(wrapped)) == length
if __name__ == '__main__':
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover

View File

@@ -1,81 +0,0 @@
"""Test utilities.
.. warning:: This module is not part of the public API.
"""
import os
import sys
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import josepy as jose
from josepy.util import ComparableECKey
from OpenSSL import crypto
if sys.version_info >= (3, 9): # pragma: no cover
import importlib.resources as importlib_resources
else: # pragma: no cover
import importlib_resources
def load_vector(*names):
"""Load contents of a test vector."""
# luckily, resource_string opens file in binary mode
vector_ref = importlib_resources.files(__package__).joinpath('testdata', *names)
return vector_ref.read_bytes()
def _guess_loader(filename, loader_pem, loader_der):
_, ext = os.path.splitext(filename)
if ext.lower() == '.pem':
return loader_pem
elif ext.lower() == '.der':
return loader_der
raise ValueError("Loader could not be recognized based on extension") # pragma: no cover
def load_cert(*names):
"""Load certificate."""
loader = _guess_loader(
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_certificate(loader, load_vector(*names))
def load_comparable_cert(*names):
"""Load ComparableX509 cert."""
return jose.ComparableX509(load_cert(*names))
def load_csr(*names):
"""Load certificate request."""
loader = _guess_loader(
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_certificate_request(loader, load_vector(*names))
def load_comparable_csr(*names):
"""Load ComparableX509 certificate request."""
return jose.ComparableX509(load_csr(*names))
def load_rsa_private_key(*names):
"""Load RSA private key."""
loader = _guess_loader(names[-1], serialization.load_pem_private_key,
serialization.load_der_private_key)
return jose.ComparableRSAKey(loader(
load_vector(*names), password=None, backend=default_backend()))
def load_ecdsa_private_key(*names):
"""Load ECDSA private key."""
loader = _guess_loader(names[-1], serialization.load_pem_private_key,
serialization.load_der_private_key)
return ComparableECKey(loader(
load_vector(*names), password=None, backend=default_backend()))
def load_pyopenssl_private_key(*names):
"""Load pyOpenSSL private key."""
loader = _guess_loader(
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_privatekey(loader, load_vector(*names))

View File

@@ -1,16 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICdjCCAV4CAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMXq
v1y8EIcCbaUIzCtOcLkLS0MJ35oS+6DmV5WB1A0cIk6YrjsHIsY2lwMm13BWIvmw
tY+Y6n0rr7eViNx5ZRGHpHEI/TL3Neb+VefTydL5CgvK3dd4ex2kSbTaed3fmpOx
qMajEduwNcZPCcmoEXPkfrCP8w2vKQUkQ+JRPcdX1nTuzticeRP5B7YCmJsmxkEh
Y0tzzZ+NIRDARoYNofefY86h3e5q66gtJxccNchmIM3YQahhg5n3Xoo8hGfM/TIc
R7ncCBCLO6vtqo0QFva/NQODrgOmOsmgvqPkUWQFdZfWM8yIaU826dktx0CPB78t
TudnJ1rBRvGsjHMsZikCAwEAAaAxMC8GCSqGSIb3DQEJDjEiMCAwHgYDVR0RBBcw
FYINYS5leGVtcGxlLmNvbYcEwAACbzANBgkqhkiG9w0BAQsFAAOCAQEAdGMcRCxq
1X09gn1TNdMt64XUv+wdJCKDaJ+AgyIJj7QvVw8H5k7dOnxS4I+a/yo4jE+LDl2/
AuHcBLFEI4ddewdJSMrTNZjuRYuOdr3KP7fL7MffICSBi45vw5EOXg0tnjJCEiKu
6gcJgbLSP5JMMd7Haf33Q/VWsmHofR3VwOMdrnakwAU3Ff5WTuXTNVhL1kT/uLFX
yW1ru6BF4unwNqSR2UeulljpNfRBsiN4zJK11W6n9KT0NkBr9zY5WCM4sW7i8k9V
TeypWGo3jBKzYAGeuxZsB97U77jZ2lrGdBLZKfbcjnTeRVqCvCRrui4El7UGYFmj
7s6OJyWx5DSV8w==
-----END CERTIFICATE REQUEST-----

View File

@@ -1,458 +0,0 @@
"""Crypto utilities."""
import binascii
import contextlib
import ipaddress
import logging
import os
import re
import socket
from typing import Any
from typing import Callable
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Union
import josepy as jose
from OpenSSL import crypto
from OpenSSL import SSL
from acme import errors
logger = logging.getLogger(__name__)
# Default SSL method selected here is the most compatible, while secure
# SSL method: TLSv1_METHOD is only compatible with
# TLSv1_METHOD, while SSLv23_METHOD is compatible with all other
# methods, including TLSv2_METHOD (read more at
# https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni
# should be changed to use "set_options" to disable SSLv2 and SSLv3,
# in case it's used for things other than probing/serving!
_DEFAULT_SSL_METHOD = SSL.SSLv23_METHOD
class _DefaultCertSelection:
def __init__(self, certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]]):
self.certs = certs
def __call__(self, connection: SSL.Connection) -> Optional[Tuple[crypto.PKey, crypto.X509]]:
server_name = connection.get_servername()
if server_name:
return self.certs.get(server_name, None)
return None # pragma: no cover
class SSLSocket: # pylint: disable=too-few-public-methods
"""SSL wrapper for sockets.
:ivar socket sock: Original wrapped socket.
:ivar dict certs: Mapping from domain names (`bytes`) to
`OpenSSL.crypto.X509`.
:ivar method: See `OpenSSL.SSL.Context` for allowed values.
:ivar alpn_selection: Hook to select negotiated ALPN protocol for
connection.
:ivar cert_selection: Hook to select certificate for connection. If given,
`certs` parameter would be ignored, and therefore must be empty.
"""
def __init__(self, sock: socket.socket,
certs: Optional[Mapping[bytes, Tuple[crypto.PKey, crypto.X509]]] = None,
method: int = _DEFAULT_SSL_METHOD,
alpn_selection: Optional[Callable[[SSL.Connection, List[bytes]], bytes]] = None,
cert_selection: Optional[Callable[[SSL.Connection],
Optional[Tuple[crypto.PKey,
crypto.X509]]]] = None
) -> None:
self.sock = sock
self.alpn_selection = alpn_selection
self.method = method
if not cert_selection and not certs:
raise ValueError("Neither cert_selection or certs specified.")
if cert_selection and certs:
raise ValueError("Both cert_selection and certs specified.")
actual_cert_selection: Union[_DefaultCertSelection,
Optional[Callable[[SSL.Connection],
Optional[Tuple[crypto.PKey,
crypto.X509]]]]] = cert_selection
if actual_cert_selection is None:
actual_cert_selection = _DefaultCertSelection(certs if certs else {})
self.cert_selection = actual_cert_selection
def __getattr__(self, name: str) -> Any:
return getattr(self.sock, name)
def _pick_certificate_cb(self, connection: SSL.Connection) -> None:
"""SNI certificate callback.
This method will set a new OpenSSL context object for this
connection when an incoming connection provides an SNI name
(in order to serve the appropriate certificate, if any).
:param connection: The TLS connection object on which the SNI
extension was received.
:type connection: :class:`OpenSSL.Connection`
"""
pair = self.cert_selection(connection)
if pair is None:
logger.debug("Certificate selection for server name %s failed, dropping SSL",
connection.get_servername())
return
key, cert = pair
new_context = SSL.Context(self.method)
new_context.set_options(SSL.OP_NO_SSLv2)
new_context.set_options(SSL.OP_NO_SSLv3)
new_context.use_privatekey(key)
new_context.use_certificate(cert)
if self.alpn_selection is not None:
new_context.set_alpn_select_callback(self.alpn_selection)
connection.set_context(new_context)
class FakeConnection:
"""Fake OpenSSL.SSL.Connection."""
# pylint: disable=missing-function-docstring
def __init__(self, connection: SSL.Connection) -> None:
self._wrapped = connection
def __getattr__(self, name: str) -> Any:
return getattr(self._wrapped, name)
def shutdown(self, *unused_args: Any) -> bool:
# OpenSSL.SSL.Connection.shutdown doesn't accept any args
try:
return self._wrapped.shutdown()
except SSL.Error as error:
# We wrap the error so we raise the same error type as sockets
# in the standard library. This is useful when this object is
# used by code which expects a standard socket such as
# socketserver in the standard library.
raise socket.error(error)
def accept(self) -> Tuple[FakeConnection, Any]: # pylint: disable=missing-function-docstring
sock, addr = self.sock.accept()
try:
context = SSL.Context(self.method)
context.set_options(SSL.OP_NO_SSLv2)
context.set_options(SSL.OP_NO_SSLv3)
context.set_tlsext_servername_callback(self._pick_certificate_cb)
if self.alpn_selection is not None:
context.set_alpn_select_callback(self.alpn_selection)
ssl_sock = self.FakeConnection(SSL.Connection(context, sock))
ssl_sock.set_accept_state()
# This log line is especially desirable because without it requests to
# our standalone TLSALPN server would not be logged.
logger.debug("Performing handshake with %s", addr)
try:
ssl_sock.do_handshake()
except SSL.Error as error:
# _pick_certificate_cb might have returned without
# creating SSL context (wrong server name)
raise socket.error(error)
return ssl_sock, addr
except:
# If we encounter any error, close the new socket before reraising
# the exception.
sock.close()
raise
def probe_sni(name: bytes, host: bytes, port: int = 443, timeout: int = 300, # pylint: disable=too-many-arguments
method: int = _DEFAULT_SSL_METHOD, source_address: Tuple[str, int] = ('', 0),
alpn_protocols: Optional[Sequence[bytes]] = None) -> crypto.X509:
"""Probe SNI server for SSL certificate.
:param bytes name: Byte string to send as the server name in the
client hello message.
:param bytes host: Host to connect to.
:param int port: Port to connect to.
:param int timeout: Timeout in seconds.
:param method: See `OpenSSL.SSL.Context` for allowed values.
:param tuple source_address: Enables multi-path probing (selection
of source interface). See `socket.creation_connection` for more
info. Available only in Python 2.7+.
:param alpn_protocols: Protocols to request using ALPN.
:type alpn_protocols: `Sequence` of `bytes`
:raises acme.errors.Error: In case of any problems.
:returns: SSL certificate presented by the server.
:rtype: OpenSSL.crypto.X509
"""
context = SSL.Context(method)
context.set_timeout(timeout)
socket_kwargs = {'source_address': source_address}
try:
logger.debug(
"Attempting to connect to %s:%d%s.", host, port,
" from {0}:{1}".format(
source_address[0],
source_address[1]
) if any(source_address) else ""
)
socket_tuple: Tuple[bytes, int] = (host, port)
sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore[arg-type]
except socket.error as error:
raise errors.Error(error)
with contextlib.closing(sock) as client:
client_ssl = SSL.Connection(context, client)
client_ssl.set_connect_state()
client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13
if alpn_protocols is not None:
client_ssl.set_alpn_protos(alpn_protocols)
try:
client_ssl.do_handshake()
client_ssl.shutdown()
except SSL.Error as error:
raise errors.Error(error)
cert = client_ssl.get_peer_certificate()
assert cert # Appease mypy. We would have crashed out by now if there was no certificate.
return cert
def make_csr(private_key_pem: bytes, domains: Optional[Union[Set[str], List[str]]] = None,
must_staple: bool = False,
ipaddrs: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None
) -> bytes:
"""Generate a CSR containing domains or IPs as subjectAltNames.
:param buffer private_key_pem: Private key, in PEM PKCS#8 format.
:param list domains: List of DNS names to include in subjectAltNames of CSR.
:param bool must_staple: Whether to include the TLS Feature extension (aka
OCSP Must Staple: https://tools.ietf.org/html/rfc7633).
:param list ipaddrs: List of IPaddress(type ipaddress.IPv4Address or ipaddress.IPv6Address)
names to include in subbjectAltNames of CSR.
params ordered this way for backward competablity when called by positional argument.
:returns: buffer PEM-encoded Certificate Signing Request.
"""
private_key = crypto.load_privatekey(
crypto.FILETYPE_PEM, private_key_pem)
csr = crypto.X509Req()
sanlist = []
# if domain or ip list not supplied make it empty list so it's easier to iterate
if domains is None:
domains = []
if ipaddrs is None:
ipaddrs = []
if len(domains)+len(ipaddrs) == 0:
raise ValueError("At least one of domains or ipaddrs parameter need to be not empty")
for address in domains:
sanlist.append('DNS:' + address)
for ips in ipaddrs:
sanlist.append('IP:' + ips.exploded)
# make sure its ascii encoded
san_string = ', '.join(sanlist).encode('ascii')
# for IP san it's actually need to be octet-string,
# but somewhere downsteam thankfully handle it for us
extensions = [
crypto.X509Extension(
b'subjectAltName',
critical=False,
value=san_string
),
]
if must_staple:
extensions.append(crypto.X509Extension(
b"1.3.6.1.5.5.7.1.24",
critical=False,
value=b"DER:30:03:02:01:05"))
csr.add_extensions(extensions)
csr.set_pubkey(private_key)
# RFC 2986 Section 4.1 only defines version 0
csr.set_version(0)
csr.sign(private_key, 'sha256')
return crypto.dump_certificate_request(
crypto.FILETYPE_PEM, csr)
def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req: Union[crypto.X509, crypto.X509Req]
) -> List[str]:
# unlike its name this only outputs DNS names, other type of idents will ignored
common_name = loaded_cert_or_req.get_subject().CN
sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req)
if common_name is None:
return sans
return [common_name] + [d for d in sans if d != common_name]
def _pyopenssl_cert_or_req_san(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]:
"""Get Subject Alternative Names from certificate or CSR using pyOpenSSL.
.. todo:: Implement directly in PyOpenSSL!
.. note:: Although this is `acme` internal API, it is used by
`letsencrypt`.
:param cert_or_req: Certificate or CSR.
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
:returns: A list of Subject Alternative Names that is DNS.
:rtype: `list` of `str`
"""
# This function finds SANs with dns name
# constants based on PyOpenSSL certificate/CSR text dump
part_separator = ":"
prefix = "DNS" + part_separator
sans_parts = _pyopenssl_extract_san_list_raw(cert_or_req)
return [part.split(part_separator)[1]
for part in sans_parts if part.startswith(prefix)]
def _pyopenssl_cert_or_req_san_ip(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]:
"""Get Subject Alternative Names IPs from certificate or CSR using pyOpenSSL.
:param cert_or_req: Certificate or CSR.
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
:returns: A list of Subject Alternative Names that are IP Addresses.
:rtype: `list` of `str`. note that this returns as string, not IPaddress object
"""
# constants based on PyOpenSSL certificate/CSR text dump
part_separator = ":"
prefix = "IP Address" + part_separator
sans_parts = _pyopenssl_extract_san_list_raw(cert_or_req)
return [part[len(prefix):] for part in sans_parts if part.startswith(prefix)]
def _pyopenssl_extract_san_list_raw(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]:
"""Get raw SAN string from cert or csr, parse it as UTF-8 and return.
:param cert_or_req: Certificate or CSR.
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
:returns: raw san strings, parsed byte as utf-8
:rtype: `list` of `str`
"""
# This function finds SANs by dumping the certificate/CSR to text and
# searching for "X509v3 Subject Alternative Name" in the text. This method
# is used to because in PyOpenSSL version <0.17 `_subjectAltNameString` methods are
# not able to Parse IP Addresses in subjectAltName string.
if isinstance(cert_or_req, crypto.X509):
# pylint: disable=line-too-long
text = crypto.dump_certificate(crypto.FILETYPE_TEXT, cert_or_req).decode('utf-8')
else:
text = crypto.dump_certificate_request(crypto.FILETYPE_TEXT, cert_or_req).decode('utf-8')
# WARNING: this function does not support multiple SANs extensions.
# Multiple X509v3 extensions of the same type is disallowed by RFC 5280.
raw_san = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text)
parts_separator = ", "
# WARNING: this function assumes that no SAN can include
# parts_separator, hence the split!
sans_parts = [] if raw_san is None else raw_san.group(1).split(parts_separator)
return sans_parts
def gen_ss_cert(key: crypto.PKey, domains: Optional[List[str]] = None,
not_before: Optional[int] = None,
validity: int = (7 * 24 * 60 * 60), force_san: bool = True,
extensions: Optional[List[crypto.X509Extension]] = None,
ips: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None
) -> crypto.X509:
"""Generate new self-signed certificate.
:type domains: `list` of `str`
:param OpenSSL.crypto.PKey key:
:param bool force_san:
:param extensions: List of additional extensions to include in the cert.
:type extensions: `list` of `OpenSSL.crypto.X509Extension`
:type ips: `list` of (`ipaddress.IPv4Address` or `ipaddress.IPv6Address`)
If more than one domain is provided, all of the domains are put into
``subjectAltName`` X.509 extension and first domain is set as the
subject CN. If only one domain is provided no ``subjectAltName``
extension is used, unless `force_san` is ``True``.
"""
assert domains or ips, "Must provide one or more hostnames or IPs for the cert."
cert = crypto.X509()
cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16))
cert.set_version(2)
if extensions is None:
extensions = []
if domains is None:
domains = []
if ips is None:
ips = []
extensions.append(
crypto.X509Extension(
b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
)
if len(domains) > 0:
cert.get_subject().CN = domains[0]
# TODO: what to put into cert.get_subject()?
cert.set_issuer(cert.get_subject())
sanlist = []
for address in domains:
sanlist.append('DNS:' + address)
for ip in ips:
sanlist.append('IP:' + ip.exploded)
san_string = ', '.join(sanlist).encode('ascii')
if force_san or len(domains) > 1 or len(ips) > 0:
extensions.append(crypto.X509Extension(
b"subjectAltName",
critical=False,
value=san_string
))
cert.add_extensions(extensions)
cert.gmtime_adj_notBefore(0 if not_before is None else not_before)
cert.gmtime_adj_notAfter(validity)
cert.set_pubkey(key)
cert.sign(key, "sha256")
return cert
def dump_pyopenssl_chain(chain: Union[List[jose.ComparableX509], List[crypto.X509]],
filetype: int = crypto.FILETYPE_PEM) -> bytes:
"""Dump certificate chain into a bundle.
:param list chain: List of `OpenSSL.crypto.X509` (or wrapped in
:class:`josepy.util.ComparableX509`).
:returns: certificate chain bundle
:rtype: bytes
"""
# XXX: returns empty string when no chain is available, which
# shuts up RenewableCert, but might not be the best solution...
def _dump_cert(cert: Union[jose.ComparableX509, crypto.X509]) -> bytes:
if isinstance(cert, jose.ComparableX509):
if isinstance(cert.wrapped, crypto.X509Req):
raise errors.Error("Unexpected CSR provided.") # pragma: no cover
cert = cert.wrapped
return crypto.dump_certificate(filetype, cert)
# assumes that OpenSSL.crypto.dump_certificate includes ending
# newline character
return b"".join(_dump_cert(cert) for cert in chain)

View File

@@ -0,0 +1,5 @@
Crypto_util
-----------
.. automodule:: acme.crypto_util
:members:

5
acme/docs/api/jws.rst Normal file
View File

@@ -0,0 +1,5 @@
JWS
---
.. automodule:: acme.jws
:members:

5
acme/docs/api/util.rst Normal file
View File

@@ -0,0 +1,5 @@
Util
----
.. automodule:: acme.util
:members:

View File

@@ -27,10 +27,11 @@ Workflow:
"""
from contextlib import contextmanager
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import josepy as jose
import OpenSSL
from acme import challenges
from acme import client
@@ -68,10 +69,11 @@ def new_csr_comp(domain_name, pkey_pem=None):
"""Create certificate signing request."""
if pkey_pem is None:
# Create private key.
pkey = OpenSSL.crypto.PKey()
pkey.generate_key(OpenSSL.crypto.TYPE_RSA, CERT_PKEY_BITS)
pkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM,
pkey)
pkey = rsa.generate_private_key(public_exponent=65537, key_size=CERT_PKEY_BITS)
pkey_pem = pkey.private_bytes(encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption())
csr_pem = crypto_util.make_csr(pkey_pem, [domain_name])
return pkey_pem, csr_pem
@@ -168,11 +170,8 @@ def example_http():
# Terms of Service URL is in client_acme.directory.meta.terms_of_service
# Registration Resource: regr
# Creates account with contact information.
email = ('fake@example.com')
regr = client_acme.new_account(
messages.NewRegistration.from_data(
email=email, terms_of_service_agreed=True))
messages.NewRegistration.from_data(terms_of_service_agreed=True))
# Create domain private key and CSR
pkey_pem, csr_pem = new_csr_comp(DOMAIN)
@@ -200,9 +199,7 @@ def example_http():
# Revoke certificate
fullchain_com = jose.ComparableX509(
OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, fullchain_pem))
fullchain_com = x509.load_pem_x509_certificate(fullchain_pem.encode())
try:
client_acme.revoke(fullchain_com, 0) # revocation reason = 0

55
acme/pyproject.toml Normal file
View File

@@ -0,0 +1,55 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "acme"
dynamic = ["version"]
description = "ACME protocol implementation in Python"
readme = "README.rst"
license = "Apache-2.0"
requires-python = ">=3.10"
authors = [
{ name = "Certbot Project", email = "certbot-dev@eff.org" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Security",
]
dependencies = [
"cryptography>=43.0.0",
"josepy>=2.0.0",
# PyOpenSSL>=25.0.0 is just needed to satisfy mypy right now so this dependency can probably be
# relaxed to >=24.0.0 if needed.
"PyOpenSSL>=25.0.0",
"pyrfc3339",
"requests>=2.25.1",
]
[project.optional-dependencies]
docs = [
"Sphinx>=1.0", # autodoc_member_order = 'bysource', autodoc_default_flags
"sphinx_rtd_theme",
]
test = [
"pytest",
"pytest-xdist",
"typing-extensions",
]
[project.urls]
Homepage = "https://github.com/certbot/certbot"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]

View File

@@ -1,2 +0,0 @@
[pytest]
norecursedirs = .* build dist CVS _darcs {arch} *.egg

View File

@@ -1,72 +1,7 @@
import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
install_requires = [
'cryptography>=3.2.1',
# Josepy 2+ may introduce backward incompatible changes by droping usage of
# deprecated PyOpenSSL APIs.
'josepy>=1.13.0, <2',
# pyOpenSSL 23.1.0 is a bad release: https://github.com/pyca/pyopenssl/issues/1199
'PyOpenSSL>=17.5.0,!=23.1.0',
'pyrfc3339',
'pytz>=2019.3',
'requests>=2.20.0',
'setuptools>=41.6.0',
]
docs_extras = [
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
'sphinx_rtd_theme',
]
test_extras = [
# In theory we could scope importlib_resources to env marker 'python_version<"3.9"'. But this
# makes the pinning mechanism emit warnings when running `poetry lock` because in the corner
# case of an extra dependency with env marker coming from a setup.py file, it generate the
# invalid requirement 'importlib_resource>=1.3.1;python<=3.9;extra=="test"'.
# To fix the issue, we do not pass the env marker. This is fine because:
# - importlib_resources can be applied to any Python version,
# - this is a "test" extra dependency for limited audience,
# - it does not change anything at the end for the generated requirement files.
'importlib_resources>=1.3.1',
'pytest',
'pytest-xdist',
'typing-extensions',
]
version = '5.2.0.dev0'
setup(
name='acme',
version=version,
description='ACME protocol implementation in Python',
url='https://github.com/certbot/certbot',
author="Certbot Project",
author_email='certbot-dev@eff.org',
license='Apache License 2.0',
python_requires='>=3.8',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],
packages=find_packages(),
include_package_data=True,
install_requires=install_requires,
extras_require={
'docs': docs_extras,
'test': test_extras,
},
)

View File

@@ -12,7 +12,7 @@ import sys
#
# It is based on
# https://github.com/requests/requests/blob/1278ecdf71a312dc2268f3bfc0aabfab3c006dcf/requests/packages.py
import josepy as jose
import josepy as jose # noqa: F401
for mod in list(sys.modules):
# This traversal is apparently necessary such that the identities are

View File

@@ -6,14 +6,12 @@ import urllib.parse as urllib_parse
import josepy as jose
from josepy.jwk import JWKEC
import OpenSSL
import pytest
import requests
from acme import errors
from acme._internal.tests import test_util
CERT = test_util.load_comparable_cert('cert.pem')
CERT = test_util.load_cert('cert.pem')
KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem'))
@@ -263,126 +261,6 @@ class HTTP01Test(unittest.TestCase):
assert not self.msg.update(token=b'..').good_token
class TLSALPN01ResponseTest(unittest.TestCase):
def setUp(self):
from acme.challenges import TLSALPN01
self.chall = TLSALPN01(
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
self.domain = u'example.com'
self.domain2 = u'example2.com'
self.response = self.chall.response(KEY)
self.jmsg = {
'resource': 'challenge',
'type': 'tls-alpn-01',
'keyAuthorization': self.response.key_authorization,
}
def test_to_partial_json(self):
assert {} == self.response.to_partial_json()
def test_from_json(self):
from acme.challenges import TLSALPN01Response
assert self.response == TLSALPN01Response.from_json(self.jmsg)
def test_from_json_hashable(self):
from acme.challenges import TLSALPN01Response
hash(TLSALPN01Response.from_json(self.jmsg))
def test_gen_verify_cert(self):
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
cert, key2 = self.response.gen_cert(self.domain, key1)
assert key1 == key2
assert self.response.verify_cert(self.domain, cert)
def test_gen_verify_cert_gen_key(self):
cert, key = self.response.gen_cert(self.domain)
assert isinstance(key, OpenSSL.crypto.PKey)
assert self.response.verify_cert(self.domain, cert)
def test_verify_bad_cert(self):
assert not self.response.verify_cert(self.domain,
test_util.load_cert('cert.pem'))
def test_verify_bad_domain(self):
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
cert, key2 = self.response.gen_cert(self.domain, key1)
assert key1 == key2
assert not self.response.verify_cert(self.domain2, cert)
def test_simple_verify_bad_key_authorization(self):
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
self.response.simple_verify(self.chall, "local", key2.public_key())
@mock.patch('acme.challenges.TLSALPN01Response.verify_cert', autospec=True)
def test_simple_verify(self, mock_verify_cert):
mock_verify_cert.return_value = mock.sentinel.verification
assert mock.sentinel.verification == self.response.simple_verify(
self.chall, self.domain, KEY.public_key(),
cert=mock.sentinel.cert)
mock_verify_cert.assert_called_once_with(
self.response, self.domain, mock.sentinel.cert)
@mock.patch('acme.challenges.socket.gethostbyname')
@mock.patch('acme.challenges.crypto_util.probe_sni')
def test_probe_cert(self, mock_probe_sni, mock_gethostbyname):
mock_gethostbyname.return_value = '127.0.0.1'
self.response.probe_cert('foo.com')
mock_gethostbyname.assert_called_once_with('foo.com')
mock_probe_sni.assert_called_once_with(
host=b'127.0.0.1', port=self.response.PORT, name=b'foo.com',
alpn_protocols=[b'acme-tls/1'])
self.response.probe_cert('foo.com', host='8.8.8.8')
mock_probe_sni.assert_called_with(
host=b'8.8.8.8', port=mock.ANY, name=b'foo.com',
alpn_protocols=[b'acme-tls/1'])
@mock.patch('acme.challenges.TLSALPN01Response.probe_cert')
def test_simple_verify_false_on_probe_error(self, mock_probe_cert):
mock_probe_cert.side_effect = errors.Error
assert not self.response.simple_verify(
self.chall, self.domain, KEY.public_key())
class TLSALPN01Test(unittest.TestCase):
def setUp(self):
from acme.challenges import TLSALPN01
self.msg = TLSALPN01(
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e'))
self.jmsg = {
'type': 'tls-alpn-01',
'token': 'a82d5ff8ef740d12881f6d3c2277ab2e',
}
def test_to_partial_json(self):
assert self.jmsg == self.msg.to_partial_json()
def test_from_json(self):
from acme.challenges import TLSALPN01
assert self.msg == TLSALPN01.from_json(self.jmsg)
def test_from_json_hashable(self):
from acme.challenges import TLSALPN01
hash(TLSALPN01.from_json(self.jmsg))
def test_from_json_invalid_token_length(self):
from acme.challenges import TLSALPN01
self.jmsg['token'] = jose.encode_b64jose(b'abcd')
with pytest.raises(jose.DeserializationError):
TLSALPN01.from_json(self.jmsg)
@mock.patch('acme.challenges.TLSALPN01Response.gen_cert')
def test_validation(self, mock_gen_cert):
mock_gen_cert.return_value = ('cert', 'key')
assert ('cert', 'key') == self.msg.validation(
KEY, cert_key=mock.sentinel.cert_key, domain=mock.sentinel.domain)
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key,
domain=mock.sentinel.domain)
class DNSTest(unittest.TestCase):
def setUp(self):

View File

@@ -5,10 +5,11 @@ import datetime
import http.client as http_client
import json
import sys
from typing import Dict
import unittest
from unittest import mock
from cryptography import x509
import josepy as jose
import pytest
import requests
@@ -24,6 +25,7 @@ from acme.client import ClientV2
CERT_SAN_PEM = test_util.load_vector('cert-san.pem')
CSR_MIXED_PEM = test_util.load_vector('csr-mixed.pem')
CSR_NO_SANS_PEM = test_util.load_vector('csr-nosans.pem')
KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
DIRECTORY_V2 = messages.Directory({
@@ -52,7 +54,7 @@ class ClientV2Test(unittest.TestCase):
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
reg = messages.Registration(
contact=self.contact, key=KEY.public_key())
the_arg: Dict = dict(reg)
the_arg: dict = dict(reg)
self.new_reg = messages.NewRegistration(**the_arg)
self.regr = messages.RegistrationResource(
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1')
@@ -97,6 +99,10 @@ class ClientV2Test(unittest.TestCase):
body=self.order,
uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1',
authorizations=[self.authzr, self.authzr2], csr_pem=CSR_MIXED_PEM)
self.orderr2 = messages.OrderResource(
body=self.order,
uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1',
authorizations=[self.authzr, self.authzr2], csr_pem=CSR_NO_SANS_PEM)
def test_new_account(self):
self.response.status_code = http_client.CREATED
@@ -158,6 +164,10 @@ class ClientV2Test(unittest.TestCase):
mock_post_as_get.side_effect = (authz_response, authz_response2)
assert self.client.new_order(CSR_MIXED_PEM) == self.orderr
with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get:
mock_post_as_get.side_effect = (authz_response, authz_response2)
assert self.client.new_order(CSR_NO_SANS_PEM) == self.orderr2
def test_answer_challege(self):
self.response.links['up'] = {'url': self.challr.authzr_uri}
self.response.json.return_value = self.challr.body.to_json()
@@ -245,6 +255,26 @@ class ClientV2Test(unittest.TestCase):
with pytest.raises(errors.IssuanceError):
self.client.finalize_order(self.orderr, deadline)
@mock.patch('acme.client.ClientV2.begin_finalization')
def test_finalize_order_ready(self, mock_begin):
# https://github.com/certbot/certbot/issues/9766
updated_order_ready = self.order.update(status=messages.STATUS_READY)
updated_order_valid = self.order.update(
certificate='https://www.letsencrypt-demo.org/acme/cert/',
status=messages.STATUS_VALID)
updated_orderr = self.orderr.update(body=updated_order_valid, fullchain_pem=CERT_SAN_PEM)
self.response.text = CERT_SAN_PEM
self.response.json.side_effect = [updated_order_ready.to_json(),
updated_order_valid.to_json()]
deadline = datetime.datetime(9999, 9, 9)
assert self.client.finalize_order(self.orderr, deadline) == updated_orderr
assert self.response.json.call_count == 2
assert mock_begin.call_count == 2
def test_finalize_order_invalid_status(self):
# https://github.com/certbot/certbot/issues/9296
order = self.order.update(error=None, status=messages.STATUS_INVALID)
@@ -252,6 +282,53 @@ class ClientV2Test(unittest.TestCase):
with pytest.raises(errors.Error, match="The certificate order failed"):
self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9))
@mock.patch('acme.client.time.sleep')
@mock.patch('acme.client.datetime')
def test_finalize_order_orderNotReady(self, dt_mock, mock_sleep):
# https://github.com/certbot/certbot/issues/9766
updated_order_processing = self.order.update(status=messages.STATUS_PROCESSING)
updated_order_ready = self.order.update(status=messages.STATUS_READY)
updated_order_valid = self.order.update(
certificate='https://www.letsencrypt-demo.org/acme/cert/',
status=messages.STATUS_VALID)
self.orderr.update(body=updated_order_valid, fullchain_pem=CERT_SAN_PEM)
self.response.text = CERT_SAN_PEM
self.response.json.side_effect = [updated_order_processing.to_json(),
updated_order_ready.to_json(),
updated_order_valid.to_json()]
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
dt_mock.timedelta = datetime.timedelta
self.response.headers['Retry-After'] = '50'
post = mock.MagicMock()
post.side_effect = [messages.Error.with_code('orderNotReady'), # first begin_finalization
# sleep 1
self.response, # first poll_finalization poll --> returns processing
# retry-after sleep here
self.response, # second poll_finalization poll --> returns ready
mock.MagicMock(), # second begin_finalization
# sleep 1
self.response, # third poll_finalization poll --> returns valid
self.response # fetch cert
]
self.net.post = post
self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9))
assert self.net.post.call_count == 6
assert mock_sleep.call_args_list == [((1,),), ((50,),), ((1,),)]
def test_finalize_order_otherErrorCode(self):
post = mock.MagicMock()
post.side_effect = [messages.Error.with_code('serverInternal')]
self.net.post = post
with pytest.raises(messages.Error):
self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9))
def test_finalize_order_timeout(self):
deadline = datetime.datetime.now() - datetime.timedelta(seconds=60)
with pytest.raises(errors.TimeoutError):
@@ -333,7 +410,7 @@ class ClientV2Test(unittest.TestCase):
with mock.patch('acme.client.ClientV2._authzr_from_response') as mock_client:
mock_client.return_value = self.authzr2
self.client.poll(self.authzr2) # pylint: disable=protected-access
self.client.poll(self.authzr2)
self.client.net.post.assert_called_once_with(
self.authzr2.uri, None,
@@ -386,6 +463,211 @@ class ClientV2Test(unittest.TestCase):
assert DIRECTORY_V2.to_partial_json() == \
ClientV2.get_directory('https://example.com/dir', self.net).to_partial_json()
@mock.patch('acme.client.datetime')
def test_renewal_time_expired_cert(self, dt_mock):
utc_now = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
dt_mock.datetime.now.return_value = utc_now
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),
)
cert = x509.load_pem_x509_certificate(cert_pem)
t, _ = self.client.renewal_time(cert_pem)
assert t == cert.not_valid_after_utc
@mock.patch('acme.client.datetime')
def test_renewal_time_no_renewal_info(self, dt_mock):
utc_now = datetime.datetime(2025, 3, 15, tzinfo=datetime.timezone.utc)
dt_mock.datetime.now.return_value = utc_now
# A directory with no 'renewalInfo' should result in None.
self.client.directory = messages.Directory({})
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),
)
t, _ = self.client.renewal_time(cert_pem)
assert t is None
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 30, 00, 00, 00),
)
t, _ = self.client.renewal_time(cert_pem)
assert t is None
@mock.patch('acme.client.datetime')
def test_renewal_time_with_renewal_info(self, dt_mock):
from cryptography import x509
from acme.client import _renewal_info_path_component
utc_now = datetime.datetime(2025, 3, 15, tzinfo=datetime.timezone.utc)
dt_mock.datetime.now.return_value = utc_now
dt_mock.timedelta = datetime.timedelta
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),
)
self.client.directory = messages.Directory({
'renewalInfo': 'https://www.letsencrypt-demo.org/acme/renewal-info',
})
self.response.json.return_value = {
"suggestedWindow": {
"start": "2025-03-14T01:01:01Z",
"end": "2025-03-14T01:01:01Z",
},
"message": "Keep those certs fresh"
}
t, _ = self.client.renewal_time(cert_pem)
cert_parsed = x509.load_pem_x509_certificate(cert_pem)
ari_path_component = _renewal_info_path_component(cert_parsed)
self.net.get.assert_called_once_with("https://www.letsencrypt-demo.org/acme/renewal-info/" +
ari_path_component,
content_type='application/json')
assert t == datetime.datetime(2025, 3, 14, 1, 1, 1, tzinfo=datetime.timezone.utc)
self.net.reset_mock()
self.response.json.return_value = {
"suggestedWindow": {
"start": "2025-03-16T01:01:01Z",
"end": "2025-03-17T01:01:01Z",
},
"message": "Keep those certs fresh"
}
t, _ = self.client.renewal_time(cert_pem)
self.net.get.assert_called_once_with("https://www.letsencrypt-demo.org/acme/renewal-info/" +
ari_path_component,
content_type='application/json')
assert t >= datetime.datetime(2025, 3, 16, 1, 1, 1, tzinfo=datetime.timezone.utc)
assert t <= datetime.datetime(2025, 3, 17, 1, 1, 1, tzinfo=datetime.timezone.utc)
@mock.patch('acme.client.datetime')
def test_renewal_time_renewal_info_errors(self, dt_mock):
def now(tzinfo=None):
return datetime.datetime(2025, 3, 15, tzinfo=tzinfo)
dt_mock.datetime.now.side_effect = now
dt_mock.timedelta = datetime.timedelta
dt_mock.timezone = datetime.timezone
self.client.directory = messages.Directory({
'renewalInfo': 'https://www.letsencrypt-demo.org/acme/renewal-info',
})
# Failure to fetch the 'renewalInfo' URL should raise an ARIError with the exception raised
# by self.net.get as the __cause__ and a Retry-After 6 hours in the future
expected_cause = requests.exceptions.RequestException
expected_retry_after = now() + datetime.timedelta(seconds=6 * 60 * 60)
self.net.get.side_effect = expected_cause
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),
)
with pytest.raises(errors.ARIError) as exception_info:
self.client.renewal_time(cert_pem)
assert isinstance(exception_info.value.__cause__, expected_cause)
assert exception_info.value.retry_after == expected_retry_after
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 30, 00, 00, 00),
)
with pytest.raises(errors.ARIError) as exception_info:
self.client.renewal_time(cert_pem)
assert isinstance(exception_info.value.__cause__, expected_cause)
assert exception_info.value.retry_after == expected_retry_after
@mock.patch('acme.client.datetime')
def test_renewal_time_returns_retry_after(self, dt_mock):
def now(tzinfo=None):
return datetime.datetime(2025, 3, 15, tzinfo=tzinfo)
dt_mock.datetime.now.side_effect = now
dt_mock.timedelta = datetime.timedelta
dt_mock.timezone = datetime.timezone
self.client.directory = messages.Directory({
'renewalInfo': 'https://www.letsencrypt-demo.org/acme/renewal-info',
})
cert_pem = make_cert_for_renewal(
not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),
not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),
)
self.response.json.return_value = {
"suggestedWindow": {
"start": "2025-03-14T01:01:01Z",
"end": "2025-03-14T01:01:01Z",
},
"message": "Keep those certs fresh"
}
# With no explicit Retry-After in header, default to six hours
_, retry_after = self.client.renewal_time(cert_pem)
assert retry_after == datetime.datetime(2025, 3, 15, 6, 0, 0)
# With an explicit Retry-After in header, use that
self.response.headers['Retry-After'] = '100'
_, retry_after = self.client.renewal_time(cert_pem)
assert retry_after == datetime.datetime(2025, 3, 15, 00, 1, 40)
def test_renewal_info_path_component():
from cryptography import x509
from acme.client import _renewal_info_path_component
cert = x509.load_pem_x509_certificate(test_util.load_vector('rsa2048_cert.pem'))
assert _renewal_info_path_component(cert) == "fL5sRirC8VS5AtOQh9DfoAzYNCI.ALVG_VbBb5U7"
# From https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html appendix A.
ARI_TEST_CERT = b"""
-----BEGIN CERTIFICATE-----
MIIBQzCB66ADAgECAgUAh2VDITAKBggqhkjOPQQDAjAVMRMwEQYDVQQDEwpFeGFt
cGxlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBYxFDAS
BgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeBZu
7cbpAYNXZLbbh8rNIzuOoqOOtmxA1v7cRm//AwyMwWxyHz4zfwmBhcSrf47NUAFf
qzLQ2PPQxdTXREYEnKMjMCEwHwYDVR0jBBgwFoAUaYhba4dGQEHhs3uEe6CuLN4B
yNQwCgYIKoZIzj0EAwIDRwAwRAIge09+S5TZAlw5tgtiVvuERV6cT4mfutXIlwTb
+FYN/8oCIClDsqBklhB9KAelFiYt9+6FDj3z4KGVelYM5MdsO3pK
-----END CERTIFICATE-----
"""
cert = x509.load_pem_x509_certificate(ARI_TEST_CERT)
assert _renewal_info_path_component(cert) == "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE"
if __name__ == '__main__':
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
def make_cert_for_renewal(not_before, not_after) -> bytes:
"""
Return a PEM-encoded, self-signed certificate with the given dates.
"""
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization, hashes
# AKID and serial are the inputs to constructing the renewalInfo URL
akid = x509.AuthorityKeyIdentifier(b"1234", None, None)
serial = 56789
key = ec.generate_private_key(ec.SECP256R1())
cert = x509.CertificateBuilder(
issuer_name=x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "Some Issuer")]),
subject_name=x509.Name([]),
public_key=key.public_key(),
serial_number=serial,
not_valid_before=not_before,
not_valid_after=not_after,
).add_extension(
x509.SubjectAlternativeName([x509.DNSName('example.com')]),
critical=False,
).add_extension(
akid,
critical=False,
).sign(
private_key=key,
algorithm=hashes.SHA256(),
)
return cert.public_bytes(serialization.Encoding.PEM)
class MockJSONDeSerializable(jose.JSONDeSerializable):
# pylint: disable=missing-docstring
@@ -612,27 +894,12 @@ class ClientNetworkTest(unittest.TestCase):
with pytest.raises(requests.exceptions.RequestException):
self.net._send_request('GET', 'uri')
def test_urllib_error(self):
# Using a connection error to test a properly formatted error message
try:
# pylint: disable=protected-access
self.net._send_request('GET', "http://localhost:19123/nonexistent.txt")
# Value Error Generated Exceptions
except ValueError as y:
assert "Requesting localhost/nonexistent: " \
"Connection refused" == str(y)
# Requests Library Exceptions
except requests.exceptions.ConnectionError as z: #pragma: no cover
assert "'Connection aborted.'" in str(z) or "[WinError 10061]" in str(z)
class ClientNetworkWithMockedResponseTest(unittest.TestCase):
"""Tests for acme.client.ClientNetwork which mock out response."""
def setUp(self):
self.net = ClientNetwork(key=None, alg=None)
self.net = ClientNetwork(key='fake', alg=None)
self.response = mock.MagicMock(ok=True, status_code=http_client.OK)
self.response.headers = {}
@@ -707,7 +974,6 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
assert self.response.checked
def test_post(self):
# pylint: disable=protected-access
assert self.response == self.net.post(
'uri', self.obj, content_type=self.content_type)
assert self.response.checked
@@ -736,7 +1002,6 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
check_response = mock.MagicMock()
check_response.side_effect = messages.Error.with_code('badNonce')
# pylint: disable=protected-access
self.net._check_response = check_response
with pytest.raises(messages.Error):
self.net.post('uri',
@@ -747,7 +1012,6 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
check_response.side_effect = [messages.Error.with_code('malformed'),
self.response]
# pylint: disable=protected-access
self.net._check_response = check_response
with pytest.raises(messages.Error):
self.net.post('uri',
@@ -758,7 +1022,6 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
post_once.side_effect = [messages.Error.with_code('badNonce'),
self.response]
# pylint: disable=protected-access
assert self.response == self.net.post(
'uri', self.obj, content_type=self.content_type)
@@ -789,6 +1052,16 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
self.content_type = None
self.net.post('uri', self.obj, content_type=None, new_nonce_url='new_nonce_uri')
def test_no_key_error(self):
"A ClientNetwork with no key should error on POST but succeed on GET"
self.net = ClientNetwork()
self.net._send_request = mock.MagicMock()
self.net._send_request.return_value = self.response
with pytest.raises(errors.Error):
self.net.post('uri', "body")
assert self.response == self.net.get(
'uri', content_type=self.content_type, bar='baz')
if __name__ == '__main__':
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover

View File

@@ -0,0 +1,16 @@
from unittest import mock
import pytest
# This avoids a bug on mac where getfqdn errors after a long timeout.
# See https://bugs.python.org/issue35164
# and discussion at https://github.com/certbot/certbot/pull/10408
@pytest.fixture(autouse=True)
def mock_getfqdn():
with mock.patch("socket.getfqdn", return_value="server_name") as mocked:
yield mocked
@pytest.fixture(autouse=True)
def mock_sleep():
with mock.patch("time.sleep") as mocked:
yield mocked

View File

@@ -0,0 +1,270 @@
"""Tests for acme.crypto_util."""
import ipaddress
import itertools
import sys
import unittest
from unittest import mock
import warnings
import pytest
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, x25519
from acme._internal.tests import test_util
class FormatTest(unittest.TestCase):
def test_to_cryptography_encoding(self):
from acme.crypto_util import Format
assert Format.DER.to_cryptography_encoding() == serialization.Encoding.DER
assert Format.PEM.to_cryptography_encoding() == serialization.Encoding.PEM
class MiscTests(unittest.TestCase):
def test_dump_cryptography_chain(self):
from acme.crypto_util import dump_cryptography_chain
cert1 = test_util.load_cert('rsa2048_cert.pem')
cert2 = test_util.load_cert('rsa4096_cert.pem')
chain = [cert1, cert2]
dumped = dump_cryptography_chain(chain)
# default is PEM encoding Encoding.PEM
assert isinstance(dumped, bytes)
class CryptographyCertOrReqSANTest(unittest.TestCase):
"""Test for acme.crypto_util._cryptography_cert_or_req_san."""
@classmethod
def _call(cls, loader, name):
# pylint: disable=protected-access
from acme.crypto_util import _cryptography_cert_or_req_san
return _cryptography_cert_or_req_san(loader(name))
@classmethod
def _get_idn_names(cls):
"""Returns expected names from '{cert,csr}-idnsans.pem'."""
chars = [chr(i) for i in itertools.chain(range(0x3c3, 0x400),
range(0x641, 0x6fc),
range(0x1820, 0x1877))]
return [''.join(chars[i: i + 45]) + '.invalid'
for i in range(0, len(chars), 45)]
def _call_cert(self, name):
return self._call(test_util.load_cert, name)
def _call_csr(self, name):
return self._call(test_util.load_csr, name)
def test_cert_no_sans(self):
assert self._call_cert('cert.pem') == []
def test_cert_two_sans(self):
assert self._call_cert('cert-san.pem') == \
['example.com', 'www.example.com']
def test_cert_hundred_sans(self):
assert self._call_cert('cert-100sans.pem') == \
['example{0}.com'.format(i) for i in range(1, 101)]
def test_cert_idn_sans(self):
assert self._call_cert('cert-idnsans.pem') == \
self._get_idn_names()
def test_csr_no_sans(self):
assert self._call_csr('csr-nosans.pem') == []
def test_csr_one_san(self):
assert self._call_csr('csr.pem') == ['example.com']
def test_csr_two_sans(self):
assert self._call_csr('csr-san.pem') == \
['example.com', 'www.example.com']
def test_csr_six_sans(self):
assert self._call_csr('csr-6sans.pem') == \
['example.com', 'example.org', 'example.net',
'example.info', 'subdomain.example.com',
'other.subdomain.example.com']
def test_csr_hundred_sans(self):
assert self._call_csr('csr-100sans.pem') == \
['example{0}.com'.format(i) for i in range(1, 101)]
def test_csr_idn_sans(self):
assert self._call_csr('csr-idnsans.pem') == \
self._get_idn_names()
def test_critical_san(self):
assert self._call_cert('critical-san.pem') == \
['chicago-cubs.venafi.example', 'cubs.venafi.example']
class GenMakeSelfSignedCertTest(unittest.TestCase):
"""Test for make_self_signed_cert."""
def setUp(self):
self.cert_count = 5
self.serial_num: list[int] = []
self.privkey = rsa.generate_private_key(public_exponent=65537, key_size=2048)
@classmethod
def _call(cls, *args, **kwargs):
from acme.crypto_util import make_self_signed_cert
with pytest.warns(DeprecationWarning, match='make_self_signed_cert is deprecated'):
return make_self_signed_cert(*args, **kwargs)
def test_sn_collisions(self):
for _ in range(self.cert_count):
cert = self._call(self.privkey, ['dummy'], force_san=True,
ips=[ipaddress.ip_address("10.10.10.10")])
self.serial_num.append(cert.serial_number)
assert len(set(self.serial_num)) >= self.cert_count
def test_no_ips(self):
self._call(self.privkey, ['dummy'])
@mock.patch("acme.crypto_util._now")
def test_expiry_times(self, mock_now):
from datetime import datetime, timedelta, timezone
not_before = 1736200830
validity = 100
not_before_dt = datetime.fromtimestamp(not_before)
validity_td = timedelta(validity)
not_after_dt = not_before_dt + validity_td
cert = self._call(
self.privkey,
['dummy'],
not_before=not_before_dt,
validity=validity_td,
)
# TODO: This should be `not_valid_before_utc` once we raise the minimum
# cryptography version.
# https://github.com/certbot/certbot/issues/10105
with warnings.catch_warnings():
warnings.filterwarnings(
'ignore',
message='Properties that return.*datetime object'
)
self.assertEqual(cert.not_valid_before, not_before_dt)
self.assertEqual(cert.not_valid_after, not_after_dt)
now = not_before + 1
now_dt = datetime.fromtimestamp(now)
mock_now.return_value = now_dt.replace(tzinfo=timezone.utc)
valid_after_now_dt = now_dt + validity_td
cert = self._call(
self.privkey,
['dummy'],
validity=validity_td,
)
with warnings.catch_warnings():
warnings.filterwarnings(
'ignore',
message='Properties that return.*datetime object'
)
self.assertEqual(cert.not_valid_before, now_dt)
self.assertEqual(cert.not_valid_after, valid_after_now_dt)
def test_no_name(self):
with pytest.raises(AssertionError):
self._call(self.privkey, ips=[ipaddress.ip_address("1.1.1.1")])
self._call(self.privkey)
def test_extensions(self):
extension_type = x509.TLSFeature([x509.TLSFeatureType.status_request])
extension = x509.Extension(
x509.TLSFeature.oid,
False,
extension_type
)
cert = self._call(
self.privkey,
ips=[ipaddress.ip_address("1.1.1.1")],
extensions=[extension]
)
self.assertIn(extension, cert.extensions)
class MakeCSRTest(unittest.TestCase):
"""Test for standalone functions."""
@classmethod
def _call_with_key(cls, *args, **kwargs):
privkey = rsa.generate_private_key(public_exponent=65537, key_size=2048)
privkey_pem = privkey.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
from acme.crypto_util import make_csr
return make_csr(privkey_pem, *args, **kwargs)
def test_make_csr(self):
csr_pem = self._call_with_key(["a.example", "b.example"])
assert b"--BEGIN CERTIFICATE REQUEST--" in csr_pem
assert b"--END CERTIFICATE REQUEST--" in csr_pem
csr = x509.load_pem_x509_csr(csr_pem)
assert len(csr.extensions) == 1
assert list(
csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
) == [
x509.DNSName("a.example"),
x509.DNSName("b.example"),
]
def test_make_csr_ip(self):
csr_pem = self._call_with_key(
["a.example"],
False,
[ipaddress.ip_address("127.0.0.1"), ipaddress.ip_address("::1")],
)
assert b"--BEGIN CERTIFICATE REQUEST--" in csr_pem
assert b"--END CERTIFICATE REQUEST--" in csr_pem
csr = x509.load_pem_x509_csr(csr_pem)
assert len(csr.extensions) == 1
assert list(
csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
) == [
x509.DNSName("a.example"),
x509.IPAddress(ipaddress.ip_address("127.0.0.1")),
x509.IPAddress(ipaddress.ip_address("::1")),
]
def test_make_csr_must_staple(self):
csr_pem = self._call_with_key(["a.example"], must_staple=True)
csr = x509.load_pem_x509_csr(csr_pem)
assert len(csr.extensions) == 2
assert list(csr.extensions.get_extension_for_class(x509.TLSFeature).value) == [
x509.TLSFeatureType.status_request
]
def test_make_csr_without_hostname(self):
with pytest.raises(ValueError):
self._call_with_key()
def test_make_csr_invalid_key_type(self):
privkey = x25519.X25519PrivateKey.generate()
privkey_pem = privkey.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
from acme.crypto_util import make_csr
with pytest.raises(ValueError):
make_csr(privkey_pem, ["a.example"])
if __name__ == '__main__':
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover

View File

@@ -51,5 +51,36 @@ class PollErrorTest(unittest.TestCase):
'sentinel.AR2})' % repr(set()) == repr(self.invalid)
class ValidationErrorTest(unittest.TestCase):
"""Tests for acme.errors.ValidationError"""
def setUp(self):
from acme.errors import ValidationError
from acme.challenges import DNS01
from acme.messages import Error
from acme.messages import Authorization
from acme.messages import AuthorizationResource
from acme.messages import IDENTIFIER_FQDN
from acme.messages import ChallengeBody
from acme.messages import Identifier
self.challenge_error = Error(typ='custom', detail='bar')
failed_authzr = AuthorizationResource(
body=Authorization(
identifier=Identifier(typ=IDENTIFIER_FQDN, value="example.com"),
challenges=[ChallengeBody(
chall=DNS01(),
error=self.challenge_error,
)]
)
)
self.error = ValidationError([failed_authzr])
def test_repr(self):
err_message = str(self.error)
assert 'Authorization for example.com failed' in err_message
assert 'Challenge dns-01 failed' in err_message
assert str(self.challenge_error) in err_message
if __name__ == "__main__":
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover

View File

@@ -2,11 +2,9 @@
import datetime
import sys
import unittest
import warnings
import josepy as jose
import pytest
import pytz
class FixedTest(unittest.TestCase):
@@ -34,7 +32,7 @@ class RFC3339FieldTest(unittest.TestCase):
"""Tests for acme.fields.RFC3339Field."""
def setUp(self):
self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.UTC)
self.decoded = datetime.datetime(2015, 3, 27, tzinfo=datetime.timezone.utc)
self.encoded = '2015-03-27T00:00:00Z'
def test_default_encoder(self):

View File

@@ -1,7 +1,6 @@
"""Tests for acme.jose shim."""
import importlib
import sys
import unittest
import pytest
@@ -21,9 +20,9 @@ def _test_it(submodule, attribute):
# We use the imports below with eval, but pylint doesn't
# understand that.
import josepy # pylint: disable=unused-import
import josepy # pylint: disable=unused-import # noqa: F401
import acme # pylint: disable=unused-import
import acme # pylint: disable=unused-import # noqa: F401
acme_jose_mod = eval(acme_jose_path) # pylint: disable=eval-used
josepy_mod = eval(josepy_path) # pylint: disable=eval-used
assert acme_jose_mod is josepy_mod

View File

@@ -21,7 +21,7 @@ class HeaderTest(unittest.TestCase):
except (ValueError, TypeError):
assert True
else:
assert False # pragma: no cover
pytest.fail("Exception from jose.b64decode wasn't raised") # pragma: no cover
def test_nonce_decoder(self):
from acme.jws import Header

View File

@@ -1,10 +1,8 @@
"""Tests for acme.messages."""
import contextlib
import sys
from typing import Dict
import json
import unittest
from unittest import mock
import warnings
import josepy as jose
import pytest
@@ -12,8 +10,8 @@ import pytest
from acme import challenges
from acme._internal.tests import test_util
CERT = test_util.load_comparable_cert('cert.der')
CSR = test_util.load_comparable_csr('csr.der')
CERT = test_util.load_cert('cert.der')
CSR = test_util.load_csr('csr.der')
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
@@ -119,7 +117,7 @@ class ConstantTest(unittest.TestCase):
from acme.messages import _Constant
class MockConstant(_Constant): # pylint: disable=missing-docstring
POSSIBLE_NAMES: Dict = {}
POSSIBLE_NAMES: dict = {}
self.MockConstant = MockConstant # pylint: disable=invalid-name
self.const_a = MockConstant('a')
@@ -162,6 +160,10 @@ class DirectoryTest(unittest.TestCase):
terms_of_service='https://example.com/acme/terms',
website='https://www.example.com/',
caa_identities=['example.com'],
profiles={
"example": "some profile",
"other example": "a different profile"
}
),
})
@@ -191,6 +193,10 @@ class DirectoryTest(unittest.TestCase):
'termsOfService': 'https://example.com/acme/terms',
'website': 'https://www.example.com/',
'caaIdentities': ['example.com'],
'profiles': {
'example': 'some profile',
'other example': 'a different profile'
}
},
}
@@ -212,17 +218,43 @@ class ExternalAccountBindingTest(unittest.TestCase):
self.key = jose.jwk.JWKRSA(key=KEY.public_key())
self.kid = "kid-for-testing"
self.hmac_key = "hmac-key-for-testing"
self.hmac_alg = "HS256"
self.dir = Directory({
'newAccount': 'http://url/acme/new-account',
})
def test_from_data(self):
from acme.messages import ExternalAccountBinding
eab = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir)
eab = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir, self.hmac_alg)
assert len(eab) == 3
assert sorted(eab.keys()) == sorted(['protected', 'payload', 'signature'])
def test_from_data_invalid_hmac_alg(self):
from acme.messages import ExternalAccountBinding
invalid_alg = "HS9999"
with pytest.raises(ValueError) as info:
ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir, invalid_alg)
assert "Invalid value for hmac_alg" in str(info.value)
def test_from_data_default_hmac_alg(self):
from acme.messages import ExternalAccountBinding
eab_default = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir)
assert len(eab_default) == 3
assert sorted(eab_default.keys()) == sorted(['protected', 'payload', 'signature'])
eab_explicit = ExternalAccountBinding.from_data(
self.key, self.kid, self.hmac_key, self.dir, "HS256"
)
assert eab_default == eab_explicit
protected_default = json.loads(
jose.b64.b64decode(eab_default['protected']).decode()
)
assert protected_default['alg'] == 'HS256'
class RegistrationTest(unittest.TestCase):
"""Tests for acme.messages.Registration."""
@@ -262,10 +294,11 @@ class RegistrationTest(unittest.TestCase):
key = jose.jwk.JWKRSA(key=KEY.public_key())
kid = "kid-for-testing"
hmac_key = "hmac-key-for-testing"
hmac_alg = "HS256"
directory = Directory({
'newAccount': 'http://url/acme/new-account',
})
eab = ExternalAccountBinding.from_data(key, kid, hmac_key, directory)
eab = ExternalAccountBinding.from_data(key, kid, hmac_key, directory, hmac_alg)
reg = NewRegistration.from_data(email='admin@foo.com', external_account_binding=eab)
assert reg.contact == (
'mailto:admin@foo.com',
@@ -528,14 +561,25 @@ class NewOrderTest(unittest.TestCase):
def setUp(self):
from acme.messages import NewOrder
self.reg = NewOrder(
self.order = NewOrder(
identifiers=mock.sentinel.identifiers)
def test_to_partial_json(self):
assert self.reg.to_json() == {
assert self.order.to_json() == {
'identifiers': mock.sentinel.identifiers,
}
def test_default_profile_empty(self):
assert self.order.profile is None
def test_non_empty_profile(self):
from acme.messages import NewOrder
order = NewOrder(identifiers=mock.sentinel.identifiers, profile='example')
assert order.to_json() == {
'identifiers': mock.sentinel.identifiers,
'profile': 'example',
}
class JWSPayloadRFC8555Compliant(unittest.TestCase):
"""Test for RFC8555 compliance of JWS generated from resources/challenges"""

View File

@@ -4,7 +4,6 @@ import socket
import socketserver
import sys
import threading
from typing import Set
import unittest
from unittest import mock
@@ -13,29 +12,9 @@ import pytest
import requests
from acme import challenges
from acme import crypto_util
from acme import errors
from acme._internal.tests import test_util
class TLSServerTest(unittest.TestCase):
"""Tests for acme.standalone.TLSServer."""
def test_bind(self): # pylint: disable=no-self-use
from acme.standalone import TLSServer
server = TLSServer(
('', 0), socketserver.BaseRequestHandler, bind_and_activate=True)
server.server_close()
def test_ipv6(self):
if socket.has_ipv6:
from acme.standalone import TLSServer
server = TLSServer(
('', 0), socketserver.BaseRequestHandler, bind_and_activate=True, ipv6=True)
server.server_close()
class HTTP01ServerTest(unittest.TestCase):
"""Tests for acme.standalone.HTTP01Server."""
@@ -43,7 +22,7 @@ class HTTP01ServerTest(unittest.TestCase):
def setUp(self):
self.account_key = jose.JWK.load(
test_util.load_vector('rsa1024_key.pem'))
self.resources: Set = set()
self.resources: set = set()
from acme.standalone import HTTP01Server
self.server = HTTP01Server(('', 0), resources=self.resources)
@@ -110,58 +89,6 @@ class HTTP01ServerTest(unittest.TestCase):
assert not is_hung, 'Server shutdown should not be hung'
@unittest.skipIf(not challenges.TLSALPN01.is_supported(), "pyOpenSSL too old")
class TLSALPN01ServerTest(unittest.TestCase):
"""Test for acme.standalone.TLSALPN01Server."""
def setUp(self):
self.certs = {b'localhost': (
test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
test_util.load_cert('rsa2048_cert.pem'),
)}
# Use different certificate for challenge.
self.challenge_certs = {b'localhost': (
test_util.load_pyopenssl_private_key('rsa4096_key.pem'),
test_util.load_cert('rsa4096_cert.pem'),
)}
from acme.standalone import TLSALPN01Server
self.server = TLSALPN01Server(("localhost", 0), certs=self.certs,
challenge_certs=self.challenge_certs)
# pylint: disable=no-member
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.start()
def tearDown(self):
self.server.shutdown() # pylint: disable=no-member
self.thread.join()
self.server.server_close()
# TODO: This is not implemented yet, see comments in standalone.py
# def test_certs(self):
# host, port = self.server.socket.getsockname()[:2]
# cert = crypto_util.probe_sni(
# b'localhost', host=host, port=port, timeout=1)
# # Expect normal cert when connecting without ALPN.
# self.assertEqual(jose.ComparableX509(cert),
# jose.ComparableX509(self.certs[b'localhost'][1]))
def test_challenge_certs(self):
host, port = self.server.socket.getsockname()[:2]
cert = crypto_util.probe_sni(
b'localhost', host=host, port=port, timeout=1,
alpn_protocols=[b"acme-tls/1"])
# Expect challenge cert when connecting with ALPN.
assert jose.ComparableX509(cert) == \
jose.ComparableX509(self.challenge_certs[b'localhost'][1])
def test_bad_alpn(self):
host, port = self.server.socket.getsockname()[:2]
with pytest.raises(errors.Error):
crypto_util.probe_sni(
b'localhost', host=host, port=port, timeout=1,
alpn_protocols=[b"bad-alpn"])
class BaseDualNetworkedServersTest(unittest.TestCase):
"""Test for acme.standalone.BaseDualNetworkedServers."""
@@ -193,7 +120,7 @@ class BaseDualNetworkedServersTest(unittest.TestCase):
from acme.standalone import BaseDualNetworkedServers
mock_bind.side_effect = socket.error(EADDRINUSE, "Fake addr in use error")
mock_bind.side_effect = OSError(EADDRINUSE, "Fake addr in use error")
with pytest.raises(socket.error) as exc_info:
BaseDualNetworkedServers(
@@ -226,7 +153,7 @@ class HTTP01DualNetworkedServersTest(unittest.TestCase):
def setUp(self):
self.account_key = jose.JWK.load(
test_util.load_vector('rsa1024_key.pem'))
self.resources: Set = set()
self.resources: set = set()
from acme.standalone import HTTP01DualNetworkedServers
self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources)

View File

@@ -0,0 +1,61 @@
"""Test utilities.
.. warning:: This module is not part of the public API.
"""
import importlib.resources
import os
from typing import Callable
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import josepy as jose
from josepy.util import ComparableECKey
def load_vector(*names):
"""Load contents of a test vector."""
# luckily, resource_string opens file in binary mode
vector_ref = importlib.resources.files(__package__).joinpath('testdata', *names)
return vector_ref.read_bytes()
def _guess_loader(filename: str, loader_pem: Callable, loader_der: Callable) -> Callable:
_, ext = os.path.splitext(filename)
if ext.lower() == ".pem":
return loader_pem
elif ext.lower() == ".der":
return loader_der
else: # pragma: no cover
raise ValueError("Loader could not be recognized based on extension")
def load_cert(*names: str) -> x509.Certificate:
"""Load certificate."""
loader = _guess_loader(
names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate
)
return loader(load_vector(*names))
def load_csr(*names: str) -> x509.CertificateSigningRequest:
"""Load certificate request."""
loader = _guess_loader(names[-1], x509.load_pem_x509_csr, x509.load_der_x509_csr)
return loader(load_vector(*names))
def load_rsa_private_key(*names):
"""Load RSA private key."""
loader = _guess_loader(names[-1], serialization.load_pem_private_key,
serialization.load_der_private_key)
return jose.ComparableRSAKey(loader(
load_vector(*names), password=None, backend=default_backend()))
def load_ecdsa_private_key(*names):
"""Load ECDSA private key."""
loader = _guess_loader(names[-1], serialization.load_pem_private_key,
serialization.load_der_private_key)
return ComparableECKey(loader(
load_vector(*names), password=None, backend=default_backend()))

View File

@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICdjCCAV4CAQAwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANoV
T1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svNPSa+oPTK
7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm7Gj6m2Ez
pSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFnxvvOjBYo
p7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTDg7P4UAuF
kejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1RAeEeRTk
h0WjUfltoem/5f8bIdsCAwEAAaAxMC8GCSqGSIb3DQEJDjEiMCAwHgYDVR0RBBcw
FYINYS5leGVtcGxlLmNvbYcEwAACbzANBgkqhkiG9w0BAQsFAAOCAQEAQ7n/hYen
5INHlcslHPYCQ/BAbX6Ou+Y8hUu8puWNVpE2OM95L2C87jbWwTmCRnkFBwtyoNqo
j3DXVW2RYv8y/exq7V6Y5LtpHTgwfugINJ3XlcVzA4Vnf1xqOxv3kwejkq74RuXn
xd5N28srgiFqb0e4tOAWVI8Tw27bgBqjoXl0QDFPZpctqUia5bcDJ9WzNSM7VaO1
CBNGHBRz+zL8sqoqJA4HV58tjcgzl+1RtGM+iUHxXpnH+aCNKWIUINrAzIm4Sm00
93RJjhb1kdNR0BC7ikWVbAWaVviHdvATK/RfpmhWDqfEaNgBpvT91GnkhpzctSFD
ro0yCUUXXrIr0w==
-----END CERTIFICATE REQUEST-----

View File

@@ -1,6 +1,5 @@
"""Tests for acme.util."""
import sys
import unittest
import pytest

View File

@@ -1,29 +1,19 @@
"""ACME Identifier Validation Challenges."""
import abc
import codecs
import functools
import hashlib
import logging
import socket
from typing import Any
from typing import cast
from typing import Dict
from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
from cryptography.hazmat.primitives import hashes
import josepy as jose
from OpenSSL import crypto
from OpenSSL import SSL
import requests
from acme import crypto_util
from acme import errors
logger = logging.getLogger(__name__)
GenericChallenge = TypeVar('GenericChallenge', bound='Challenge')
@@ -32,10 +22,10 @@ GenericChallenge = TypeVar('GenericChallenge', bound='Challenge')
class Challenge(jose.TypedJSONObjectWithFields):
# _fields_to_partial_json
"""ACME challenge."""
TYPES: Dict[str, Type['Challenge']] = {}
TYPES: dict[str, type['Challenge']] = {}
@classmethod
def from_json(cls: Type[GenericChallenge],
def from_json(cls: type[GenericChallenge],
jobj: Mapping[str, Any]) -> Union[GenericChallenge, 'UnrecognizedChallenge']:
try:
return cast(GenericChallenge, super().from_json(jobj))
@@ -47,9 +37,9 @@ class Challenge(jose.TypedJSONObjectWithFields):
class ChallengeResponse(jose.TypedJSONObjectWithFields):
# _fields_to_partial_json
"""ACME challenge response."""
TYPES: Dict[str, Type['ChallengeResponse']] = {}
TYPES: dict[str, type['ChallengeResponse']] = {}
def to_partial_json(self) -> Dict[str, Any]:
def to_partial_json(self) -> dict[str, Any]:
# Removes the `type` field which is inserted by TypedJSONObjectWithFields.to_partial_json.
# This field breaks RFC8555 compliance.
jobj = super().to_partial_json()
@@ -69,13 +59,13 @@ class UnrecognizedChallenge(Challenge):
:ivar jobj: Original JSON decoded object.
"""
jobj: Dict[str, Any]
jobj: dict[str, Any]
def __init__(self, jobj: Mapping[str, Any]) -> None:
super().__init__()
object.__setattr__(self, "jobj", jobj)
def to_partial_json(self) -> Dict[str, Any]:
def to_partial_json(self) -> dict[str, Any]:
return self.jobj # pylint: disable=no-member
@classmethod
@@ -89,7 +79,7 @@ class _TokenChallenge(Challenge):
:ivar bytes token:
"""
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
TOKEN_SIZE = 128 // 8 # Based on the entropy value from the spec
"""Minimum size of the :attr:`token` in bytes."""
# TODO: acme-spec doesn't specify token as base64-encoded value
@@ -154,7 +144,7 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
return True
def to_partial_json(self) -> Dict[str, Any]:
def to_partial_json(self) -> dict[str, Any]:
jobj = super().to_partial_json()
jobj.pop('keyAuthorization', None)
return jobj
@@ -171,7 +161,7 @@ class KeyAuthorizationChallenge(_TokenChallenge, metaclass=abc.ABCMeta):
:param str typ: type of the challenge
"""
typ: str = NotImplemented
response_cls: Type[KeyAuthorizationChallengeResponse] = NotImplemented
response_cls: type[KeyAuthorizationChallengeResponse] = NotImplemented
thumbprint_hash_function = (
KeyAuthorizationChallengeResponse.thumbprint_hash_function)
@@ -214,7 +204,7 @@ class KeyAuthorizationChallenge(_TokenChallenge, metaclass=abc.ABCMeta):
raise NotImplementedError() # pragma: no cover
def response_and_validation(self, account_key: jose.JWK, *args: Any, **kwargs: Any
) -> Tuple[KeyAuthorizationChallengeResponse, Any]:
) -> tuple[KeyAuthorizationChallengeResponse, Any]:
"""Generate response and validation.
Convenience function that return results of `response` and
@@ -397,173 +387,6 @@ class HTTP01(KeyAuthorizationChallenge):
return self.key_authorization(account_key)
@ChallengeResponse.register
class TLSALPN01Response(KeyAuthorizationChallengeResponse):
"""ACME tls-alpn-01 challenge response."""
typ = "tls-alpn-01"
PORT = 443
"""Verification port as defined by the protocol.
You can override it (e.g. for testing) by passing ``port`` to
`simple_verify`.
"""
ID_PE_ACME_IDENTIFIER_V1 = b"1.3.6.1.5.5.7.1.30.1"
ACME_TLS_1_PROTOCOL = b"acme-tls/1"
@property
def h(self) -> bytes:
"""Hash value stored in challenge certificate"""
return hashlib.sha256(self.key_authorization.encode('utf-8')).digest()
def gen_cert(self, domain: str, key: Optional[crypto.PKey] = None, bits: int = 2048
) -> Tuple[crypto.X509, crypto.PKey]:
"""Generate tls-alpn-01 certificate.
:param str domain: Domain verified by the challenge.
:param OpenSSL.crypto.PKey key: Optional private key used in
certificate generation. If not provided (``None``), then
fresh key will be generated.
:param int bits: Number of bits for newly generated key.
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
"""
if key is None:
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, bits)
der_value = b"DER:" + codecs.encode(self.h, 'hex')
acme_extension = crypto.X509Extension(self.ID_PE_ACME_IDENTIFIER_V1,
critical=True, value=der_value)
return crypto_util.gen_ss_cert(key, [domain], force_san=True,
extensions=[acme_extension]), key
def probe_cert(self, domain: str, host: Optional[str] = None,
port: Optional[int] = None) -> crypto.X509:
"""Probe tls-alpn-01 challenge certificate.
:param str domain: domain being validated, required.
:param str host: IP address used to probe the certificate.
:param int port: Port used to probe the certificate.
"""
if host is None:
host = socket.gethostbyname(domain)
logger.debug('%s resolved to %s', domain, host)
if port is None:
port = self.PORT
return crypto_util.probe_sni(host=host.encode(), port=port, name=domain.encode(),
alpn_protocols=[self.ACME_TLS_1_PROTOCOL])
def verify_cert(self, domain: str, cert: crypto.X509) -> bool:
"""Verify tls-alpn-01 challenge certificate.
:param str domain: Domain name being validated.
:param OpensSSL.crypto.X509 cert: Challenge certificate.
:returns: Whether the certificate was successfully verified.
:rtype: bool
"""
# pylint: disable=protected-access
names = crypto_util._pyopenssl_cert_or_req_all_names(cert)
# Type ignore needed due to
# https://github.com/pyca/pyopenssl/issues/730.
logger.debug('Certificate %s. SANs: %s',
cert.digest('sha256'), names)
if len(names) != 1 or names[0].lower() != domain.lower():
return False
for i in range(cert.get_extension_count()):
ext = cert.get_extension(i)
# FIXME: assume this is the ACME extension. Currently there is no
# way to get full OID of an unknown extension from pyopenssl.
if ext.get_short_name() == b'UNDEF':
data = ext.get_data()
return data == self.h
return False
# pylint: disable=too-many-arguments
def simple_verify(self, chall: 'TLSALPN01', domain: str, account_public_key: jose.JWK,
cert: Optional[crypto.X509] = None, host: Optional[str] = None,
port: Optional[int] = None) -> bool:
"""Simple verify.
Verify ``validation`` using ``account_public_key``, optionally
probe tls-alpn-01 certificate and check using `verify_cert`.
:param .challenges.TLSALPN01 chall: Corresponding challenge.
:param str domain: Domain name being validated.
:param JWK account_public_key:
:param OpenSSL.crypto.X509 cert: Optional certificate. If not
provided (``None``) certificate will be retrieved using
`probe_cert`.
:param string host: IP address used to probe the certificate.
:param int port: Port used to probe the certificate.
:returns: ``True`` if and only if client's control of the domain has been verified.
:rtype: bool
"""
if not self.verify(chall, account_public_key):
logger.debug("Verification of key authorization in response failed")
return False
if cert is None:
try:
cert = self.probe_cert(domain=domain, host=host, port=port)
except errors.Error as error:
logger.debug(str(error), exc_info=True)
return False
return self.verify_cert(domain, cert)
@Challenge.register # pylint: disable=too-many-ancestors
class TLSALPN01(KeyAuthorizationChallenge):
"""ACME tls-alpn-01 challenge."""
response_cls = TLSALPN01Response
typ = response_cls.typ
def validation(self, account_key: jose.JWK, **kwargs: Any) -> Tuple[crypto.X509, crypto.PKey]:
"""Generate validation.
:param JWK account_key:
:param str domain: Domain verified by the challenge.
:param OpenSSL.crypto.PKey cert_key: Optional private key used
in certificate generation. If not provided (``None``), then
fresh key will be generated.
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
"""
# TODO: Remove cast when response() is generic.
return cast(TLSALPN01Response, self.response(account_key)).gen_cert(
key=kwargs.get('cert_key'),
domain=cast(str, kwargs.get('domain')))
@staticmethod
def is_supported() -> bool:
"""
Check if TLS-ALPN-01 challenge is supported on this machine.
This implies that a recent version of OpenSSL is installed (>= 1.0.2),
or a recent cryptography version shipped with the OpenSSL library is installed.
:returns: ``True`` if TLS-ALPN-01 is supported on this machine, ``False`` otherwise.
:rtype: bool
"""
return (hasattr(SSL.Connection, "set_alpn_protos")
and hasattr(SSL.Context, "set_alpn_select_callback"))
@Challenge.register
class DNS(_TokenChallenge):
"""ACME "dns" challenge."""

View File

@@ -4,19 +4,18 @@ import datetime
from email.utils import parsedate_tz
import http.client as http_client
import logging
import re
import math
import random
import time
from typing import Any
from typing import cast
from typing import List
from typing import Mapping
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Union
from cryptography import x509
import josepy as jose
import OpenSSL
import requests
from requests.adapters import HTTPAdapter
from requests.utils import parse_header_links
@@ -113,7 +112,7 @@ class ClientV2:
self.net.account = new_regr
return new_regr
def new_order(self, csr_pem: bytes) -> messages.OrderResource:
def new_order(self, csr_pem: bytes, profile: Optional[str] = None) -> messages.OrderResource:
"""Request a new Order object from the server.
:param bytes csr_pem: A CSR in PEM format.
@@ -121,19 +120,24 @@ class ClientV2:
:returns: The newly created order.
:rtype: OrderResource
"""
csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)
# pylint: disable=protected-access
dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr)
ipNames = crypto_util._pyopenssl_cert_or_req_san_ip(csr)
# ipNames is now []string
csr = x509.load_pem_x509_csr(csr_pem)
dnsNames = crypto_util.get_names_from_subject_and_extensions(csr.subject, csr.extensions)
try:
san_ext = csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
except x509.ExtensionNotFound:
ipNames = []
else:
ipNames = san_ext.value.get_values_for_type(x509.IPAddress)
identifiers = []
for name in dnsNames:
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN,
value=name))
for ips in ipNames:
for ip in ipNames:
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_IP,
value=ips))
order = messages.NewOrder(identifiers=identifiers)
value=str(ip)))
if profile is None:
profile = ""
order = messages.NewOrder(identifiers=identifiers, profile=profile)
response = self._post(self.directory['newOrder'], order)
body = messages.Order.from_json(response.json())
authorizations = []
@@ -149,7 +153,7 @@ class ClientV2:
csr_pem=csr_pem)
def poll(self, authzr: messages.AuthorizationResource
) -> Tuple[messages.AuthorizationResource, requests.Response]:
) -> tuple[messages.AuthorizationResource, requests.Response]:
"""Poll Authorization Resource for status.
:param authzr: Authorization Resource
@@ -218,10 +222,14 @@ class ClientV2:
:returns: updated order
:rtype: messages.OrderResource
:raises .messages.Error: If server indicates order is not yet in ready state,
it will return a 403 (Forbidden) error with a problem document/error code of type
"orderNotReady"
"""
csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, orderr.csr_pem)
wrapped_csr = messages.CertificateRequest(csr=jose.ComparableX509(csr))
csr = x509.load_pem_x509_csr(orderr.csr_pem)
wrapped_csr = messages.CertificateRequest(csr=csr)
res = self._post(orderr.body.finalize, wrapped_csr)
orderr = orderr.update(body=messages.Order.from_json(res.json()))
return orderr
@@ -234,21 +242,37 @@ class ClientV2:
Poll an order that has been finalized for its status.
If it becomes valid, obtain the certificate.
If a finalization request previously returned `orderNotReady`,
poll until ready, send a new finalization request, and continue
polling until valid as above.
:returns: finalized order (with certificate)
:rtype: messages.OrderResource
"""
sleep_seconds: float = 1
while datetime.datetime.now() < deadline:
time.sleep(1)
if sleep_seconds > 0:
time.sleep(sleep_seconds)
response = self._post_as_get(orderr.uri)
body = messages.Order.from_json(response.json())
if body.status == messages.STATUS_INVALID:
# "invalid": The certificate will not be issued. Consider this
# order process abandoned.
if body.error is not None:
raise errors.IssuanceError(body.error)
raise errors.Error(
"The certificate order failed. No further information was provided "
"by the server.")
elif body.status == messages.STATUS_READY:
# "ready": The server agrees that the requirements have been
# fulfilled, and is awaiting finalization. Submit a finalization
# request.
self.begin_finalization(orderr)
sleep_seconds = 1
elif body.status == messages.STATUS_VALID and body.certificate is not None:
# "valid": The server has issued the certificate and provisioned its
# URL to the "certificate" field of the order. Download the
# certificate.
certificate_response = self._post_as_get(body.certificate)
orderr = orderr.update(body=body, fullchain_pem=certificate_response.text)
if fetch_alternative_chains:
@@ -256,6 +280,14 @@ class ClientV2:
alt_chains = [self._post_as_get(url).text for url in alt_chains_urls]
orderr = orderr.update(alternative_fullchains_pem=alt_chains)
return orderr
elif body.status == messages.STATUS_PROCESSING:
# "processing": The certificate is being issued. Send a POST-as-GET request after
# the time given in the Retry-After header field of the response, if any.
retry_after = self.retry_after(response, 1)
# Whatever Retry-After the ACME server requests, the polling must not take
# longer than the overall deadline
retry_after = min(retry_after, deadline)
sleep_seconds = (retry_after - datetime.datetime.now()).total_seconds()
raise errors.TimeoutError()
def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.datetime,
@@ -271,14 +303,84 @@ class ClientV2:
:rtype: messages.OrderResource
"""
self.begin_finalization(orderr)
try:
self.begin_finalization(orderr)
except messages.Error as e:
if e.code != 'orderNotReady':
raise e
return self.poll_finalization(orderr, deadline, fetch_alternative_chains)
def revoke(self, cert: jose.ComparableX509, rsn: int) -> None:
def renewal_time(self, cert_pem: bytes
) -> tuple[Optional[datetime.datetime], datetime.datetime]:
"""Return an appropriate time to attempt renewal of the certificate,
and the next time to ask the ACME server for renewal info.
If the certificate has already expired, renewal info isn't checked.
Instead, the certificate's notAfter time is returned and the certificate
should be immediately renewed.
If the ACME directory has a "renewalInfo" field, the response will be
based on a fetch of the renewal info resource for the certificate
(https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html).
If there is no "renewalInfo" field, this function will return a tuple of
None, and the next time to ask the ACME server for renewal info.
This function may make other network calls in the future (e.g., OCSP
or CRL).
:param bytes cert_pem: cert as pem file
:returns: Tuple of time to attempt renewal, next time to ask for renewal info
:raises errors.ARIError: If an error occurs fetching ARI from the
server. Explicit exception chaining is used so the original error
can be accessed through the __cause__ attribute on the ARIError if
desired.
"""
now = datetime.datetime.now()
# https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html#section-4.3.3
default_retry_after = datetime.timedelta(seconds=6 * 60 * 60)
cert = x509.load_pem_x509_certificate(cert_pem)
# from https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html#section-4.3, "Clients
# MUST NOT check a certificate's RenewalInfo after the certificate has expired."
#
# we call datetime.datetime.now here with the UTC argument to create a timezone aware
# datetime object that can be compared with the UTC notAfter from cryptography
if cert.not_valid_after_utc < datetime.datetime.now(datetime.timezone.utc):
return cert.not_valid_after_utc, now + default_retry_after
try:
renewal_info_base_url = self.directory['renewalInfo']
except KeyError:
return None, now + default_retry_after
ari_url = renewal_info_base_url + '/' + _renewal_info_path_component(cert)
try:
resp = self.net.get(ari_url, content_type='application/json')
except Exception as e: # pylint: disable=broad-except
error_msg = f'failed to fetch renewal_info URL {ari_url}'
raise errors.ARIError(error_msg, now + default_retry_after) from e
renewal_info: messages.RenewalInfo = messages.RenewalInfo.from_json(resp.json())
start = renewal_info.suggested_window.start # pylint: disable=no-member
end = renewal_info.suggested_window.end # pylint: disable=no-member
delta_seconds = (end - start).total_seconds()
random_seconds = random.uniform(0, delta_seconds)
random_time = start + datetime.timedelta(seconds=random_seconds)
retry_after = self.retry_after(resp, default_retry_after.seconds)
return random_time, retry_after
def revoke(self, cert: x509.Certificate, rsn: int) -> None:
"""Revoke certificate.
:param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
`.ComparableX509`
:param x509.Certificate cert: `x509.Certificate`
:param int rsn: Reason code for certificate revocation.
@@ -303,7 +405,7 @@ class ClientV2:
new_args = args[:1] + (None,) + args[1:]
return self._post(*new_args, **kwargs)
def _get_links(self, response: requests.Response, relation_type: str) -> List[str]:
def _get_links(self, response: requests.Response, relation_type: str) -> list[str]:
"""
Retrieves all Link URIs of relation_type from the response.
:param requests.Response response: The requests HTTP response.
@@ -314,8 +416,8 @@ class ClientV2:
if 'Link' not in response.headers:
return []
links = parse_header_links(response.headers['Link'])
return [l['url'] for l in links
if 'rel' in l and 'url' in l and l['rel'] == relation_type]
return [link['url'] for link in links
if 'rel' in link and 'url' in link and link['rel'] == relation_type]
@classmethod
def get_directory(cls, url: str, net: 'ClientNetwork') -> messages.Directory:
@@ -466,11 +568,10 @@ class ClientV2:
return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
def _revoke(self, cert: jose.ComparableX509, rsn: int, url: str) -> None:
def _revoke(self, cert: x509.Certificate, rsn: int, url: str) -> None:
"""Revoke certificate.
:param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
`.ComparableX509`
:param .x509.Certificate cert: `x509.Certificate`
:param int rsn: Reason code for certificate revocation.
@@ -500,7 +601,7 @@ class ClientNetwork:
"""Initialize.
:param josepy.JWK key: Account private key
:param josepy.JWK key: Account private key. Required to use .post().
:param messages.RegistrationResource account: Account object. Required if you are
planning to use .post() for anything other than creating a new account;
may be set later after registering.
@@ -509,14 +610,15 @@ class ClientNetwork:
:param str user_agent: String to send as User-Agent header.
:param int timeout: Timeout for requests.
"""
def __init__(self, key: jose.JWK, account: Optional[messages.RegistrationResource] = None,
def __init__(self, key: Optional[jose.JWK] = None,
account: Optional[messages.RegistrationResource] = None,
alg: jose.JWASignature = jose.RS256, verify_ssl: bool = True,
user_agent: str = 'acme-python', timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None:
self.key = key
self.account = account
self.alg = alg
self.verify_ssl = verify_ssl
self._nonces: Set[str] = set()
self._nonces: set[str] = set()
self.user_agent = user_agent
self.session = requests.Session()
self._default_timeout = timeout
@@ -546,16 +648,17 @@ class ClientNetwork:
"""
jobj = obj.json_dumps(indent=2).encode() if obj else b''
logger.debug('JWS payload:\n%s', jobj)
assert self.key
kwargs = {
"alg": self.alg,
"nonce": nonce,
"url": url
"url": url,
"key": self.key
}
# newAccount and revokeCert work without the kid
# newAccount must not have kid
if self.account is not None:
kwargs["kid"] = self.account["uri"]
kwargs["key"] = self.key
return jws.JWS.sign(jobj, **cast(Mapping[str, Any], kwargs)).json_dumps(indent=2)
@classmethod
@@ -643,30 +746,7 @@ class ClientNetwork:
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('User-Agent', self.user_agent)
kwargs.setdefault('timeout', self._default_timeout)
try:
response = self.session.request(method, url, *args, **kwargs)
except requests.exceptions.RequestException as e:
# pylint: disable=pointless-string-statement
"""Requests response parsing
The requests library emits exceptions with a lot of extra text.
We parse them with a regexp to raise a more readable exceptions.
Example:
HTTPSConnectionPool(host='acme-v01.api.letsencrypt.org',
port=443): Max retries exceeded with url: /directory
(Caused by NewConnectionError('
<requests.packages.urllib3.connection.VerifiedHTTPSConnection
object at 0x108356c50>: Failed to establish a new connection:
[Errno 65] No route to host',))"""
# pylint: disable=line-too-long
err_regex = r".*host='(\S*)'.*Max retries exceeded with url\: (\/\w*).*(\[Errno \d+\])([A-Za-z ]*)"
m = re.match(err_regex, str(e))
if m is None:
raise # pragma: no cover
host, path, _err_no, err_msg = m.groups()
raise ValueError(f"Requesting {host}{path}:{err_msg}")
response = self.session.request(method, url, *args, **kwargs)
# If an Accept header was sent in the request, the response may not be
# UTF-8 encoded. In this case, we don't set response.encoding and log
@@ -745,9 +825,30 @@ class ClientNetwork:
def _post_once(self, url: str, obj: jose.JSONDeSerializable,
content_type: str = JOSE_CONTENT_TYPE, **kwargs: Any) -> requests.Response:
new_nonce_url = kwargs.pop('new_nonce_url', None)
if not self.key:
raise errors.Error("acme.ClientNetwork with no private key can't POST.")
data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url)
kwargs.setdefault('headers', {'Content-Type': content_type})
response = self._send_request('POST', url, data=data, **kwargs)
response = self._check_response(response, content_type=content_type)
self._add_nonce(response)
return response
def _renewal_info_path_component(cert: x509.Certificate) -> str:
akid_ext = cert.extensions.get_extension_for_oid(x509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER)
key_identifier = akid_ext.value.key_identifier # type: ignore[attr-defined]
akid_encoded = base64.urlsafe_b64encode(key_identifier).decode('ascii').replace("=", "")
# We add one to the reported bit_length so there is room for the sign bit.
# https://docs.python.org/3/library/stdtypes.html#int.bit_length
# "Return the number of bits necessary to represent an integer in binary, excluding
# the sign and leading zeros"
serial = cert.serial_number
encoded_serial_len = math.ceil((serial.bit_length()+1)/8)
# Serials are encoded as ASN.1 INTEGERS, which means big endian and signed (two's complement).
# https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/#integer-encoding
serial_bytes = serial.to_bytes(encoded_serial_len, byteorder='big', signed=True)
serial_encoded = base64.urlsafe_b64encode(serial_bytes).decode('ascii').replace("=", "")
return f"{akid_encoded}.{serial_encoded}"

View File

@@ -0,0 +1,277 @@
"""Crypto utilities."""
import enum
from datetime import datetime, timedelta, timezone
import ipaddress
import logging
import typing
from typing import Literal
from typing import Optional
from typing import Union
import warnings
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import dsa, rsa, ec, ed25519, ed448, types
from cryptography.hazmat.primitives.serialization import Encoding
from OpenSSL import crypto
logger = logging.getLogger(__name__)
class Format(enum.IntEnum):
"""File format to be used when parsing or serializing X.509 structures.
Backwards compatible with the `FILETYPE_ASN1` and `FILETYPE_PEM` constants
from pyOpenSSL.
"""
DER = crypto.FILETYPE_ASN1
PEM = crypto.FILETYPE_PEM
def to_cryptography_encoding(self) -> Encoding:
"""Converts the Format to the corresponding cryptography `Encoding`.
"""
if self == Format.DER:
return Encoding.DER
else:
return Encoding.PEM
# Even *more* annoyingly, due to a mypy bug, we can't use Union[] types in
# isinstance expressions without causing false mypy errors. So we have to
# recreate the type collection as a tuple here. And no, typing.get_args doesn't
# work due to another mypy bug.
#
# mypy issues:
# * https://github.com/python/mypy/issues/17680
# * https://github.com/python/mypy/issues/15106
CertificateIssuerPrivateKeyTypesTpl = (
dsa.DSAPrivateKey,
rsa.RSAPrivateKey,
ec.EllipticCurvePrivateKey,
ed25519.Ed25519PrivateKey,
ed448.Ed448PrivateKey,
)
def make_csr(
private_key_pem: bytes,
domains: Optional[Union[set[str], list[str]]] = None,
must_staple: bool = False,
ipaddrs: Optional[list[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None,
) -> bytes:
"""Generate a CSR containing domains or IPs as subjectAltNames.
Parameters are ordered this way for backwards compatibility when called using positional
arguments.
:param buffer private_key_pem: Private key, in PEM PKCS#8 format.
:param list domains: List of DNS names to include in subjectAltNames of CSR.
:param bool must_staple: Whether to include the TLS Feature extension (aka
OCSP Must Staple: https://tools.ietf.org/html/rfc7633).
:param list ipaddrs: List of IPaddress(type ipaddress.IPv4Address or ipaddress.IPv6Address)
names to include in subbjectAltNames of CSR.
:returns: buffer PEM-encoded Certificate Signing Request.
"""
private_key = serialization.load_pem_private_key(private_key_pem, password=None)
if not isinstance(private_key, CertificateIssuerPrivateKeyTypesTpl):
raise ValueError(f"Invalid private key type: {type(private_key)}")
if domains is None:
domains = []
if ipaddrs is None:
ipaddrs = []
if len(domains) + len(ipaddrs) == 0:
raise ValueError(
"At least one of domains or ipaddrs parameter need to be not empty"
)
builder = (
x509.CertificateSigningRequestBuilder()
.subject_name(x509.Name([]))
.add_extension(
x509.SubjectAlternativeName(
[x509.DNSName(d) for d in domains]
+ [x509.IPAddress(i) for i in ipaddrs]
),
critical=False,
)
)
if must_staple:
builder = builder.add_extension(
# "status_request" is the feature commonly known as OCSP
# Must-Staple
x509.TLSFeature([x509.TLSFeatureType.status_request]),
critical=False,
)
csr = builder.sign(private_key, hashes.SHA256())
return csr.public_bytes(Encoding.PEM)
def get_names_from_subject_and_extensions(
subject: x509.Name, exts: x509.Extensions
) -> list[str]:
"""Gets all DNS SAN names as well as the first Common Name from subject.
:param subject: Name of the x509 object, which may include Common Name
:type subject: `cryptography.x509.Name`
:param exts: Extensions of the x509 object, which may include SANs
:type exts: `cryptography.x509.Extensions`
:returns: List of DNS Subject Alternative Names and first Common Name
:rtype: `list` of `str`
"""
# We know these are always `str` because `bytes` is only possible for
# other OIDs.
cns = [
typing.cast(str, c.value)
for c in subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
]
try:
san_ext = exts.get_extension_for_class(x509.SubjectAlternativeName)
except x509.ExtensionNotFound:
dns_names = []
else:
dns_names = san_ext.value.get_values_for_type(x509.DNSName)
if not cns:
return dns_names
else:
# We only include the first CN, if there are multiple. This matches
# the behavior of the previous implementation using pyOpenSSL.
return [cns[0]] + [d for d in dns_names if d != cns[0]]
def _cryptography_cert_or_req_san(
cert_or_req: Union[x509.Certificate, x509.CertificateSigningRequest],
) -> list[str]:
"""Get Subject Alternative Names from certificate or CSR using cryptography.
.. note:: Although this is `acme` internal API, it is used by
`letsencrypt`.
:param cert_or_req: Certificate or CSR.
:type cert_or_req: `x509.Certificate` or `x509.CertificateSigningRequest`.
:returns: A list of Subject Alternative Names that is DNS.
:rtype: `list` of `str`
Deprecated
.. deprecated: 3.2.1
"""
# ???: is this translation needed?
exts = cert_or_req.extensions
try:
san_ext = exts.get_extension_for_class(x509.SubjectAlternativeName)
except x509.ExtensionNotFound:
return []
return san_ext.value.get_values_for_type(x509.DNSName)
# Helper function that can be mocked in unit tests
def _now() -> datetime:
return datetime.now(tz=timezone.utc)
def make_self_signed_cert(private_key: types.CertificateIssuerPrivateKeyTypes,
domains: Optional[list[str]] = None,
not_before: Optional[datetime] = None,
validity: Optional[timedelta] = None, force_san: bool = True,
extensions: Optional[list[x509.Extension]] = None,
ips: Optional[list[Union[ipaddress.IPv4Address,
ipaddress.IPv6Address]]] = None
) -> x509.Certificate:
"""Generate new self-signed certificate.
:param buffer private_key_pem: Private key, in PEM PKCS#8 format.
:type domains: `list` of `str`
:param int not_before: A datetime after which the cert is valid. If no
timezone is specified, UTC is assumed
:type not_before: `datetime.datetime`
:param validity: Duration for which the cert will be valid. Defaults to 1
week
:type validity: `datetime.timedelta`
:param buffer private_key_pem: One of
`cryptography.hazmat.primitives.asymmetric.types.CertificateIssuerPrivateKeyTypes`
:param bool force_san:
:param extensions: List of additional extensions to include in the cert.
:type extensions: `list` of `x509.Extension[x509.ExtensionType]`
:type ips: `list` of (`ipaddress.IPv4Address` or `ipaddress.IPv6Address`)
If more than one domain is provided, all of the domains are put into
``subjectAltName`` X.509 extension and first domain is set as the
subject CN. If only one domain is provided no ``subjectAltName``
extension is used, unless `force_san` is ``True``.
"""
warnings.warn("make_self_signed_cert is deprecated and will be removed in "
"an upcoming release", DeprecationWarning)
assert domains or ips, "Must provide one or more hostnames or IPs for the cert."
builder = x509.CertificateBuilder()
builder = builder.serial_number(x509.random_serial_number())
if extensions is not None:
for ext in extensions:
builder = builder.add_extension(ext.value, ext.critical)
if domains is None:
domains = []
if ips is None:
ips = []
builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
name_attrs = []
if len(domains) > 0:
name_attrs.append(x509.NameAttribute(
x509.OID_COMMON_NAME,
domains[0]
))
builder = builder.subject_name(x509.Name(name_attrs))
builder = builder.issuer_name(x509.Name(name_attrs))
sanlist: list[x509.GeneralName] = []
for address in domains:
sanlist.append(x509.DNSName(address))
for ip in ips:
sanlist.append(x509.IPAddress(ip))
if force_san or len(domains) > 1 or len(ips) > 0:
builder = builder.add_extension(
x509.SubjectAlternativeName(sanlist),
critical=False
)
if not_before is None:
not_before = _now()
if validity is None:
validity = timedelta(seconds=7 * 24 * 60 * 60)
builder = builder.not_valid_before(not_before)
builder = builder.not_valid_after(not_before + validity)
public_key = private_key.public_key()
builder = builder.public_key(public_key)
return builder.sign(private_key, hashes.SHA256())
def dump_cryptography_chain(
chain: list[x509.Certificate],
encoding: Literal[Encoding.PEM, Encoding.DER] = Encoding.PEM,
) -> bytes:
"""Dump certificate chain into a bundle.
:param list chain: List of `cryptography.x509.Certificate`.
:returns: certificate chain bundle
:rtype: bytes
Deprecated
.. deprecated: 3.2.1
"""
# XXX: returns empty string when no chain is available, which
# shuts up RenewableCert, but might not be the best solution...
def _dump_cert(cert: x509.Certificate) -> bytes:
return cert.public_bytes(encoding)
# assumes that x509.Certificate.public_bytes includes ending
# newline character
return b"".join(_dump_cert(cert) for cert in chain)

View File

@@ -1,9 +1,8 @@
"""ACME errors."""
import datetime
import typing
from typing import Any
from typing import List
from typing import Mapping
from typing import Set
from josepy import errors as jose_errors
import requests
@@ -81,7 +80,7 @@ class PollError(ClientError):
to the most recently updated one
"""
def __init__(self, exhausted: Set['messages.AuthorizationResource'],
def __init__(self, exhausted: set['messages.AuthorizationResource'],
updated: Mapping['messages.AuthorizationResource',
'messages.AuthorizationResource']
) -> None:
@@ -103,10 +102,20 @@ class ValidationError(Error):
"""Error for authorization failures. Contains a list of authorization
resources, each of which is invalid and should have an error field.
"""
def __init__(self, failed_authzrs: List['messages.AuthorizationResource']) -> None:
def __init__(self, failed_authzrs: list['messages.AuthorizationResource']) -> None:
self.failed_authzrs = failed_authzrs
super().__init__()
def __str__(self) -> str:
msg = []
for authzr in self.failed_authzrs:
msg.append(f'Authorization for {authzr.body.identifier.value} ' \
'failed due to one or more failed challenges:')
for challenge in authzr.body.challenges:
msg.append(f' Challenge {challenge.chall.typ} failed ' \
f'with error {str(challenge.error)}')
return '\n'.join(msg)
class TimeoutError(Error): # pylint: disable=redefined-builtin
"""Error for when polling an authorization or an order times out."""
@@ -139,3 +148,10 @@ class ConflictError(ClientError):
class WildcardUnsupportedError(Error):
"""Error for when a wildcard is requested but is unsupported by ACME CA."""
class ARIError(ClientError):
"""An error occurred during an ARI request and we want to suggest a Retry-After time."""
def __init__(self, message: str, retry_after: datetime.datetime) -> None:
super().__init__(message, retry_after)
self.retry_after = retry_after

View File

@@ -34,7 +34,7 @@ class RFC3339Field(jose.Field):
Handles decoding/encoding between RFC3339 strings and aware (not
naive) `datetime.datetime` objects
(e.g. ``datetime.datetime.now(pytz.UTC)``).
(e.g. ``datetime.datetime.now(datetime.timezone.utc)``).
"""

View File

@@ -3,16 +3,14 @@ from collections.abc import Hashable
import datetime
import json
from typing import Any
from typing import Dict
from typing import Iterator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TypeVar
from cryptography import x509
import josepy as jose
from acme import challenges
@@ -71,7 +69,7 @@ def is_acme_error(err: BaseException) -> bool:
class _Constant(jose.JSONDeSerializable, Hashable):
"""ACME constant."""
__slots__ = ('name',)
POSSIBLE_NAMES: Dict[str, '_Constant'] = NotImplemented
POSSIBLE_NAMES: dict[str, '_Constant'] = NotImplemented
def __init__(self, name: str) -> None:
super().__init__()
@@ -99,7 +97,7 @@ class _Constant(jose.JSONDeSerializable, Hashable):
class IdentifierType(_Constant):
"""ACME identifier type."""
POSSIBLE_NAMES: Dict[str, _Constant] = {}
POSSIBLE_NAMES: dict[str, _Constant] = {}
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
@@ -138,12 +136,12 @@ class Error(jose.JSONObjectWithFields, errors.Error):
detail: str = jose.field('detail', omitempty=True)
identifier: Optional['Identifier'] = jose.field(
'identifier', decoder=Identifier.from_json, omitempty=True)
subproblems: Optional[Tuple['Error', ...]] = jose.field('subproblems', omitempty=True)
subproblems: Optional[tuple['Error', ...]] = jose.field('subproblems', omitempty=True)
# Mypy does not understand the josepy magic happening here, and falsely claims
# that subproblems is redefined. Let's ignore the type check here.
@subproblems.decoder # type: ignore
def subproblems(value: List[Dict[str, Any]]) -> Tuple['Error', ...]: # pylint: disable=no-self-argument,missing-function-docstring
def subproblems(value: list[dict[str, Any]]) -> tuple['Error', ...]: # pylint: disable=no-self-argument,missing-function-docstring
return tuple(Error.from_json(subproblem) for subproblem in value)
@classmethod
@@ -206,7 +204,7 @@ class Error(jose.JSONObjectWithFields, errors.Error):
class Status(_Constant):
"""ACME "status" field."""
POSSIBLE_NAMES: Dict[str, _Constant] = {}
POSSIBLE_NAMES: dict[str, _Constant] = {}
STATUS_UNKNOWN = Status('unknown')
@@ -229,8 +227,9 @@ class Directory(jose.JSONDeSerializable):
"""Directory Meta."""
_terms_of_service: str = jose.field('termsOfService', omitempty=True)
website: str = jose.field('website', omitempty=True)
caa_identities: List[str] = jose.field('caaIdentities', omitempty=True)
caa_identities: list[str] = jose.field('caaIdentities', omitempty=True)
external_account_required: bool = jose.field('externalAccountRequired', omitempty=True)
profiles: dict[str, str] = jose.field('profiles', omitempty=True)
def __init__(self, **kwargs: Any) -> None:
kwargs = {self._internal_name(k): v for k, v in kwargs.items()}
@@ -265,7 +264,7 @@ class Directory(jose.JSONDeSerializable):
except KeyError:
raise KeyError(f'Directory field "{name}" not found')
def to_partial_json(self) -> Dict[str, Any]:
def to_partial_json(self) -> dict[str, Any]:
return util.map_keys(self._jobj, lambda k: k)
@classmethod
@@ -301,15 +300,26 @@ class ExternalAccountBinding:
@classmethod
def from_data(cls, account_public_key: jose.JWK, kid: str, hmac_key: str,
directory: Directory) -> Dict[str, Any]:
directory: Directory, hmac_alg: str = "HS256") -> dict[str, Any]:
"""Create External Account Binding Resource from contact details, kid and hmac."""
key_json = json.dumps(account_public_key.to_partial_json()).encode()
decoded_hmac_key = jose.b64.b64decode(hmac_key)
url = directory["newAccount"]
hmac_alg_map = {
"HS256": jose.jwa.HS256,
"HS384": jose.jwa.HS384,
"HS512": jose.jwa.HS512,
}
alg = hmac_alg_map.get(hmac_alg)
if alg is None:
supported = ", ".join(hmac_alg_map.keys())
raise ValueError(f"Invalid value for hmac_alg: {hmac_alg}. "
f"Expected one of: {supported}.")
eab = jws.JWS.sign(key_json, jose.jwk.JWKOct(key=decoded_hmac_key),
jose.jwa.HS256, None,
alg, None,
url, kid)
return eab.to_partial_json()
@@ -333,21 +343,21 @@ class Registration(ResourceBody):
# Contact field implements special behavior to allow messages that clear existing
# contacts while not expecting the `contact` field when loading from json.
# This is implemented in the constructor and *_json methods.
contact: Tuple[str, ...] = jose.field('contact', omitempty=True, default=())
contact: tuple[str, ...] = jose.field('contact', omitempty=True, default=())
agreement: str = jose.field('agreement', omitempty=True)
status: Status = jose.field('status', omitempty=True)
terms_of_service_agreed: bool = jose.field('termsOfServiceAgreed', omitempty=True)
only_return_existing: bool = jose.field('onlyReturnExisting', omitempty=True)
external_account_binding: Dict[str, Any] = jose.field('externalAccountBinding',
external_account_binding: dict[str, Any] = jose.field('externalAccountBinding',
omitempty=True)
phone_prefix = 'tel:'
email_prefix = 'mailto:'
@classmethod
def from_data(cls: Type[GenericRegistration], phone: Optional[str] = None,
def from_data(cls: type[GenericRegistration], phone: Optional[str] = None,
email: Optional[str] = None,
external_account_binding: Optional[Dict[str, Any]] = None,
external_account_binding: Optional[dict[str, Any]] = None,
**kwargs: Any) -> GenericRegistration:
"""
Create registration resource from contact details.
@@ -384,12 +394,12 @@ class Registration(ResourceBody):
object.__setattr__(self, '_add_contact', True)
super().__init__(**kwargs)
def _filter_contact(self, prefix: str) -> Tuple[str, ...]:
def _filter_contact(self, prefix: str) -> tuple[str, ...]:
return tuple(
detail[len(prefix):] for detail in self.contact # pylint: disable=not-an-iterable
if detail.startswith(prefix))
def _add_contact_if_appropriate(self, jobj: Dict[str, Any]) -> Dict[str, Any]:
def _add_contact_if_appropriate(self, jobj: dict[str, Any]) -> dict[str, Any]:
"""
The `contact` member of Registration objects should not be required when
de-serializing (as it would be if the Fields' `omitempty` flag were `False`), but
@@ -406,23 +416,23 @@ class Registration(ResourceBody):
return jobj
def to_partial_json(self) -> Dict[str, Any]:
def to_partial_json(self) -> dict[str, Any]:
"""Modify josepy.JSONDeserializable.to_partial_json()"""
jobj = super().to_partial_json()
return self._add_contact_if_appropriate(jobj)
def fields_to_partial_json(self) -> Dict[str, Any]:
def fields_to_partial_json(self) -> dict[str, Any]:
"""Modify josepy.JSONObjectWithFields.fields_to_partial_json()"""
jobj = super().fields_to_partial_json()
return self._add_contact_if_appropriate(jobj)
@property
def phones(self) -> Tuple[str, ...]:
def phones(self) -> tuple[str, ...]:
"""All phones found in the ``contact`` field."""
return self._filter_contact(self.phone_prefix)
@property
def emails(self) -> Tuple[str, ...]:
def emails(self) -> tuple[str, ...]:
"""All emails found in the ``contact`` field."""
return self._filter_contact(self.email_prefix)
@@ -484,13 +494,13 @@ class ChallengeBody(ResourceBody):
def encode(self, name: str) -> Any:
return super().encode(self._internal_name(name))
def to_partial_json(self) -> Dict[str, Any]:
def to_partial_json(self) -> dict[str, Any]:
jobj = super().to_partial_json()
jobj.update(self.chall.to_partial_json())
return jobj
@classmethod
def fields_from_json(cls, jobj: Mapping[str, Any]) -> Dict[str, Any]:
def fields_from_json(cls, jobj: Mapping[str, Any]) -> dict[str, Any]:
jobj_fields = super().fields_from_json(jobj)
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
return jobj_fields
@@ -539,7 +549,7 @@ class Authorization(ResourceBody):
"""
identifier: Identifier = jose.field('identifier', decoder=Identifier.from_json, omitempty=True)
challenges: List[ChallengeBody] = jose.field('challenges', omitempty=True)
challenges: list[ChallengeBody] = jose.field('challenges', omitempty=True)
status: Status = jose.field('status', omitempty=True, decoder=Status.from_json)
# TODO: 'expires' is allowed for Authorization Resources in
@@ -552,7 +562,7 @@ class Authorization(ResourceBody):
# Mypy does not understand the josepy magic happening here, and falsely claims
# that challenge is redefined. Let's ignore the type check here.
@challenges.decoder # type: ignore
def challenges(value: List[Dict[str, Any]]) -> Tuple[ChallengeBody, ...]: # pylint: disable=no-self-argument,missing-function-docstring
def challenges(value: list[dict[str, Any]]) -> tuple[ChallengeBody, ...]: # pylint: disable=no-self-argument,missing-function-docstring
return tuple(ChallengeBody.from_json(chall) for chall in value)
@@ -578,34 +588,32 @@ class AuthorizationResource(ResourceWithURI):
class CertificateRequest(jose.JSONObjectWithFields):
"""ACME newOrder request.
:ivar jose.ComparableX509 csr:
`OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
:ivar x509.CertificateSigningRequest csr: `x509.CertificateSigningRequest`
"""
csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
csr: x509.CertificateSigningRequest = jose.field(
'csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
class CertificateResource(ResourceWithURI):
"""Certificate Resource.
:ivar josepy.util.ComparableX509 body:
`OpenSSL.crypto.X509` wrapped in `.ComparableX509`
:ivar x509.Certificate body: `x509.Certificate`
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
"""
cert_chain_uri: str = jose.field('cert_chain_uri')
authzrs: Tuple[AuthorizationResource, ...] = jose.field('authzrs')
authzrs: tuple[AuthorizationResource, ...] = jose.field('authzrs')
class Revocation(jose.JSONObjectWithFields):
"""Revocation message.
:ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
`jose.ComparableX509`
:ivar x509.Certificate certificate: `x509.Certificate`
"""
certificate: jose.ComparableX509 = jose.field(
certificate: x509.Certificate = jose.field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
reason: int = jose.field('reason')
@@ -613,6 +621,8 @@ class Revocation(jose.JSONObjectWithFields):
class Order(ResourceBody):
"""Order Resource Body.
:ivar profile: The profile to request.
:vartype profile: str
:ivar identifiers: List of identifiers for the certificate.
:vartype identifiers: `list` of `.Identifier`
:ivar acme.messages.Status status:
@@ -624,9 +634,11 @@ class Order(ResourceBody):
:ivar datetime.datetime expires: When the order expires.
:ivar ~.Error error: Any error that occurred during finalization, if applicable.
"""
identifiers: List[Identifier] = jose.field('identifiers', omitempty=True)
# https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/
profile: str = jose.field('profile', omitempty=True)
identifiers: list[Identifier] = jose.field('identifiers', omitempty=True)
status: Status = jose.field('status', decoder=Status.from_json, omitempty=True)
authorizations: List[str] = jose.field('authorizations', omitempty=True)
authorizations: list[str] = jose.field('authorizations', omitempty=True)
certificate: str = jose.field('certificate', omitempty=True)
finalize: str = jose.field('finalize', omitempty=True)
expires: datetime.datetime = fields.rfc3339('expires', omitempty=True)
@@ -635,7 +647,7 @@ class Order(ResourceBody):
# Mypy does not understand the josepy magic happening here, and falsely claims
# that identifiers is redefined. Let's ignore the type check here.
@identifiers.decoder # type: ignore
def identifiers(value: List[Dict[str, Any]]) -> Tuple[Identifier, ...]: # pylint: disable=no-self-argument,missing-function-docstring
def identifiers(value: list[dict[str, Any]]) -> tuple[Identifier, ...]: # pylint: disable=no-self-argument,missing-function-docstring
return tuple(Identifier.from_json(identifier) for identifier in value)
@@ -665,17 +677,33 @@ class OrderResource(ResourceWithURI):
decoder=lambda s: s.encode("utf-8"),
encoder=lambda b: b.decode("utf-8"))
authorizations: List[AuthorizationResource] = jose.field('authorizations')
authorizations: list[AuthorizationResource] = jose.field('authorizations')
fullchain_pem: str = jose.field('fullchain_pem', omitempty=True)
alternative_fullchains_pem: List[str] = jose.field('alternative_fullchains_pem',
alternative_fullchains_pem: list[str] = jose.field('alternative_fullchains_pem',
omitempty=True)
# Mypy does not understand the josepy magic happening here, and falsely claims
# that authorizations is redefined. Let's ignore the type check here.
@authorizations.decoder # type: ignore
def authorizations(value: List[Dict[str, Any]]) -> Tuple[AuthorizationResource, ...]: # pylint: disable=no-self-argument,missing-function-docstring
def authorizations(value: list[dict[str, Any]]) -> tuple[AuthorizationResource, ...]: # pylint: disable=no-self-argument,missing-function-docstring
return tuple(AuthorizationResource.from_json(authz) for authz in value)
class NewOrder(Order):
"""New order."""
class RenewalInfo(ResourceBody):
"""Renewal Info Resource Body.
:ivar acme.messages.SuggestedWindow window: The suggested renewal window.
"""
class SuggestedWindow(jose.JSONObjectWithFields):
"""Suggested Renewal Window, sub-resource of Renewal Info Resource.
:ivar datetime.datetime start: Beginning of suggested renewal window
:ivar datetime.datetime end: End of suggested renewal window (inclusive)
"""
start: datetime.datetime = fields.rfc3339('start', omitempty=True)
end: datetime.datetime = fields.rfc3339('end', omitempty=True)
suggested_window: SuggestedWindow = jose.field('suggestedWindow',
decoder=SuggestedWindow.from_json)

View File

@@ -1,4 +1,6 @@
"""Support for standalone client challenge solvers. """
from __future__ import annotations
import collections
import functools
import http.client as http_client
@@ -8,56 +10,13 @@ import socket
import socketserver
import threading
from typing import Any
from typing import cast
from typing import List
from typing import Mapping
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Type
from OpenSSL import crypto
from OpenSSL import SSL
from acme import challenges
from acme import crypto_util
logger = logging.getLogger(__name__)
class TLSServer(socketserver.TCPServer):
"""Generic TLS Server."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.ipv6 = kwargs.pop("ipv6", False)
if self.ipv6:
self.address_family = socket.AF_INET6
else:
self.address_family = socket.AF_INET
self.certs = kwargs.pop("certs", {})
self.method = kwargs.pop("method", crypto_util._DEFAULT_SSL_METHOD)
self.allow_reuse_address = kwargs.pop("allow_reuse_address", True)
super().__init__(*args, **kwargs)
def _wrap_sock(self) -> None:
self.socket = cast(socket.socket, crypto_util.SSLSocket(
self.socket, cert_selection=self._cert_selection,
alpn_selection=getattr(self, '_alpn_selection', None),
method=self.method))
def _cert_selection(self, connection: SSL.Connection
) -> Optional[Tuple[crypto.PKey, crypto.X509]]: # pragma: no cover
"""Callback selecting certificate for connection."""
server_name = connection.get_servername()
if server_name:
return self.certs.get(server_name, None)
return None
def server_bind(self) -> None:
self._wrap_sock()
return socketserver.TCPServer.server_bind(self)
class ACMEServerMixin:
"""ACME server common settings mixin."""
# TODO: c.f. #858
@@ -73,11 +32,11 @@ class BaseDualNetworkedServers:
If two servers are instantiated, they will serve on the same port.
"""
def __init__(self, ServerClass: Type[socketserver.TCPServer], server_address: Tuple[str, int],
def __init__(self, ServerClass: type[socketserver.TCPServer], server_address: tuple[str, int],
*remaining_args: Any, **kwargs: Any) -> None:
port = server_address[1]
self.threads: List[threading.Thread] = []
self.servers: List[socketserver.BaseServer] = []
self.threads: list[threading.Thread] = []
self.servers: list[socketserver.BaseServer] = []
# Preserve socket error for re-raising, if no servers can be started
last_socket_err: Optional[socket.error] = None
@@ -98,7 +57,7 @@ class BaseDualNetworkedServers:
logger.debug(
"Successfully bound to %s:%s using %s", new_address[0],
new_address[1], "IPv6" if ip_version else "IPv4")
except socket.error as e:
except OSError as e:
last_socket_err = e
if self.servers:
# Already bound using IPv6.
@@ -121,7 +80,7 @@ class BaseDualNetworkedServers:
if last_socket_err:
raise last_socket_err
else: # pragma: no cover
raise socket.error("Could not bind to IPv4 or IPv6.")
raise OSError("Could not bind to IPv4 or IPv6.")
def serve_forever(self) -> None:
"""Wraps socketserver.TCPServer.serve_forever"""
@@ -131,7 +90,7 @@ class BaseDualNetworkedServers:
thread.start()
self.threads.append(thread)
def getsocknames(self) -> List[Tuple[str, int]]:
def getsocknames(self) -> list[tuple[str, int]]:
"""Wraps socketserver.TCPServer.socket.getsockname"""
return [server.socket.getsockname() for server in self.servers]
@@ -146,49 +105,6 @@ class BaseDualNetworkedServers:
self.threads = []
class TLSALPN01Server(TLSServer, ACMEServerMixin):
"""TLSALPN01 Server."""
ACME_TLS_1_PROTOCOL = b"acme-tls/1"
def __init__(self, server_address: Tuple[str, int],
certs: List[Tuple[crypto.PKey, crypto.X509]],
challenge_certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]],
ipv6: bool = False) -> None:
# We don't need to implement a request handler here because the work
# (including logging) is being done by wrapped socket set up in the
# parent TLSServer class.
TLSServer.__init__(
self, server_address, socketserver.BaseRequestHandler, certs=certs,
ipv6=ipv6)
self.challenge_certs = challenge_certs
def _cert_selection(self, connection: SSL.Connection) -> Optional[Tuple[crypto.PKey,
crypto.X509]]:
# TODO: We would like to serve challenge cert only if asked for it via
# ALPN. To do this, we need to retrieve the list of protos from client
# hello, but this is currently impossible with openssl [0], and ALPN
# negotiation is done after cert selection.
# Therefore, currently we always return challenge cert, and terminate
# handshake in alpn_selection() if ALPN protos are not what we expect.
# [0] https://github.com/openssl/openssl/issues/4952
server_name = connection.get_servername()
if server_name:
logger.debug("Serving challenge cert for server name %s", server_name)
return self.challenge_certs[server_name]
return None # pragma: no cover
def _alpn_selection(self, _connection: SSL.Connection, alpn_protos: List[bytes]) -> bytes:
"""Callback to select alpn protocol."""
if len(alpn_protos) == 1 and alpn_protos[0] == self.ACME_TLS_1_PROTOCOL:
logger.debug("Agreed on %s ALPN", self.ACME_TLS_1_PROTOCOL)
return self.ACME_TLS_1_PROTOCOL
logger.debug("Cannot agree on ALPN proto. Got: %s", str(alpn_protos))
# Explicitly close the connection now, by returning an empty string.
# See https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_alpn_select_callback # pylint: disable=line-too-long
return b""
class HTTPServer(BaseHTTPServer.HTTPServer):
"""Generic HTTP Server."""
@@ -204,7 +120,8 @@ class HTTPServer(BaseHTTPServer.HTTPServer):
class HTTP01Server(HTTPServer, ACMEServerMixin):
"""HTTP01 Server."""
def __init__(self, server_address: Tuple[str, int], resources: Set[challenges.HTTP01],
def __init__(self, server_address: tuple[str, int],
resources: set[HTTP01RequestHandler.HTTP01Resource],
ipv6: bool = False, timeout: int = 30) -> None:
super().__init__(
server_address, HTTP01RequestHandler.partial_init(
@@ -299,8 +216,8 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
self.path)
@classmethod
def partial_init(cls, simple_http_resources: Set[challenges.HTTP01],
timeout: int) -> 'functools.partial[HTTP01RequestHandler]':
def partial_init(cls, simple_http_resources: set[HTTP01RequestHandler.HTTP01Resource],
timeout: int) -> functools.partial[HTTP01RequestHandler]:
"""Partially initialize this handler.
This is useful because `socketserver.BaseServer` takes

View File

@@ -1,10 +1,9 @@
"""ACME utilities."""
from typing import Any
from typing import Callable
from typing import Dict
from typing import Mapping
def map_keys(dikt: Mapping[Any, Any], func: Callable[[Any], Any]) -> Dict[Any, Any]:
def map_keys(dikt: Mapping[Any, Any], func: Callable[[Any], Any]) -> dict[Any, Any]:
"""Map dictionary keys."""
return {func(key): value for key, value in dikt.items()}

View File

@@ -1,8 +1,8 @@
include LICENSE.txt
include README.rst
recursive-include certbot_apache/_internal/augeas_lens *.aug
recursive-include certbot_apache/_internal/tls_configs *.conf
recursive-include certbot_apache/_internal/tests/testdata *
include certbot_apache/py.typed
recursive-include src/certbot_apache/_internal/augeas_lens *.aug
recursive-include src/certbot_apache/_internal/tls_configs *.conf
recursive-include src/certbot_apache/_internal/tests/testdata *
include src/certbot_apache/py.typed
global-exclude __pycache__
global-exclude *.py[cod]

View File

@@ -0,0 +1,52 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "certbot-apache"
dynamic = ["version", "dependencies"]
description = "Apache plugin for Certbot"
readme = "README.rst"
license = "Apache-2.0"
requires-python = ">=3.10"
authors = [
{ name = "Certbot Project", email = "certbot-dev@eff.org" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Plugins",
"Intended Audience :: System Administrators",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Security",
"Topic :: System :: Installation/Setup",
"Topic :: System :: Networking",
"Topic :: System :: Systems Administration",
"Topic :: Utilities",
]
[project.optional-dependencies]
dev = [
"apacheconfig>=0.3.2",
]
test = [
"pytest",
]
[project.entry-points."certbot.plugins"]
apache = "certbot_apache._internal.entrypoint:ENTRYPOINT"
[project.urls]
Homepage = "https://github.com/certbot/certbot"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]

View File

@@ -1,7 +1,6 @@
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '5.2.0.dev0'
install_requires = [
# We specify the minimum acme and certbot version as the current plugin
@@ -9,59 +8,10 @@ install_requires = [
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
'importlib_resources>=1.3.1; python_version < "3.9"',
'python-augeas',
'setuptools>=41.6.0',
]
dev_extras = [
'apacheconfig>=0.3.2',
]
test_extras = [
'pytest',
]
setup(
name='certbot-apache',
version=version,
description="Apache plugin for Certbot",
url='https://github.com/certbot/certbot',
author="Certbot Project",
author_email='certbot-dev@eff.org',
license='Apache License 2.0',
python_requires='>=3.8',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',
'Topic :: System :: Networking',
'Topic :: System :: Systems Administration',
'Topic :: Utilities',
],
packages=find_packages(),
include_package_data=True,
install_requires=install_requires,
extras_require={
'dev': dev_extras,
'test': test_extras,
},
entry_points={
'certbot.plugins': [
'apache = certbot_apache._internal.entrypoint:ENTRYPOINT',
],
},
)

Some files were not shown because too many files have changed in this diff Show More