Compare commits

...

225 Commits

Author SHA1 Message Date
Brad Warren
c93a261aad Release 4.1.1 2025-06-12 11:09:57 -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
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
829 changed files with 4473 additions and 3678 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:

View File

@@ -4,7 +4,7 @@ jobs:
- name: IMAGE_NAME
value: ubuntu-22.04
- name: PYTHON_VERSION
value: 3.12
value: 3.13
- group: certbot-common
strategy:
matrix:
@@ -17,16 +17,17 @@ jobs:
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.9
TOXENV: integration-certbot-oldest
linux-integration-nginx-oldest:
PYTHON_VERSION: 3.8
PYTHON_VERSION: 3.9
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
@@ -39,17 +40,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.9
TOXENV: oldest
linux-py38:
linux-py39:
# linux unit tests with the oldest python we support
IMAGE_NAME: ubuntu-22.04
PYTHON_VERSION: 3.8
TOXENV: py38
PYTHON_VERSION: 3.9
TOXENV: py39
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
@@ -52,6 +47,6 @@ jobs:
- 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

@@ -8,7 +8,7 @@ stages:
# 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 ~-)"
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

View File

@@ -5,7 +5,7 @@ stages:
variables:
- group: certbot-common
pool:
vmImage: ubuntu-20.04
vmImage: ubuntu-latest
steps:
- bash: |
set -e

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,6 @@
## 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), 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.

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

@@ -0,0 +1,17 @@
name: Issue Assigned
on:
issues:
types: [assigned]
pull_request_target:
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.issue.assignee.login || github.event.pull_request.assignee.login }} assigned to "${{ github.event.issue.title || github.event.pull_request.title }}": ${{ github.event.issue.html_url || github.event.pull_request.html_url }}

View File

@@ -1,14 +1,13 @@
name: Merge Event
on:
pull_request:
pull_request_target:
types:
- closed
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 }})

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

2
.gitignore vendored
View File

@@ -27,6 +27,8 @@ tags
.idea
.ropeproject
.vscode
*.sublime-project
*.sublime-workspace
# auth --cert-path --chain-path
/*.pem

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)

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,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

View File

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

View File

@@ -1,21 +1,17 @@
import sys
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '4.1.1'
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',
'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',
'pytz>=2019.3',
'requests>=2.20.0',
'setuptools>=41.6.0',
]
docs_extras = [
@@ -24,15 +20,6 @@ docs_extras = [
]
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',
@@ -46,23 +33,24 @@ setup(
author="Certbot Project",
author_email='certbot-dev@eff.org',
license='Apache License 2.0',
python_requires='>=3.8',
python_requires='>=3.9.2',
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',
'Programming Language :: Python :: 3.13',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],
packages=find_packages(),
packages=find_packages(where='src'),
package_dir={'': 'src'},
include_package_data=True,
install_requires=install_requires,
extras_require={

View File

@@ -13,7 +13,7 @@ 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'))

View File

@@ -9,6 +9,8 @@ from typing import Dict
import unittest
from unittest import mock
from cryptography import x509
import josepy as jose
import pytest
import requests
@@ -24,6 +26,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({
@@ -97,6 +100,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 +165,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 +256,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 +283,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)
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_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 +411,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 +464,200 @@ 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 == 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 == 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):
utc_now = datetime.datetime(2025, 3, 15, tzinfo=datetime.timezone.utc)
dt_mock.datetime.now.return_value = utc_now
self.client.directory = messages.Directory({
'renewalInfo': 'https://www.letsencrypt-demo.org/acme/renewal-info',
})
# Failure to fetch the 'renewalInfo' URL should return None
self.net.get.side_effect = requests.exceptions.RequestException
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 == 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 == None
@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
@@ -627,12 +899,11 @@ class ClientNetworkTest(unittest.TestCase):
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 +978,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 +1006,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 +1016,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 +1026,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 +1056,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,342 @@
"""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
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 import errors
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 SSLSocketAndProbeSNITest(unittest.TestCase):
"""Tests for acme.crypto_util.SSLSocket/probe_sni."""
def setUp(self):
self.cert = test_util.load_cert('rsa2048_cert.pem')
key = test_util.load_pyopenssl_private_key('rsa2048_key.pem')
# pylint: disable=protected-access
certs = {b'foo': (key, self.cert)}
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 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 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)
def test_sn_collisions(self):
from acme.crypto_util import make_self_signed_cert
for _ in range(self.cert_count):
cert = make_self_signed_cert(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):
from acme.crypto_util import make_self_signed_cert
cert = make_self_signed_cert(self.privkey, ['dummy'])
@mock.patch("acme.crypto_util._now")
def test_expiry_times(self, mock_now):
from acme.crypto_util import make_self_signed_cert
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 = make_self_signed_cert(
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 = make_self_signed_cert(
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):
from acme.crypto_util import make_self_signed_cert
with pytest.raises(AssertionError):
make_self_signed_cert(self.privkey, ips=[ipaddress.ip_address("1.1.1.1")])
make_self_signed_cert(self.privkey)
def test_extensions(self):
from acme.crypto_util import make_self_signed_cert
extension_type = x509.TLSFeature([x509.TLSFeatureType.status_request])
extension = x509.Extension(
x509.TLSFeature.oid,
False,
extension_type
)
cert = make_self_signed_cert(
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

@@ -1,7 +1,6 @@
"""Tests for acme.jose shim."""
import importlib
import sys
import unittest
import pytest

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 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')
@@ -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'
}
},
}
@@ -528,14 +534,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

@@ -11,6 +11,8 @@ from unittest import mock
import josepy as jose
import pytest
import requests
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from acme import challenges
from acme import crypto_util
@@ -116,13 +118,13 @@ class TLSALPN01ServerTest(unittest.TestCase):
def setUp(self):
self.certs = {b'localhost': (
test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
test_util.load_cert('rsa2048_cert.pem'),
serialization.load_pem_private_key(test_util.load_vector('rsa2048_key.pem'), password=None),
x509.load_pem_x509_certificate(test_util.load_vector('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'),
serialization.load_pem_private_key(test_util.load_vector('rsa4096_key.pem'), password=None),
x509.load_pem_x509_certificate(test_util.load_vector('rsa4096_cert.pem')),
)}
from acme.standalone import TLSALPN01Server
self.server = TLSALPN01Server(("localhost", 0), certs=self.certs,
@@ -142,8 +144,8 @@ class TLSALPN01ServerTest(unittest.TestCase):
# 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]))
# self.assertEqual(cert,
# self.certs[b'localhost'][1])
def test_challenge_certs(self):
host, port = self.server.socket.getsockname()[:2]
@@ -151,8 +153,7 @@ class TLSALPN01ServerTest(unittest.TestCase):
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])
assert cert == self.challenge_certs[b'localhost'][1]
def test_bad_alpn(self):
host, port = self.server.socket.getsockname()[:2]
@@ -193,7 +194,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(

View File

@@ -3,59 +3,55 @@
.. warning:: This module is not part of the public API.
"""
import importlib.resources
import os
import sys
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
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)
vector_ref = importlib.resources.files(__package__).joinpath('testdata', *names)
return vector_ref.read_bytes()
def _guess_loader(filename, loader_pem, loader_der):
def _guess_loader(filename: str, loader_pem: Callable, loader_der: Callable) -> Callable:
_, ext = os.path.splitext(filename)
if ext.lower() == '.pem':
if ext.lower() == ".pem":
return loader_pem
elif ext.lower() == '.der':
elif ext.lower() == ".der":
return loader_der
raise ValueError("Loader could not be recognized based on extension") # pragma: no cover
else: # pragma: no cover
raise ValueError("Loader could not be recognized based on extension")
def load_cert(*names):
def _guess_pyopenssl_loader(filename: str, loader_pem: int, loader_der: int) -> int:
_, ext = os.path.splitext(filename)
if ext.lower() == ".pem":
return loader_pem
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], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_certificate(loader, load_vector(*names))
names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate
)
return loader(load_vector(*names))
def load_comparable_cert(*names):
"""Load ComparableX509 cert."""
return jose.ComparableX509(load_cert(*names))
def load_csr(*names):
def load_csr(*names: str) -> x509.CertificateSigningRequest:
"""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))
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):
@@ -76,6 +72,6 @@ def load_ecdsa_private_key(*names):
def load_pyopenssl_private_key(*names):
"""Load pyOpenSSL private key."""
loader = _guess_loader(
loader = _guess_pyopenssl_loader(
names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1)
return crypto.load_privatekey(loader, load_vector(*names))

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,6 +1,5 @@
"""ACME Identifier Validation Challenges."""
import abc
import codecs
import functools
import hashlib
import logging
@@ -14,7 +13,9 @@ from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
import warnings
from cryptography import x509
from cryptography.hazmat.primitives import hashes
import josepy as jose
from OpenSSL import crypto
@@ -89,7 +90,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
@@ -399,7 +400,11 @@ class HTTP01(KeyAuthorizationChallenge):
@ChallengeResponse.register
class TLSALPN01Response(KeyAuthorizationChallengeResponse):
"""ACME tls-alpn-01 challenge response."""
"""ACME tls-alpn-01 challenge response.
.. deprecated:: 4.1.0
"""
typ = "tls-alpn-01"
PORT = 443
@@ -413,13 +418,18 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse):
ID_PE_ACME_IDENTIFIER_V1 = b"1.3.6.1.5.5.7.1.30.1"
ACME_TLS_1_PROTOCOL = b"acme-tls/1"
def __init__(self, *args: Any, **kwargs: Any) -> None:
warnings.warn("TLSALPN01Response is deprecated and will be removed in an "
"upcoming certbot major version update", DeprecationWarning)
super().__init__(*args, **kwargs)
@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]:
) -> Tuple[x509.Certificate, crypto.PKey]:
"""Generate tls-alpn-01 certificate.
:param str domain: Domain verified by the challenge.
@@ -428,22 +438,32 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse):
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`
:rtype: `tuple` of `x509.Certificate` 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)
oid = x509.ObjectIdentifier(self.ID_PE_ACME_IDENTIFIER_V1.decode())
acme_extension = x509.Extension(
oid,
critical=True,
value=x509.UnrecognizedExtension(oid, self.h)
)
return crypto_util.gen_ss_cert(key, [domain], force_san=True,
extensions=[acme_extension]), key
cryptography_key = key.to_cryptography_key()
assert isinstance(cryptography_key, crypto_util.CertificateIssuerPrivateKeyTypesTpl)
cert = crypto_util.make_self_signed_cert(
cryptography_key,
[domain],
force_san=True,
extensions=[acme_extension]
)
return cert, key
def probe_cert(self, domain: str, host: Optional[str] = None,
port: Optional[int] = None) -> crypto.X509:
port: Optional[int] = None) -> x509.Certificate:
"""Probe tls-alpn-01 challenge certificate.
:param str domain: domain being validated, required.
@@ -457,41 +477,48 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse):
if port is None:
port = self.PORT
return crypto_util.probe_sni(host=host.encode(), port=port, name=domain.encode(),
with warnings.catch_warnings():
warnings.filterwarnings(
'ignore',
message='alpn_protocols parameter is deprecated'
)
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:
def verify_cert(self, domain: str, cert: x509.Certificate, ) -> bool:
"""Verify tls-alpn-01 challenge certificate.
:param str domain: Domain name being validated.
:param OpensSSL.crypto.X509 cert: Challenge certificate.
:param cert: Challenge certificate.
:type cert: `cryptography.x509.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)
names = crypto_util.get_names_from_subject_and_extensions(
cert.subject, cert.extensions
)
logger.debug(
"Certificate %s. SANs: %s", cert.fingerprint(hashes.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
try:
ext = cert.extensions.get_extension_for_oid(
x509.ObjectIdentifier(self.ID_PE_ACME_IDENTIFIER_V1.decode())
)
except x509.ExtensionNotFound:
return False
return False
# This is for the type checker.
assert isinstance(ext.value, x509.UnrecognizedExtension)
return ext.value.value == self.h
# 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,
cert: Optional[x509.Certificate] = None, host: Optional[str] = None,
port: Optional[int] = None) -> bool:
"""Simple verify.
@@ -501,7 +528,7 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse):
: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
:param x509.Certificate cert: Optional certificate. If not
provided (``None``) certificate will be retrieved using
`probe_cert`.
:param string host: IP address used to probe the certificate.
@@ -528,11 +555,21 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse):
@Challenge.register # pylint: disable=too-many-ancestors
class TLSALPN01(KeyAuthorizationChallenge):
"""ACME tls-alpn-01 challenge."""
"""ACME tls-alpn-01 challenge.
.. deprecated:: 4.1.0
"""
response_cls = TLSALPN01Response
typ = response_cls.typ
def validation(self, account_key: jose.JWK, **kwargs: Any) -> Tuple[crypto.X509, crypto.PKey]:
def __init__(self, *args: Any, **kwargs: Any) -> None:
warnings.warn("TLSALPN01 is deprecated and will be removed in an "
"upcoming certbot major version update", DeprecationWarning)
super().__init__(*args, **kwargs)
def validation(self, account_key: jose.JWK,
**kwargs: Any) -> Tuple[x509.Certificate, crypto.PKey]:
"""Generate validation.
:param JWK account_key:
@@ -541,7 +578,7 @@ class TLSALPN01(KeyAuthorizationChallenge):
in certificate generation. If not provided (``None``), then
fresh key will be generated.
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
:rtype: `tuple` of `x509.Certificate` and `OpenSSL.crypto.PKey`
"""
# TODO: Remove cast when response() is generic.
@@ -560,6 +597,8 @@ class TLSALPN01(KeyAuthorizationChallenge):
:rtype: bool
"""
warnings.warn("TLSALPN01 is deprecated and will be removed in an "
"upcoming certbot major version update", DeprecationWarning)
return (hasattr(SSL.Connection, "set_alpn_protos")
and hasattr(SSL.Context, "set_alpn_select_callback"))

View File

@@ -4,6 +4,8 @@ import datetime
from email.utils import parsedate_tz
import http.client as http_client
import logging
import math
import random
import re
import time
from typing import Any
@@ -15,8 +17,9 @@ 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 +116,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 +124,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 = []
@@ -218,10 +226,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 +246,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 +284,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 +307,79 @@ 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
"""
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 (requests.exceptions.RequestException, messages.Error) as error:
logger.info("failed to fetch renewal_info URL (%s): %s", ari_url, error)
return None, now + default_retry_after
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.
@@ -466,11 +567,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 +600,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,7 +609,8 @@ 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
@@ -546,16 +647,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
@@ -745,9 +847,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,500 @@
"""Crypto utilities."""
import contextlib
import enum
from datetime import datetime, timedelta, timezone
import ipaddress
import logging
import socket
import typing
from typing import Any
from typing import Callable
from typing import List
from typing import Literal
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 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
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 TLS_method is compatible with all other
# methods, including TLSv2_METHOD (read more at
# https://docs.openssl.org/master/man3/SSL_CTX_new/#notes). _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.TLS_METHOD
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
_KeyAndCert = Union[
Tuple[crypto.PKey, crypto.X509],
Tuple[types.CertificateIssuerPrivateKeyTypes, x509.Certificate],
]
class _DefaultCertSelection:
def __init__(self, certs: Mapping[bytes, _KeyAndCert]):
self.certs = certs
def __call__(self, connection: SSL.Connection) -> Optional[_KeyAndCert]:
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, _KeyAndCert]] = None,
method: int = _DEFAULT_SSL_METHOD,
alpn_selection: Optional[Callable[[SSL.Connection, List[bytes]], bytes]] = None,
cert_selection: Optional[
Callable[
[SSL.Connection],
Optional[_KeyAndCert],
]
] = None,
) -> None:
warnings.warn("SSLSocket is deprecated and will be removed in an upcoming release",
DeprecationWarning)
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.")
if cert_selection is None:
cert_selection = _DefaultCertSelection(certs if certs else {})
self.cert_selection = 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_min_proto_version(SSL.TLS1_2_VERSION)
new_context.use_privatekey(key)
if isinstance(cert, x509.Certificate):
cert = crypto.X509.from_cryptography(cert)
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: # pragma: no cover
# 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.
#
# We don't track code coverage in this "except" branch to avoid spurious CI failures
# caused by missing test coverage. These aren't worth fixing because this entire
# class has been deprecated. See https://github.com/certbot/certbot/issues/10284.
raise OSError(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 OSError(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) -> x509.Certificate:
"""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: cryptography.x509.Certificate
"""
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 OSError 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(list(alpn_protocols))
warnings.warn("alpn_protocols parameter is deprecated and will be removed in an "
"upcoming certbot major version update", DeprecationWarning)
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.to_cryptography()
# 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 previously 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 pyOpenSSL.
.. 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``.
"""
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

@@ -107,6 +107,16 @@ class ValidationError(Error):
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."""

View File

@@ -13,6 +13,8 @@ from typing import Tuple
from typing import Type
from typing import TypeVar
from cryptography import x509
import josepy as jose
from acme import challenges
@@ -231,6 +233,7 @@ class Directory(jose.JSONDeSerializable):
website: str = jose.field('website', 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()}
@@ -578,18 +581,17 @@ 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`.
@@ -601,11 +603,10 @@ class CertificateResource(ResourceWithURI):
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 +614,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,6 +627,8 @@ class Order(ResourceBody):
:ivar datetime.datetime expires: When the order expires.
:ivar ~.Error error: Any error that occurred during finalization, if applicable.
"""
# 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)
@@ -679,3 +684,19 @@ class OrderResource(ResourceWithURI):
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

@@ -15,8 +15,8 @@ from typing import Optional
from typing import Set
from typing import Tuple
from typing import Type
import warnings
from OpenSSL import crypto
from OpenSSL import SSL
from acme import challenges
@@ -26,9 +26,15 @@ logger = logging.getLogger(__name__)
class TLSServer(socketserver.TCPServer):
"""Generic TLS Server."""
"""Generic TLS Server
.. deprecated:: 4.1.0
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
warnings.warn("TLSServer is deprecated and will be removed in an upcoming release",
DeprecationWarning)
self.ipv6 = kwargs.pop("ipv6", False)
if self.ipv6:
self.address_family = socket.AF_INET6
@@ -40,13 +46,15 @@ class TLSServer(socketserver.TCPServer):
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))
with warnings.catch_warnings():
warnings.filterwarnings('ignore', 'SSLSocket is deprecated')
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
) -> Optional[crypto_util._KeyAndCert]: # pragma: no cover
"""Callback selecting certificate for connection."""
server_name = connection.get_servername()
if server_name:
@@ -98,7 +106,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 +129,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"""
@@ -147,24 +155,31 @@ class BaseDualNetworkedServers:
class TLSALPN01Server(TLSServer, ACMEServerMixin):
"""TLSALPN01 Server."""
"""TLSALPN01 Server.
.. deprecated:: 4.1.0
"""
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]],
certs: List[crypto_util._KeyAndCert],
challenge_certs: Mapping[bytes, crypto_util._KeyAndCert],
ipv6: bool = False) -> None:
warnings.warn("TLSALPN01Server is deprecated and will be removed in an "
"upcoming certbot major version update", DeprecationWarning)
# 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)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", "TLSServer is deprecated")
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]]:
def _cert_selection(self, connection: SSL.Connection) -> Optional[crypto_util._KeyAndCert]:
# 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

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

@@ -1,7 +1,7 @@
from setuptools import find_packages
from setuptools import setup
version = '2.12.0.dev0'
version = '4.1.1'
install_requires = [
# We specify the minimum acme and certbot version as the current plugin
@@ -9,9 +9,7 @@ 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 = [
@@ -30,7 +28,7 @@ setup(
author="Certbot Project",
author_email='certbot-dev@eff.org',
license='Apache License 2.0',
python_requires='>=3.8',
python_requires='>=3.9.2',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
@@ -39,11 +37,11 @@ setup(
'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',
'Programming Language :: Python :: 3.13',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',
@@ -52,7 +50,8 @@ setup(
'Topic :: Utilities',
],
packages=find_packages(),
packages=find_packages(where='src'),
package_dir={'': 'src'},
include_package_data=True,
install_requires=install_requires,
extras_require={

View File

@@ -2,10 +2,10 @@
import atexit
import binascii
import fnmatch
import importlib.resources
import logging
import re
import subprocess
import sys
from contextlib import ExitStack
from typing import Dict
from typing import Iterable
@@ -17,12 +17,6 @@ from certbot import errors
from certbot import util
from certbot.compat import os
if sys.version_info >= (3, 9): # pragma: no cover
import importlib.resources as importlib_resources
else: # pragma: no cover
import importlib_resources
logger = logging.getLogger(__name__)
@@ -257,6 +251,6 @@ def find_ssl_apache_conf(prefix: str) -> str:
"""
file_manager = ExitStack()
atexit.register(file_manager.close)
ref = (importlib_resources.files("certbot_apache").joinpath("_internal")
ref = (importlib.resources.files("certbot_apache").joinpath("_internal")
.joinpath("tls_configs").joinpath("{0}-options-ssl-apache.conf".format(prefix)))
return str(file_manager.enter_context(importlib_resources.as_file(ref)))
return str(file_manager.enter_context(importlib.resources.as_file(ref)))

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