mirror of
https://github.com/go-acme/lego
synced 2026-03-14 22:45:48 +01:00
Compare commits
309 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87b172f103 |
||
|
|
9be8cd43ae |
||
|
|
7b1aa50081 |
||
|
|
a56697ed1c |
||
|
|
847c763504 |
||
|
|
da51631cd3 |
||
|
|
491dcaad1d |
||
|
|
7d459b59c5 |
||
|
|
4547c4317e |
||
|
|
c50918c54e |
||
|
|
dd1ea80c08 |
||
|
|
078a1889c8 |
||
|
|
d896c1f036 |
||
|
|
84f3be40f0 |
||
|
|
94e3bfb96a |
||
|
|
c06f378f0e |
||
|
|
2e095b95a5 |
||
|
|
4a61728ff0 |
||
|
|
1991339cc1 |
||
|
|
dc51d5ee65 |
||
|
|
4c6d29882e |
||
|
|
c1aaf19aac |
||
|
|
fac5c39f5f |
||
|
|
a7145a29ac |
||
|
|
2ce04a6586 |
||
|
|
44b89b7e92 |
||
|
|
16894fb99e |
||
|
|
de869c8a7e |
||
|
|
05333f3c84 |
||
|
|
4d41c52db8 |
||
|
|
d063b15c02 |
||
|
|
527d51d485 |
||
|
|
7f10c131f4 |
||
|
|
ac1092710d | ||
|
|
9f3dde3f6d | ||
|
|
b77b8709b6 |
||
|
|
eed3f0dcc8 |
||
|
|
b7a9b7dad0 |
||
|
|
dd6ab7ca95 |
||
|
|
c5a259564f |
||
|
|
4783c128fa |
||
|
|
2eede6d620 |
||
|
|
1b634097c1 |
||
|
|
a6a73754af |
||
|
|
ff885d99c2 |
||
|
|
ee616417a1 |
||
|
|
8b327005b3 |
||
|
|
96168f78de |
||
|
|
bb98b9a899 |
||
|
|
a5cc0e1555 |
||
|
|
7af0efdf72 | ||
|
|
27075d562a | ||
|
|
5574de68cd |
||
|
|
43dc1aa835 |
||
|
|
222cd85cbc | ||
|
|
4e6426cb2e | ||
|
|
5b30df22b5 |
||
|
|
e21ba75da8 |
||
|
|
bb5e70a4e5 | ||
|
|
a6e6b92d35 | ||
|
|
465d7918a8 |
||
|
|
c59d163e79 |
||
|
|
e54598536b |
||
|
|
9e2dffe8d2 |
||
|
|
1e57e29a9d |
||
|
|
961fd586d9 |
||
|
|
02dd7152f0 |
||
|
|
36552da309 |
||
|
|
dea97e4dfa |
||
|
|
bc163db9ed |
||
|
|
ea97ce2f62 |
||
|
|
cc83c025b5 |
||
|
|
742741fe05 | ||
|
|
5488fdf856 | ||
|
|
fc5e0174b8 |
||
|
|
3f2ebf7ef1 |
||
|
|
1757cdeaee |
||
|
|
dc0a595a9f |
||
|
|
42fb4346e2 |
||
|
|
ad6adbffd4 |
||
|
|
56cb356ef2 |
||
|
|
aea6afe2d6 |
||
|
|
93b8bb71ca |
||
|
|
0abf391bd1 |
||
|
|
57c14f8d2a |
||
|
|
ea8aca4366 |
||
|
|
a8226a6713 |
||
|
|
b338263c96 |
||
|
|
877738cef3 |
||
|
|
1c33fba180 |
||
|
|
d5dc3866e6 |
||
|
|
22955739a1 | ||
|
|
b704b26e6c | ||
|
|
14778cc1f1 |
||
|
|
e12c9fc637 | ||
|
|
102f7067ac | ||
|
|
591116b3a4 |
||
|
|
7d099f2ad7 |
||
|
|
31772ec503 |
||
|
|
81e0f2b42a |
||
|
|
5dba10703f |
||
|
|
da8280ac49 |
||
|
|
12dc42accf |
||
|
|
5ea0509b86 |
||
|
|
4bb17b0234 |
||
|
|
fe0a1f8668 |
||
|
|
e6c98a195e |
||
|
|
95953b45b5 |
||
|
|
9e7572e1ed |
||
|
|
4022827c6e |
||
|
|
b44293d8b1 |
||
|
|
753d31f254 | ||
|
|
a58c45ee4f | ||
|
|
acfb5ea938 | ||
|
|
0fcac851b3 | ||
|
|
2eb76de5a0 |
||
|
|
8873a5539c |
||
|
|
526ca7395c |
||
|
|
07683e60d8 |
||
|
|
f0c314c3ef |
||
|
|
213d7b8fa3 |
||
|
|
a3f3c620e9 |
||
|
|
7a6aa1110a |
||
|
|
621d9d0d0e |
||
|
|
8249f73fa2 |
||
|
|
26920e75f7 |
||
|
|
95eb44ccbe |
||
|
|
ba156d5344 |
||
|
|
bb33817a61 |
||
|
|
f432d2141e |
||
|
|
bf0e89cdd9 |
||
|
|
0d567188f6 | ||
|
|
42f057cc72 | ||
|
|
e76933536e |
||
|
|
bfe7df489b |
||
|
|
f4bd48e672 |
||
|
|
de8959229d |
||
|
|
6bfc090680 |
||
|
|
2308cd4778 |
||
|
|
5c1e21308c |
||
|
|
cb44524070 |
||
|
|
784ce2be95 |
||
|
|
50a24ced37 |
||
|
|
8a11af149f |
||
|
|
0012e20e52 |
||
|
|
1904d17e89 |
||
|
|
ddce5cff4a |
||
|
|
0ec467f075 |
||
|
|
8521cbc977 |
||
|
|
f37aaa788c | ||
|
|
8737b36c85 | ||
|
|
fc21d23f7f |
||
|
|
756d5ade0e |
||
|
|
c9157f756e |
||
|
|
4d2dc64364 |
||
|
|
238454b5f7 |
||
|
|
b4ddc1e5e2 |
||
|
|
605d49d500 |
||
|
|
137ad86fa4 |
||
|
|
c689b20fee |
||
|
|
8a75475894 | ||
|
|
6f54d60599 | ||
|
|
833d3b8147 |
||
|
|
f54baa83ac | ||
|
|
ced6669dcd | ||
|
|
793f65fed9 |
||
|
|
cb602702d2 |
||
|
|
79f496e11c |
||
|
|
d0008c42f5 |
||
|
|
7d82b83bfd |
||
|
|
0eac4b3dda |
||
|
|
8b40479678 |
||
|
|
1742e6d0ae |
||
|
|
a8e19ef7f3 |
||
|
|
b0e3fd2682 |
||
|
|
08e9db687b |
||
|
|
b8beddc267 |
||
|
|
52e167c930 |
||
|
|
fae73fdc5d |
||
|
|
bfa487cc48 |
||
|
|
96b18d764d |
||
|
|
713acefd7f |
||
|
|
40baed291c |
||
|
|
d9bba80a19 |
||
|
|
d1c79386e1 | ||
|
|
14d66f0c20 | ||
|
|
17c65de6e7 |
||
|
|
990f9ac601 |
||
|
|
1fecd31d3d |
||
|
|
94d871230d |
||
|
|
b28d1ac67a |
||
|
|
08316e47a6 |
||
|
|
9531f9e9c9 |
||
|
|
6ecdde77f0 |
||
|
|
45790d3c68 |
||
|
|
8d7ed798a7 |
||
|
|
a528e280f9 |
||
|
|
7571c0bd31 |
||
|
|
375300f969 |
||
|
|
f05362515a |
||
|
|
476f9ed910 |
||
|
|
e9a255df9b |
||
|
|
2f10624b11 |
||
|
|
65608d8bbf |
||
|
|
b82e6d88e4 |
||
|
|
d6df946223 |
||
|
|
1cee2efbdc |
||
|
|
c56d45486b |
||
|
|
950d4a0201 | ||
|
|
42c37d3779 | ||
|
|
ca25f1c83a | ||
|
|
ffaa64a88b | ||
|
|
da2aad2215 | ||
|
|
b2faa73e23 |
||
|
|
fcc64f0068 |
||
|
|
3b9653beec |
||
|
|
3f795d6ab1 |
||
|
|
24a46d0c15 |
||
|
|
627e6e2c35 |
||
|
|
ba7b4bcf11 |
||
|
|
dc8a3390ae |
||
|
|
0fae2f0511 |
||
|
|
f4d47c8606 |
||
|
|
e57af854f1 |
||
|
|
937f83c92c |
||
|
|
eb48c607ad |
||
|
|
51aaf75afb |
||
|
|
a8693c1aea |
||
|
|
46420fef71 |
||
|
|
730af10596 |
||
|
|
2bc147f58a |
||
|
|
3b9752b625 |
||
|
|
13780562cc |
||
|
|
c8aa9920ea |
||
|
|
4675ef7d9a |
||
|
|
5b06dd7874 |
||
|
|
fe10c3ab3c |
||
|
|
da260e45b0 |
||
|
|
b31c6ce79b | ||
|
|
55b012ba06 |
||
|
|
9d7e2a8c44 | ||
|
|
d8c11a8cf5 |
||
|
|
f1afe52251 |
||
|
|
0ab907c183 |
||
|
|
526ac35e5c | ||
|
|
dca1090bb5 | ||
|
|
c0c8bef783 | ||
|
|
584d374714 |
||
|
|
d183572e93 | ||
|
|
5a02346226 | ||
|
|
f9c1e241f3 |
||
|
|
c34138845e | ||
|
|
9b7343cb42 | ||
|
|
29cf89ea49 |
||
|
|
b16da88eb7 |
||
|
|
5d7fd6621e |
||
|
|
c96a165aa9 |
||
|
|
a25218dbb8 |
||
|
|
c0260c1d8a |
||
|
|
4552d03a4d |
||
|
|
4349dfc5eb |
||
|
|
e644196bfc |
||
|
|
7cd008e80a |
||
|
|
dc992b8d87 |
||
|
|
2e497ca928 |
||
|
|
7d7bc7b044 |
||
|
|
2211b56fea | ||
|
|
248e775788 |
||
|
|
b7947d83c5 |
||
|
|
5f69695771 |
||
|
|
4c65680b7a |
||
|
|
c2b88e19da |
||
|
|
b83c1d5f64 |
||
|
|
5f53d3e87d |
||
|
|
ee7a9e4fa0 | ||
|
|
258fb88ec9 | ||
|
|
bcc17b1bf8 |
||
|
|
52e711e049 |
||
|
|
bfe3606793 |
||
|
|
eac62e3037 |
||
|
|
65250372ee |
||
|
|
0bbf5ab59c |
||
|
|
1a62bbab40 |
||
|
|
2c13835084 |
||
|
|
eb041044b8 |
||
|
|
aacfa2b069 |
||
|
|
19a02023b4 |
||
|
|
c2f179f144 |
||
|
|
8e5448ccd7 |
||
|
|
2c42b264d0 |
||
|
|
abccd21e75 |
||
|
|
87b7e7191f | ||
|
|
b34902160d |
||
|
|
6fccca616a |
||
|
|
645169e3e5 | ||
|
|
3fc9ae13e6 | ||
|
|
b66d768d64 | ||
|
|
d5290e9834 | ||
|
|
b38be9150b | ||
|
|
7d83daef46 |
||
|
|
8ed8207007 |
||
|
|
a628db57d9 |
||
|
|
5987820520 |
||
|
|
15af1079a0 |
||
|
|
92d437fb1b |
||
|
|
062e355439 | ||
|
|
669cf4d21d |
||
|
|
11929c9c78 |
||
|
|
e0207678be |
1753 changed files with 73188 additions and 19280 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
**/zz_gen_*.* linguist-generated
|
||||
docs/data/zz_cli_help.toml linguist-generated
|
||||
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -7,9 +7,9 @@ body:
|
|||
attributes:
|
||||
label: Welcome
|
||||
options:
|
||||
- label: Yes, I'm using a binary release within 2 latest releases.
|
||||
- label: Yes, I'm using a binary release within the two latest releases.
|
||||
required: true
|
||||
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||
- label: Yes, I've searched for similar issues on GitHub and didn't find any.
|
||||
required: true
|
||||
- label: Yes, I've included all information below (version, config, etc).
|
||||
required: true
|
||||
|
|
@ -35,6 +35,7 @@ body:
|
|||
attributes:
|
||||
label: How do you use lego?
|
||||
options:
|
||||
- I don't know
|
||||
- Library
|
||||
- Binary
|
||||
- Docker image
|
||||
|
|
@ -44,6 +45,8 @@ body:
|
|||
- Through Bitnami
|
||||
- Through 1Panel
|
||||
- Through Zoraxy
|
||||
- Through Certimate
|
||||
- go install
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
|
@ -64,8 +67,9 @@ body:
|
|||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version of lego
|
||||
label: Effective version of lego
|
||||
description: |-
|
||||
`latest` or `dev` are not effective versions.
|
||||
```console
|
||||
$ lego --version
|
||||
```
|
||||
|
|
|
|||
13
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
13
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -6,7 +6,7 @@ body:
|
|||
attributes:
|
||||
label: Welcome
|
||||
options:
|
||||
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||
- label: Yes, I've searched for similar issues on GitHub and didn't find any.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
|
|
@ -14,6 +14,7 @@ body:
|
|||
attributes:
|
||||
label: How do you use lego?
|
||||
options:
|
||||
- I don't know
|
||||
- Library
|
||||
- Binary
|
||||
- Docker image
|
||||
|
|
@ -23,10 +24,20 @@ body:
|
|||
- Through Bitnami
|
||||
- Through 1Panel
|
||||
- Through Zoraxy
|
||||
- Through Certimate
|
||||
- go install
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Effective version of lego
|
||||
description: "`latest` or `dev` are not effective versions."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
|
|
|||
24
.github/ISSUE_TEMPLATE/new_dns_provider.yml
vendored
24
.github/ISSUE_TEMPLATE/new_dns_provider.yml
vendored
|
|
@ -8,15 +8,21 @@ body:
|
|||
attributes:
|
||||
label: Welcome
|
||||
options:
|
||||
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||
- label: Yes, I've searched for similar issues on GitHub and didn't find any.
|
||||
required: true
|
||||
- label: Yes, the DNS provider exposes a public API.
|
||||
required: true
|
||||
- label: Yes, I know that the lego maintainers don't have an account in all DNS providers in the world.
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: pr
|
||||
attributes:
|
||||
label: Implementation
|
||||
options:
|
||||
- label: Yes, I'm able to create a pull request and be able to maintain the implementation.
|
||||
required: false
|
||||
- label: Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider.
|
||||
- label: Yes, I can test an implementation with the help of the maintainers if someone creates a pull request.
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
|
|
@ -24,6 +30,7 @@ body:
|
|||
attributes:
|
||||
label: How do you use lego?
|
||||
options:
|
||||
- I don't know
|
||||
- Library
|
||||
- Binary
|
||||
- Docker image
|
||||
|
|
@ -33,10 +40,23 @@ body:
|
|||
- Through Bitnami
|
||||
- Through 1Panel
|
||||
- Through Zoraxy
|
||||
- Through Certimate
|
||||
- go install
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: profile
|
||||
attributes:
|
||||
label: Who are you?
|
||||
options:
|
||||
- A customer of this DNS provider
|
||||
- An employee of this DNS provider
|
||||
- Other (please explain)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: provider-link
|
||||
attributes:
|
||||
|
|
|
|||
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!--
|
||||
|
||||
IMPORTANT:
|
||||
|
||||
1. Create an issue and wait for a maintainer to approve it BEFORE opening a pull request.
|
||||
2. Don't open a work-in-progress pull request. If you open a PR, the PR must be ready to be reviewed.
|
||||
3. If a pull request doesn't follow one of the previous elements, it will be closed.
|
||||
|
||||
Also, pull requests from a fork inside a GitHub organization are not allowed because of access limitation on them.
|
||||
Only pull requests from personal forks are allowed.
|
||||
|
||||
-->
|
||||
10
.github/workflows/documentation.yml
vendored
10
.github/workflows/documentation.yml
vendored
|
|
@ -12,20 +12,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO_VERSION: stable
|
||||
HUGO_VERSION: 0.131.0
|
||||
HUGO_VERSION: 0.148.2
|
||||
CGO_ENABLED: 0
|
||||
|
||||
steps:
|
||||
|
||||
# https://github.com/marketplace/actions/checkout
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Set up Go ${{ env.GO_VERSION }}
|
||||
uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
|
|
|||
9
.github/workflows/go-cross.yml
vendored
9
.github/workflows/go-cross.yml
vendored
|
|
@ -20,13 +20,8 @@ jobs:
|
|||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
# https://github.com/marketplace/actions/checkout
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
|
|
|
|||
42
.github/workflows/pr.yml
vendored
42
.github/workflows/pr.yml
vendored
|
|
@ -13,54 +13,44 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO_VERSION: stable
|
||||
GOLANGCI_LINT_VERSION: v1.62.0
|
||||
HUGO_VERSION: 0.131.0
|
||||
GOLANGCI_LINT_VERSION: v2.10
|
||||
HUGO_VERSION: 0.148.2
|
||||
CGO_ENABLED: 0
|
||||
LEGO_E2E_TESTS: CI
|
||||
MEMCACHED_HOSTS: localhost:11211
|
||||
|
||||
steps:
|
||||
|
||||
# https://github.com/marketplace/actions/checkout
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Set up Go ${{ env.GO_VERSION }}
|
||||
uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Check and get dependencies
|
||||
run: |
|
||||
go mod tidy
|
||||
git diff --exit-code go.mod
|
||||
git diff --exit-code go.sum
|
||||
go mod tidy --diff
|
||||
|
||||
# https://golangci-lint.run/usage/install#other-ci
|
||||
- name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }}
|
||||
- name: Generate and Check generated elements
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION}
|
||||
golangci-lint --version
|
||||
make generate-dns
|
||||
git diff --exit-code
|
||||
|
||||
- uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: ${{ env.GOLANGCI_LINT_VERSION }}
|
||||
install-only: true
|
||||
|
||||
- name: Install Pebble
|
||||
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@3fe019bbc0a41ed16e2fee31592bb91751acaa47
|
||||
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0
|
||||
|
||||
- name: Install challtestsrv
|
||||
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@3fe019bbc0a41ed16e2fee31592bb91751acaa47
|
||||
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0
|
||||
|
||||
- name: Set up a Memcached server
|
||||
uses: niden/actions-memcached@v7
|
||||
|
||||
- name: Setup /etc/hosts
|
||||
run: |
|
||||
echo "127.0.0.1 acme.wtf" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 lego.wtf" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 acme.lego.wtf" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 légô.wtf" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 xn--lg-bja9b.wtf" | sudo tee -a /etc/hosts
|
||||
run: docker run -d --rm -p 11211:11211 memcached:1.6-alpine
|
||||
|
||||
- name: Make
|
||||
run: |
|
||||
|
|
|
|||
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
|
|
@ -5,6 +5,11 @@ on:
|
|||
tags:
|
||||
- v*
|
||||
|
||||
permissions:
|
||||
# Allow the workflow to write attestations.
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
|
||||
release:
|
||||
|
|
@ -37,13 +42,11 @@ jobs:
|
|||
docker-images: true
|
||||
swap-storage: false
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go ${{ env.GO_VERSION }}
|
||||
uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
|
@ -64,10 +67,21 @@ jobs:
|
|||
|
||||
# https://goreleaser.com/ci/actions/
|
||||
- name: Run GoReleaser
|
||||
id: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
version: v2.13.0
|
||||
args: release -p 1 --clean --timeout=90m
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
|
||||
- uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-checksums: ./dist/lego_${{ fromJSON(steps.goreleaser.outputs.metadata).version }}_checksums.txt
|
||||
github-token: ${{ secrets.GH_TOKEN_REPO }}
|
||||
- uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-checksums: ./dist/digests.txt
|
||||
github-token: ${{ secrets.GH_TOKEN_REPO }}
|
||||
|
|
|
|||
517
.golangci.yml
517
.golangci.yml
|
|
@ -1,265 +1,284 @@
|
|||
version: "2"
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
settings:
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: 'interface{}'
|
||||
replacement: 'any'
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
default: all
|
||||
disable:
|
||||
- wsl # Deprecated
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- contextcheck
|
||||
- cyclop # duplicate of gocyclo
|
||||
- sqlclosecheck # not relevant (SQL)
|
||||
- rowserrcheck # not relevant (SQL)
|
||||
- lll
|
||||
- gosec
|
||||
- dupl # not relevant
|
||||
- prealloc # too many false-positive
|
||||
- bodyclose # too many false-positive
|
||||
- mnd
|
||||
- testpackage # not relevant
|
||||
- tparallel # not relevant
|
||||
- paralleltest # not relevant
|
||||
- nestif # too many false-positive
|
||||
- wrapcheck
|
||||
- err113 # not relevant
|
||||
- nlreturn # not relevant
|
||||
- wsl # not relevant
|
||||
- errchkjson
|
||||
- errname
|
||||
- exhaustive # not relevant
|
||||
- exhaustruct # not relevant
|
||||
- makezero # not relevant
|
||||
- forbidigo
|
||||
- varnamelen # not relevant
|
||||
- nilnil # not relevant
|
||||
- ireturn # not relevant
|
||||
- contextcheck # too many false-positive
|
||||
- tenv # we already have a test "framework" to handle env vars
|
||||
- noctx
|
||||
- forcetypeassert
|
||||
- tagliatelle
|
||||
- errname
|
||||
- errchkjson
|
||||
- nonamedreturns
|
||||
- gosec
|
||||
- gosmopolitan # not relevant
|
||||
- ireturn # not relevant
|
||||
- lll
|
||||
- makezero # not relevant
|
||||
- mnd
|
||||
- musttag # false-positive https://github.com/junk1tm/musttag/issues/17
|
||||
- gosmopolitan # not relevant
|
||||
- exportloopref # Useless with go1.22
|
||||
- canonicalheader # Can create side effects in the context of API clients
|
||||
- usestdlibvars # false-positive https://github.com/sashamelentyev/usestdlibvars/issues/96
|
||||
- nestif # too many false-positive
|
||||
- nilnil # not relevant
|
||||
- nlreturn # not relevant
|
||||
- noctx
|
||||
- noinlineerr # too strict
|
||||
- nonamedreturns
|
||||
- paralleltest # not relevant
|
||||
- prealloc # too many false-positive
|
||||
- rowserrcheck # not relevant (SQL)
|
||||
- sqlclosecheck # not relevant (SQL)
|
||||
- tagliatelle
|
||||
- testpackage # not relevant
|
||||
- tparallel # not relevant
|
||||
- varnamelen # not relevant
|
||||
- wrapcheck
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
enable:
|
||||
- shadow
|
||||
gocyclo:
|
||||
min-complexity: 12
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 3
|
||||
funlen:
|
||||
lines: -1
|
||||
statements: 50
|
||||
misspell:
|
||||
locale: US
|
||||
ignore-words:
|
||||
- internetbs
|
||||
depguard:
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: github.com/instana/testify
|
||||
desc: not allowed
|
||||
- pkg: github.com/pkg/errors
|
||||
desc: Should be replaced by standard lib errors package
|
||||
funlen:
|
||||
lines: -1
|
||||
statements: 50
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 3
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- paramTypeCombine # already handle by gofumpt.extra-rules
|
||||
- whyNoLint # already handle by nonolint
|
||||
- unnamedResult
|
||||
- hugeParam
|
||||
- sloppyReassign
|
||||
- rangeValCopy
|
||||
- octalLiteral
|
||||
- ptrToRefParam
|
||||
- appendAssign
|
||||
- ruleguard
|
||||
- httpNoBody
|
||||
- exposedSyncMutex
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- style
|
||||
- performance
|
||||
gocyclo:
|
||||
min-complexity: 12
|
||||
godox:
|
||||
keywords:
|
||||
- FIXME
|
||||
govet:
|
||||
disable:
|
||||
- fieldalignment
|
||||
enable-all: true
|
||||
settings:
|
||||
printf:
|
||||
funcs:
|
||||
- Print
|
||||
- Printf
|
||||
- Warn
|
||||
- Warnf
|
||||
- Fatal
|
||||
- Fatalf
|
||||
misspell:
|
||||
locale: US
|
||||
ignore-rules:
|
||||
- internetbs
|
||||
perfsprint:
|
||||
err-error: true
|
||||
errorf: true
|
||||
sprintf1: true
|
||||
strconcat: false
|
||||
revive:
|
||||
rules:
|
||||
- name: struct-tag
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: error-naming
|
||||
- name: exported
|
||||
disabled: true
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
- name: var-naming
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
disabled: true
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: empty-block
|
||||
- name: superfluous-else
|
||||
- name: unused-parameter
|
||||
disabled: true
|
||||
- name: unreachable-code
|
||||
- name: redefines-builtin-id
|
||||
tagalign:
|
||||
align: false
|
||||
order:
|
||||
- xml
|
||||
- json
|
||||
- yaml
|
||||
- yml
|
||||
- toml
|
||||
- mapstructure
|
||||
- url
|
||||
testifylint:
|
||||
disable:
|
||||
- require-error
|
||||
- go-require
|
||||
usetesting:
|
||||
os-setenv: false # we already have a test "framework" to handle env vars
|
||||
funcorder:
|
||||
struct-method: false
|
||||
|
||||
exclusions:
|
||||
warn-unused: true
|
||||
presets:
|
||||
- comments
|
||||
- std-error-handling
|
||||
paths:
|
||||
# Those elements are related to code borrowed from the official HuaweiCloud API client.
|
||||
- providers/dns/huaweicloud/internal
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: "github.com/instana/testify"
|
||||
desc: not allowed
|
||||
- pkg: "github.com/pkg/errors"
|
||||
desc: Should be replaced by standard lib errors package
|
||||
tagalign:
|
||||
align: false
|
||||
order:
|
||||
- xml
|
||||
- json
|
||||
- yaml
|
||||
- yml
|
||||
- toml
|
||||
- mapstructure
|
||||
- url
|
||||
godox:
|
||||
keywords:
|
||||
- FIXME
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- style
|
||||
- performance
|
||||
disabled-checks:
|
||||
- paramTypeCombine # already handle by gofumpt.extra-rules
|
||||
- whyNoLint # already handle by nonolint
|
||||
- unnamedResult
|
||||
- hugeParam
|
||||
- sloppyReassign
|
||||
- rangeValCopy
|
||||
- octalLiteral
|
||||
- ptrToRefParam
|
||||
- appendAssign
|
||||
- ruleguard
|
||||
- httpNoBody
|
||||
- exposedSyncMutex
|
||||
revive:
|
||||
rules:
|
||||
- name: struct-tag
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: error-naming
|
||||
- name: exported
|
||||
disabled: true
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
- name: var-naming
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
disabled: true
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
- name: indent-error-flow
|
||||
- name: errorf
|
||||
- name: empty-block
|
||||
- name: superfluous-else
|
||||
- name: unused-parameter
|
||||
disabled: true
|
||||
- name: unreachable-code
|
||||
- name: redefines-builtin-id
|
||||
testifylint:
|
||||
disable:
|
||||
- require-error
|
||||
- go-require
|
||||
perfsprint:
|
||||
err-error: true
|
||||
errorf: true
|
||||
sprintf1: true
|
||||
strconcat: false
|
||||
|
||||
run:
|
||||
timeout: 10m
|
||||
|
||||
output:
|
||||
show-stats: true
|
||||
sort-results: true
|
||||
sort-order:
|
||||
- linter
|
||||
- file
|
||||
- path: (.+)_test.go
|
||||
linters:
|
||||
- funlen
|
||||
- goconst
|
||||
- maintidx
|
||||
- path: (.+)_test.go
|
||||
text: Error return value of `fmt.Fprintln` is not checked
|
||||
linters:
|
||||
- errcheck
|
||||
- text: "var-naming: avoid meaningless package names"
|
||||
linters:
|
||||
- revive
|
||||
- text: "var-naming: avoid package names that conflict with Go standard library package names"
|
||||
linters:
|
||||
- revive
|
||||
- path: certcrypto/crypto.go
|
||||
text: (tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: challenge/dns01/nameserver.go
|
||||
text: (defaultNameservers|recursiveNameservers|fqdnSoaCache|muFqdnSoaCache) is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: challenge/dns01/nameserver_.+.go
|
||||
text: dnsTimeout is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: challenge/dns01/precheck.go
|
||||
text: defaultNameserverPort is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: challenge/http01/domain_matcher.go
|
||||
text: cyclomatic complexity \d+ of func `parseForwardedHeader` is high
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: challenge/http01/domain_matcher.go
|
||||
text: Function 'parseForwardedHeader' has too many statements
|
||||
linters:
|
||||
- funlen
|
||||
- path: challenge/tlsalpn01/tls_alpn_challenge.go
|
||||
text: idPeAcmeIdentifierV1 is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: log/logger.go
|
||||
text: Logger is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: e2e/(dnschallenge/)?[\d\w]+_test.go
|
||||
text: load is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/(dns|http)/([\d\w]+/)*[\d\w]+_test.go
|
||||
text: envTest is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/dns/namecheap/namecheap_test.go
|
||||
text: testCases is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/dns/namecheap/transport.go
|
||||
text: (envProxyOnce|envProxyFuncValue) is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/dns/acmedns/mock_test.go
|
||||
text: egTestAccount is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/http/memcached/memcached_test.go
|
||||
text: memcachedHosts is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/dns/checkdomain/internal/types.go
|
||||
text: '`payed` is a misspelling of `paid`'
|
||||
linters:
|
||||
- misspell
|
||||
- path: platform/tester/env_test.go
|
||||
linters:
|
||||
- thelper
|
||||
- path: providers/dns/oraclecloud/oraclecloud_test.go
|
||||
text: 'SA1019: x509.EncryptPEMBlock has been deprecated since Go 1.16'
|
||||
linters:
|
||||
- staticcheck
|
||||
- path: providers/dns/sakuracloud/wrapper.go
|
||||
text: mu is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: cmd/cmd_renew.go
|
||||
text: cyclomatic complexity \d+ of func `(renewForDomains|renewForCSR)` is high
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: cmd/cmd_renew.go
|
||||
text: Function 'renewForDomains' has too many statements
|
||||
linters:
|
||||
- funlen
|
||||
- path: providers/dns/cpanel/cpanel.go
|
||||
text: cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: providers/dns/manual/manual.go
|
||||
text: 'SA1019: dns01.DNSProviderManual is deprecated'
|
||||
linters:
|
||||
- staticcheck
|
||||
# Those elements have been replaced by non-exposed structures.
|
||||
- path: providers/dns/linode/linode_test.go
|
||||
text: 'SA1019: linodego\.(DomainsPagedResponse|DomainRecordsPagedResponse) is deprecated'
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
issues:
|
||||
exclude-generated: strict
|
||||
exclude-use-default: false
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
exclude:
|
||||
- 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked'
|
||||
- 'exported (type|method|function) (.+) should have comment or be unexported'
|
||||
- 'ST1000: at least one file in a package should have a package comment'
|
||||
exclude-rules:
|
||||
- path: (.+)_test.go
|
||||
linters:
|
||||
- funlen
|
||||
- goconst
|
||||
- maintidx
|
||||
- path: (.+)_test.go
|
||||
text: 'Error return value of `fmt.Fprintln` is not checked'
|
||||
linters:
|
||||
- errcheck
|
||||
- path: providers/dns/dns_providers.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: certcrypto/crypto.go
|
||||
text: '(tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: challenge/dns01/nameserver.go
|
||||
text: '(defaultNameservers|recursiveNameservers|fqdnSoaCache|muFqdnSoaCache) is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: challenge/dns01/nameserver_.+.go
|
||||
text: 'dnsTimeout is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: challenge/dns01/nameserver_test.go
|
||||
text: 'findXByFqdnTestCases is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: challenge/http01/domain_matcher.go
|
||||
text: 'string `Host` has \d occurrences, make it a constant'
|
||||
linters:
|
||||
- goconst
|
||||
- path: challenge/http01/domain_matcher.go
|
||||
text: 'cyclomatic complexity \d+ of func `parseForwardedHeader` is high'
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: challenge/http01/domain_matcher.go
|
||||
text: "Function 'parseForwardedHeader' has too many statements"
|
||||
linters:
|
||||
- funlen
|
||||
- path: challenge/tlsalpn01/tls_alpn_challenge.go
|
||||
text: 'idPeAcmeIdentifierV1 is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: log/logger.go
|
||||
text: 'Logger is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: 'e2e/(dnschallenge/)?[\d\w]+_test.go'
|
||||
text: load is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: 'providers/dns/([\d\w]+/)*[\d\w]+_test.go'
|
||||
text: 'envTest is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: 'providers/http/([\d\w]+/)*[\d\w]+_test.go'
|
||||
text: 'envTest is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/dns/namecheap/namecheap_test.go
|
||||
text: 'testCases is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/dns/acmedns/acmedns_test.go
|
||||
text: 'egTestAccount is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/http/memcached/memcached_test.go
|
||||
text: 'memcachedHosts is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: cmd/zz_gen_cmd_dnshelp.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- funlen
|
||||
- path: providers/dns/checkdomain/internal/types.go
|
||||
text: '`payed` is a misspelling of `paid`'
|
||||
linters:
|
||||
- misspell
|
||||
- path: platform/tester/env_test.go
|
||||
linters:
|
||||
- thelper
|
||||
- path: providers/dns/oraclecloud/oraclecloud_test.go
|
||||
text: 'SA1019: x509.EncryptPEMBlock has been deprecated since Go 1.16'
|
||||
linters:
|
||||
- staticcheck
|
||||
- path: providers/dns/sakuracloud/wrapper.go
|
||||
text: 'mu is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: cmd/cmd_renew.go
|
||||
text: 'cyclomatic complexity \d+ of func `(renewForDomains|renewForCSR)` is high'
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: providers/dns/cpanel/cpanel.go
|
||||
text: 'cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high'
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: providers/dns/servercow/internal/types.go
|
||||
text: 'the methods of "Value" use pointer receiver and non-pointer receiver.'
|
||||
linters:
|
||||
- recvcheck
|
||||
|
||||
# Those elements have been replaced by non-exposed structures.
|
||||
- path: providers/dns/linode/linode_test.go
|
||||
linters:
|
||||
- staticcheck
|
||||
text: "SA1019: linodego\\.(DomainsPagedResponse|DomainRecordsPagedResponse) is deprecated"
|
||||
|
|
|
|||
161
.goreleaser.yml
161
.goreleaser.yml
|
|
@ -42,6 +42,10 @@ builds:
|
|||
goarch: 386
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
# Deprecated in go1.25, Removed in go1.26
|
||||
# https://go.dev/doc/go1.25#windows
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
|
|
@ -51,125 +55,98 @@ changelog:
|
|||
- '(?i)^Detach v[\d|.]+'
|
||||
- '(?i)^Prepare release v[\d|.]+'
|
||||
|
||||
release:
|
||||
skip_upload: false
|
||||
github:
|
||||
owner: 'go-acme'
|
||||
name: 'lego'
|
||||
header: |
|
||||
lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️
|
||||
|
||||
Everybody thinks that the others will donate, but in the end, nobody does.
|
||||
|
||||
So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev).
|
||||
|
||||
For key updates, see the [changelog](https://github.com/go-acme/lego/blob/HEAD/CHANGELOG.md#v{{ .Major }}{{ .Minor }}{{ .Patch }}).
|
||||
|
||||
archives:
|
||||
- id: lego
|
||||
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}'
|
||||
format: tar.gz
|
||||
formats: ['tar.gz']
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats: ['zip']
|
||||
files:
|
||||
- LICENSE
|
||||
- CHANGELOG.md
|
||||
|
||||
docker_manifests:
|
||||
- name_template: 'goacme/lego:{{ .Tag }}'
|
||||
image_templates:
|
||||
- 'goacme/lego:{{ .Tag }}-amd64'
|
||||
- 'goacme/lego:{{ .Tag }}-arm64'
|
||||
- 'goacme/lego:{{ .Tag }}-armv7'
|
||||
- name_template: 'goacme/lego:latest'
|
||||
image_templates:
|
||||
- 'goacme/lego:{{ .Tag }}-amd64'
|
||||
- 'goacme/lego:{{ .Tag }}-arm64'
|
||||
- 'goacme/lego:{{ .Tag }}-armv7'
|
||||
- name_template: 'goacme/lego:v{{ .Major }}.{{ .Minor }}'
|
||||
image_templates:
|
||||
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-amd64'
|
||||
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-arm64'
|
||||
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-armv7'
|
||||
|
||||
dockers:
|
||||
- use: buildx
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
dockers_v2:
|
||||
- images:
|
||||
- 'goacme/lego'
|
||||
dockerfile: buildx.Dockerfile
|
||||
image_templates:
|
||||
- 'goacme/lego:latest-amd64'
|
||||
- 'goacme/lego:{{ .Tag }}-amd64'
|
||||
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-amd64'
|
||||
build_flag_templates:
|
||||
- '--pull'
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
tags:
|
||||
- 'latest'
|
||||
- 'v{{ .Major }}'
|
||||
- 'v{{ .Major }}.{{ .Minor }}'
|
||||
- '{{ .Tag }}'
|
||||
labels:
|
||||
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
|
||||
- '--label=org.opencontainers.image.title={{.ProjectName}}'
|
||||
- '--label=org.opencontainers.image.description=Lets Encrypt/ACME client and library written in Go'
|
||||
- '--label=org.opencontainers.image.source={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.url={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.documentation=https://go-acme.github.io/lego'
|
||||
- '--label=org.opencontainers.image.created={{.Date}}'
|
||||
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
|
||||
- '--label=org.opencontainers.image.version={{.Version}}'
|
||||
- '--platform=linux/amd64'
|
||||
'org.opencontainers.image.title': '{{.ProjectName}}'
|
||||
'org.opencontainers.image.description': 'Lets Encrypt/ACME client and library written in Go'
|
||||
'org.opencontainers.image.source': '{{.GitURL}}'
|
||||
'org.opencontainers.image.url': '{{.GitURL}}'
|
||||
'org.opencontainers.image.documentation': 'https://go-acme.github.io/lego'
|
||||
'org.opencontainers.image.created': '{{.Date}}'
|
||||
'org.opencontainers.image.revision': '{{.FullCommit}}'
|
||||
'org.opencontainers.image.version': '{{.Version}}'
|
||||
|
||||
- use: buildx
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
dockerfile: buildx.Dockerfile
|
||||
image_templates:
|
||||
- 'goacme/lego:latest-arm64'
|
||||
- 'goacme/lego:{{ .Tag }}-arm64'
|
||||
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-arm64'
|
||||
build_flag_templates:
|
||||
- '--pull'
|
||||
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
|
||||
- '--label=org.opencontainers.image.title={{.ProjectName}}'
|
||||
- '--label=org.opencontainers.image.description=Lets Encrypt/ACME client and library written in Go'
|
||||
- '--label=org.opencontainers.image.source={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.url={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.documentation=https://go-acme.github.io/lego'
|
||||
- '--label=org.opencontainers.image.created={{.Date}}'
|
||||
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
|
||||
- '--label=org.opencontainers.image.version={{.Version}}'
|
||||
- '--platform=linux/arm64'
|
||||
|
||||
- use: buildx
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: '7'
|
||||
dockerfile: buildx.Dockerfile
|
||||
image_templates:
|
||||
- 'goacme/lego:latest-armv7'
|
||||
- 'goacme/lego:{{ .Tag }}-armv7'
|
||||
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-armv7'
|
||||
build_flag_templates:
|
||||
- '--pull'
|
||||
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
|
||||
- '--label=org.opencontainers.image.title={{.ProjectName}}'
|
||||
- '--label=org.opencontainers.image.description=Lets Encrypt/ACME client and library written in Go'
|
||||
- '--label=org.opencontainers.image.source={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.url={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.documentation=https://go-acme.github.io/lego'
|
||||
- '--label=org.opencontainers.image.created={{.Date}}'
|
||||
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
|
||||
- '--label=org.opencontainers.image.version={{.Version}}'
|
||||
- '--platform=linux/arm/v7'
|
||||
|
||||
# Disabled because https://github.com/go-acme/lego/pull/2134#issuecomment-2135293270
|
||||
snapcrafts:
|
||||
- name: lego
|
||||
disable: true
|
||||
- name_template: "{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
disable: false
|
||||
publish: true
|
||||
grade: stable
|
||||
confinement: strict
|
||||
license: MIT
|
||||
base: core22
|
||||
publish: true
|
||||
summary: Lego is a Let's Encrypt/ACME client.
|
||||
description: |
|
||||
Lego is a Let's Encrypt/ACME client written in Go.
|
||||
|
||||
|
||||
The lego snap makes it easy to install and use Lego on any Linux distribution that supports snaps.
|
||||
|
||||
|
||||
Usage:
|
||||
* `sudo snap install lego`
|
||||
* `sudo lego --email="you@example.com" --domains="example.com" --server=https://acme-staging-v02.api.letsencrypt.org/directory --http --http.port :8080 run
|
||||
|
||||
channel_templates:
|
||||
- edge
|
||||
|
||||
apps:
|
||||
lego:
|
||||
command: bin/lego
|
||||
command: lego
|
||||
environment:
|
||||
LEGO_PATH: /var/snap/lego/common/.lego
|
||||
plugs:
|
||||
- network-bind
|
||||
|
||||
aurs:
|
||||
- description: "Let s Encrypt client and ACME library written in Go"
|
||||
skip_upload: false
|
||||
homepage: https://go-acme.github.io/lego/
|
||||
name: 'lego-bin'
|
||||
provides:
|
||||
- lego
|
||||
maintainers:
|
||||
- "Fernandez Ludovic <lfernandez dot dev at gmail dot com>"
|
||||
license: APACHE
|
||||
private_key: "{{ .Env.AUR_KEY }}"
|
||||
git_url: "ssh://aur@aur.archlinux.org/lego-bin.git"
|
||||
commit_author:
|
||||
name: ldez
|
||||
email: ldez@users.noreply.github.com
|
||||
package: |-
|
||||
# Bin
|
||||
install -Dm755 "./lego" "${pkgdir}/usr/bin/lego"
|
||||
|
||||
# License
|
||||
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/lego/LICENSE"
|
||||
|
|
|
|||
1085
CHANGELOG.md
1085
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,7 @@ To ensure a great and easy experience for everyone, please review the few guidel
|
|||
- If both of the above do not apply, create a new issue and include as much information as possible.
|
||||
|
||||
Bug reports should include all information a person could need to reproduce your problem without the need to
|
||||
follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behaviour and the actual behaviour.
|
||||
follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behavior and the actual behavior.
|
||||
|
||||
## Feature proposals and requests
|
||||
|
||||
|
|
@ -20,31 +20,26 @@ It is up to you to make a strong point about your proposal and convince us of th
|
|||
|
||||
## Pull requests
|
||||
|
||||
Create an issue and wait for a maintainer to approve it BEFORE opening a pull request.
|
||||
|
||||
Patches, new features and improvements are a great way to help the project.
|
||||
Please keep them focused on one thing and do not include unrelated commits.
|
||||
|
||||
All pull requests which alter the behaviour of the program, add new behaviour or somehow alter code in a non-trivial way should **always** include tests.
|
||||
All pull requests that alter the behavior of the program,
|
||||
add new behavior or somehow alter code in a non-trivial way should **always** include tests.
|
||||
|
||||
If you want to contribute a significant pull request (with a non-trivial workload for you) please **ask first**. We do not want you to spend
|
||||
a lot of time on something the project's developers might not want to merge into the project.
|
||||
|
||||
**IMPORTANT**: By submitting a patch, you agree to allow the project
|
||||
owners to license your work under the terms of the [MIT License](LICENSE).
|
||||
**IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of the [MIT License](LICENSE).
|
||||
|
||||
### How to create a pull request
|
||||
|
||||
Requirements:
|
||||
|
||||
- `go` v1.15+
|
||||
- `go` v1.24+
|
||||
- environment variable: `GO111MODULE=on`
|
||||
|
||||
First, you have to install [GoLang](https://golang.org/doc/install) and [golangci-lint](https://github.com/golangci/golangci-lint#install).
|
||||
|
||||
```bash
|
||||
# Create the root folder
|
||||
mkdir -p $GOPATH/src/github.com/go-acme
|
||||
cd $GOPATH/src/github.com/go-acme
|
||||
|
||||
# clone your fork
|
||||
git clone git@github.com:YOUR_USERNAME/lego.git
|
||||
cd lego
|
||||
|
|
@ -56,14 +51,12 @@ git fetch upstream
|
|||
|
||||
```bash
|
||||
# Create your branch
|
||||
git checkout -b my-feature
|
||||
git switch -c my-feature
|
||||
|
||||
## Create your code ##
|
||||
```
|
||||
|
||||
```bash
|
||||
# Format
|
||||
make fmt
|
||||
# Linters
|
||||
make checks
|
||||
# Tests
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -54,10 +54,10 @@ detach:
|
|||
.PHONY: docs-build docs-serve docs-themes
|
||||
|
||||
docs-build: generate-dns
|
||||
@make -C ./docs hugo-build
|
||||
@make -C ./docs build
|
||||
|
||||
docs-serve: generate-dns
|
||||
@make -C ./docs hugo
|
||||
@make -C ./docs serve
|
||||
|
||||
docs-themes:
|
||||
@make -C ./docs hugo-themes
|
||||
|
|
|
|||
135
README.md
135
README.md
|
|
@ -5,29 +5,36 @@
|
|||
|
||||
# Lego
|
||||
|
||||
Let's Encrypt client and ACME library written in Go.
|
||||
[ACME](https://www.rfc-editor.org/rfc/rfc8555.html) client and library for Let's Encrypt and other ACME CAs written in Go.
|
||||
|
||||
[](https://pkg.go.dev/github.com/go-acme/lego/v4)
|
||||
[](https://github.com//go-acme/lego/actions)
|
||||
[](https://hub.docker.com/r/goacme/lego/)
|
||||
|
||||
lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️
|
||||
|
||||
Everybody thinks that the others will donate, but in the end, nobody does.
|
||||
|
||||
So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev).
|
||||
|
||||
## Features
|
||||
|
||||
- ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html)
|
||||
- Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension
|
||||
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses
|
||||
- Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
|
||||
- Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension
|
||||
- Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension
|
||||
- Comes with about [180 DNS providers](https://go-acme.github.io/lego/dns)
|
||||
- Register with CA
|
||||
- Obtain certificates, both from scratch or with an existing CSR
|
||||
- Renew certificates
|
||||
- Revoke certificates
|
||||
- Robust implementation of all ACME challenges
|
||||
- Robust implementation of ACME challenges:
|
||||
- HTTP (http-01)
|
||||
- DNS (dns-01)
|
||||
- TLS (tls-alpn-01)
|
||||
- SAN certificate support
|
||||
- [CNAME support](https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme.html) by default
|
||||
- Comes with multiple optional [DNS providers](https://go-acme.github.io/lego/dns)
|
||||
- [Custom challenge solvers](https://go-acme.github.io/lego/usage/library/writing-a-challenge-solver/)
|
||||
- Certificate bundling
|
||||
- OCSP helper function
|
||||
|
|
@ -49,77 +56,114 @@ Documentation is hosted live at https://go-acme.github.io/lego/.
|
|||
|
||||
Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
|
||||
|
||||
If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.yml).
|
||||
|
||||
<!-- START DNS PROVIDERS LIST -->
|
||||
|
||||
<table><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/com35/">35.com/三五互联</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/active24/">Active24</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/edgedns/">Akamai EdgeDNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/alidns/">Alibaba Cloud DNS</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/aliesa/">AlibabaCloud ESA</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/allinkl/">all-inkl</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/alwaysdata/">Alwaysdata</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/lightsail/">Amazon Lightsail</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/route53/">Amazon Route 53</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/anexia/">Anexia CloudDNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/safedns/">ANS SafeDNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/artfiles/">ArtFiles</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/arvancloud/">ArvanCloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/auroradns/">Aurora DNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/autodns/">Autodns</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/axelname/">Axelname</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/azion/">Azion</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/azure/">Azure (deprecated)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/azuredns/">Azure DNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/baiducloud/">Baidu Cloud</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/beget/">Beget.com</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/binarylane/">Binary Lane</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/bindman/">Bindman</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/bluecat/">Bluecat</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/bluecatv2/">Bluecat v2</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/bookmyname/">BookMyName</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/brandit/">Brandit (deprecated)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/bunny/">Bunny</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/checkdomain/">Checkdomain</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/civo/">Civo</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/cloudru/">Cloud.ru</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/clouddns/">CloudDNS</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/cloudflare/">Cloudflare</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/cloudns/">ClouDNS</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/cloudxns/">CloudXNS (Deprecated)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/conoha/">ConoHa</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/conoha/">ConoHa v2</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/conohav3/">ConoHa v3</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/constellix/">Constellix</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/corenetworks/">Core-Networks</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/cpanel/">CPanel/WHM</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/czechia/">Czechia</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ddnss/">DDnss (DynDNS Service)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/derak/">Derak Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/desec/">deSEC.io</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/designate/">Designate DNSaaS for Openstack</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/designate/">Designate DNSaaS for Openstack</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/digitalocean/">Digital Ocean</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/directadmin/">DirectAdmin</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dnsmadeeasy/">DNS Made Easy</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dnshomede/">dnsHome.de</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dnsexit/">DNSExit</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dnshomede/">dnsHome.de</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dnsimple/">DNSimple</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dnspod/">DNSPod (deprecated)</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dode/">Domain Offensive (do.de)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/domeneshop/">Domeneshop</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dreamhost/">DreamHost</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/duckdns/">Duck DNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dyn/">Dyn</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dynu/">Dynu</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dyn/">Dyn</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dyndnsfree/">DynDnsFree.de</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/dynu/">Dynu</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/easydns/">EasyDNS</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/edgecenter/">EdgeCenter</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/efficientip/">Efficient IP</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/epik/">Epik</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/exoscale/">Exoscale</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/eurodns/">EuroDNS</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/excedo/">Excedo</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/exoscale/">Exoscale</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/exec/">External program</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/f5xc/">F5 XC</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/freemyip/">freemyip.com</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/namesurfer/">FusionLayer NameSurfer</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gcore/">G-Core</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gandi/">Gandi</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gandiv5/">Gandi Live DNS (v5)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gigahostno/">Gigahost.no</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/glesys/">Glesys</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/godaddy/">Go Daddy</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gcloud/">Google Cloud</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gcloud/">Google Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/googledomains/">Google Domains</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/gravity/">Gravity</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hetzner/">Hetzner</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hostingde/">Hosting.de</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hostingnl/">Hosting.nl</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hostinger/">Hostinger</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/hosttech/">Hosttech</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/httpreq/">HTTP request</a></td>
|
||||
|
|
@ -138,49 +182,64 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
|
|||
<td><a href="https://go-acme.github.io/lego/dns/inwx/">INWX</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ionos/">Ionos</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ionoscloud/">Ionos Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ipv64/">IPv64</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/iwantmyname/">iwantmyname</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ispconfig/">ISPConfig 3</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ispconfigddns/">ISPConfig 3 - Dynamic DNS (DDNS) Module</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/iwantmyname/">iwantmyname (Deprecated)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/jdcloud/">JD Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/joker/">Joker</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/acme-dns/">Joohoi's ACME-DNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/keyhelp/">KeyHelp</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/leaseweb/">Leaseweb</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/liara/">Liara</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/limacity/">Lima-City</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/linode/">Linode (v4)</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/liquidweb/">Liquid Web</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/loopia/">Loopia</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/luadns/">LuaDNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/mailinabox/">Mail-in-a-Box</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/manageengine/">ManageEngine CloudDNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/manual/">Manual</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/metaname/">Metaname</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/metaregistrar/">Metaregistrar</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/mijnhost/">mijn.host</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/mittwald/">Mittwald</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/myaddr/">myaddr.{tools,dev,io}</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/mydnsjp/">MyDNS.jp</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/mythicbeasts/">MythicBeasts</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/namedotcom/">Name.com</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/namecheap/">Namecheap</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/namecheap/">Namecheap</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/namesilo/">Namesilo</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/nearlyfreespeech/">NearlyFreeSpeech.NET</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/neodigit/">Neodigit</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/netcup/">Netcup</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/netlify/">Netlify</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/nicmanager/">Nicmanager</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/nifcloud/">NIFCloud</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/njalla/">Njalla</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/nodion/">Nodion</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ns1/">NS1</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/octenium/">Octenium</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/otc/">Open Telekom Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/oraclecloud/">Oracle Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ovh/">OVH</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/plesk/">plesk.com</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/porkbun/">Porkbun</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/pdns/">PowerDNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/rackspace/">Rackspace</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/rainyun/">Rain Yun/雨云</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/rcodezero/">RcodeZero</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/regru/">reg.ru</a></td>
|
||||
|
|
@ -188,51 +247,61 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
|
|||
<td><a href="https://go-acme.github.io/lego/dns/rfc2136/">RFC2136</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/rimuhosting/">RimuHosting</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/nicru/">RU CENTER</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/sakuracloud/">Sakura Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/scaleway/">Scaleway</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/selectel/">Selectel</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/selectel/">Selectel</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/selectelv2/">Selectel v2</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/selfhostde/">SelfHost.(de|eu)</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/servercow/">Servercow</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/shellrent/">Shellrent</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/shellrent/">Shellrent</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/simply/">Simply.com</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/sonic/">Sonic</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/stackpath/">Stackpath</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/technitium/">Technitium</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/spaceship/">Spaceship</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/stackpath/">Stackpath</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/syse/">Syse</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/technitium/">Technitium</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/tencentcloud/">Tencent Cloud DNS</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/edgeone/">Tencent EdgeOne</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/timewebcloud/">Timeweb Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/todaynic/">TodayNIC/时代互联</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/transip/">TransIP</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/safedns/">UKFast SafeDNS</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/ultradns/">Ultradns</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/uniteddomains/">United-Domains</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/variomedia/">Variomedia</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/vegadns/">VegaDNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/vercel/">Vercel</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/vercel/">Vercel</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/versio/">Versio.[nl|eu|uk]</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/vinyldns/">VinylDNS</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/virtualname/">Virtualname</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/vkcloud/">VK Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/volcengine/">Volcano Engine/火山引擎</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/vscale/">Vscale</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/vultr/">Vultr</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/webnames/">Webnames</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/websupport/">Websupport</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/webnamesca/">webnames.ca</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/webnames/">webnames.ru</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/websupport/">Websupport</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/wedos/">WEDOS</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/westcn/">West.cn/西部数码</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/yandex360/">Yandex 360</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/yandexcloud/">Yandex Cloud</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/yandex/">Yandex PDD</a></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/zoneee/">Zone.ee</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/zoneedit/">ZoneEdit</a></td>
|
||||
<td><a href="https://go-acme.github.io/lego/dns/zonomi/">Zonomi</a></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr></table>
|
||||
|
||||
<!-- END DNS PROVIDERS LIST -->
|
||||
|
||||
If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.md).
|
||||
If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.yml).
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type AccountService service
|
|||
// New Creates a new account.
|
||||
func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) {
|
||||
var account acme.Account
|
||||
|
||||
resp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account)
|
||||
location := getLocation(resp)
|
||||
|
||||
|
|
@ -29,9 +30,9 @@ func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) {
|
|||
|
||||
// NewEAB Creates a new account with an External Account Binding.
|
||||
func (a *AccountService) NewEAB(accMsg acme.Account, kid, hmacEncoded string) (acme.ExtendedAccount, error) {
|
||||
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
|
||||
hmac, err := decodeEABHmac(hmacEncoded)
|
||||
if err != nil {
|
||||
return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %w", err)
|
||||
return acme.ExtendedAccount{}, err
|
||||
}
|
||||
|
||||
eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac)
|
||||
|
|
@ -51,10 +52,12 @@ func (a *AccountService) Get(accountURL string) (acme.Account, error) {
|
|||
}
|
||||
|
||||
var account acme.Account
|
||||
|
||||
_, err := a.core.postAsGet(accountURL, &account)
|
||||
if err != nil {
|
||||
return acme.Account{}, err
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +68,7 @@ func (a *AccountService) Update(accountURL string, req acme.Account) (acme.Accou
|
|||
}
|
||||
|
||||
var account acme.Account
|
||||
|
||||
_, err := a.core.post(accountURL, req, &account)
|
||||
if err != nil {
|
||||
return acme.Account{}, err
|
||||
|
|
@ -81,5 +85,20 @@ func (a *AccountService) Deactivate(accountURL string) error {
|
|||
|
||||
req := acme.Account{Status: acme.StatusDeactivated}
|
||||
_, err := a.core.post(accountURL, req, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func decodeEABHmac(hmacEncoded string) ([]byte, error) {
|
||||
hmac, errRaw := base64.RawURLEncoding.DecodeString(hmacEncoded)
|
||||
if errRaw == nil {
|
||||
return hmac, nil
|
||||
}
|
||||
|
||||
hmac, err := base64.URLEncoding.DecodeString(hmacEncoded)
|
||||
if err == nil {
|
||||
return hmac, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("acme: could not decode hmac key: %w", errors.Join(errRaw, err))
|
||||
}
|
||||
|
|
|
|||
35
acme/api/account_test.go
Normal file
35
acme/api/account_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_decodeEABHmac(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
hmac string
|
||||
}{
|
||||
{
|
||||
desc: "RawURLEncoding",
|
||||
hmac: "BAEDAgQCBQcGCAUDDDMBAAIRAwQhEjEFQVFhEyJxgTIGFJGhsUIjJBVSwWIzNHKC0UMHJZJT8OHx",
|
||||
},
|
||||
{
|
||||
desc: "URLEncoding",
|
||||
hmac: "nKTo9Hu8fpCqWPXx-25LVbZrJWxcHISsr4qHrRR0j5U=",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v, err := decodeEABHmac(test.hmac)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, v)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -9,7 +10,7 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/acme/api/internal/nonces"
|
||||
"github.com/go-acme/lego/v4/acme/api/internal/secure"
|
||||
|
|
@ -60,7 +61,7 @@ func New(httpClient *http.Client, userAgent, caDirURL, kid string, privateKey cr
|
|||
|
||||
// post performs an HTTP POST request and parses the response body as JSON,
|
||||
// into the provided respBody object.
|
||||
func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) {
|
||||
func (a *Core) post(uri string, reqBody, response any) (*http.Response, error) {
|
||||
content, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to marshal message")
|
||||
|
|
@ -71,47 +72,44 @@ func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response,
|
|||
|
||||
// postAsGet performs an HTTP POST ("POST-as-GET") request.
|
||||
// https://www.rfc-editor.org/rfc/rfc8555.html#section-6.3
|
||||
func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) {
|
||||
func (a *Core) postAsGet(uri string, response any) (*http.Response, error) {
|
||||
return a.retrievablePost(uri, []byte{}, response)
|
||||
}
|
||||
|
||||
func (a *Core) retrievablePost(uri string, content []byte, response interface{}) (*http.Response, error) {
|
||||
func (a *Core) retrievablePost(uri string, content []byte, response any) (*http.Response, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// during tests, allow to support ~90% of bad nonce with a minimum of attempts.
|
||||
bo := backoff.NewExponentialBackOff()
|
||||
bo.InitialInterval = 200 * time.Millisecond
|
||||
bo.MaxInterval = 5 * time.Second
|
||||
bo.MaxElapsedTime = 20 * time.Second
|
||||
|
||||
var resp *http.Response
|
||||
operation := func() error {
|
||||
var err error
|
||||
resp, err = a.signedPost(uri, content, response)
|
||||
operation := func() (*http.Response, error) {
|
||||
resp, err := a.signedPost(uri, content, response)
|
||||
if err != nil {
|
||||
// Retry if the nonce was invalidated
|
||||
var e *acme.NonceError
|
||||
if errors.As(err, &e) {
|
||||
return err
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return backoff.Permanent(err)
|
||||
return resp, backoff.Permanent(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
notify := func(err error, duration time.Duration) {
|
||||
log.Infof("retry due to: %v", err)
|
||||
}
|
||||
|
||||
err := backoff.RetryNotify(operation, bo, notify)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
return backoff.Retry(ctx, operation,
|
||||
backoff.WithBackOff(bo),
|
||||
backoff.WithMaxElapsedTime(20*time.Second),
|
||||
backoff.WithNotify(notify))
|
||||
}
|
||||
|
||||
func (a *Core) signedPost(uri string, content []byte, response interface{}) (*http.Response, error) {
|
||||
func (a *Core) signedPost(uri string, content []byte, response any) (*http.Response, error) {
|
||||
signedContent, err := a.jws.SignContent(uri, content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to post JWS message: failed to sign content: %w", err)
|
||||
|
|
@ -157,6 +155,7 @@ func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) {
|
|||
if dir.NewAccountURL == "" {
|
||||
return dir, errors.New("directory missing new registration URL")
|
||||
}
|
||||
|
||||
if dir.NewOrderURL == "" {
|
||||
return dir, errors.New("directory missing new order URL")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@ func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error)
|
|||
}
|
||||
|
||||
var authz acme.Authorization
|
||||
|
||||
_, err := c.core.postAsGet(authzURL, &authz)
|
||||
if err != nil {
|
||||
return acme.Authorization{}, err
|
||||
}
|
||||
|
||||
return authz, nil
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +31,8 @@ func (c *AuthorizationService) Deactivate(authzURL string) error {
|
|||
}
|
||||
|
||||
var disabledAuth acme.Authorization
|
||||
|
||||
_, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,12 @@ package api
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/log"
|
||||
)
|
||||
|
||||
// maxBodySize is the maximum size of body that we will read.
|
||||
|
|
@ -77,62 +74,22 @@ func (c *CertificateService) get(certURL string, bundle bool) (*acme.RawCertific
|
|||
return nil, resp.Header, err
|
||||
}
|
||||
|
||||
cert := c.getCertificateChain(data, resp.Header, bundle, certURL)
|
||||
cert := c.getCertificateChain(data, bundle)
|
||||
|
||||
return cert, resp.Header, err
|
||||
}
|
||||
|
||||
// getCertificateChain Returns the certificate and the issuer certificate.
|
||||
func (c *CertificateService) getCertificateChain(cert []byte, headers http.Header, bundle bool, certURL string) *acme.RawCertificate {
|
||||
func (c *CertificateService) getCertificateChain(cert []byte, bundle bool) *acme.RawCertificate {
|
||||
// Get issuerCert from bundled response from Let's Encrypt
|
||||
// See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962
|
||||
_, issuer := pem.Decode(cert)
|
||||
if issuer != nil {
|
||||
// If bundle is false, we want to return a single certificate.
|
||||
// To do this, we remove the issuer cert(s) from the issued cert.
|
||||
if !bundle {
|
||||
cert = bytes.TrimSuffix(cert, issuer)
|
||||
}
|
||||
return &acme.RawCertificate{Cert: cert, Issuer: issuer}
|
||||
}
|
||||
|
||||
// The issuer certificate link may be supplied via an "up" link
|
||||
// in the response headers of a new certificate.
|
||||
// See https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2
|
||||
up := getLink(headers, "up")
|
||||
|
||||
issuer, err := c.getIssuerFromLink(up)
|
||||
if err != nil {
|
||||
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
|
||||
log.Warnf("acme: Could not bundle issuer certificate [%s]: %v", certURL, err)
|
||||
} else if len(issuer) > 0 {
|
||||
// If bundle is true, we want to return a certificate bundle.
|
||||
// To do this, we append the issuer cert to the issued cert.
|
||||
if bundle {
|
||||
cert = append(cert, issuer...)
|
||||
}
|
||||
// If bundle is false, we want to return a single certificate.
|
||||
// To do this, we remove the issuer cert(s) from the issued cert.
|
||||
if !bundle {
|
||||
cert = bytes.TrimSuffix(cert, issuer)
|
||||
}
|
||||
|
||||
return &acme.RawCertificate{Cert: cert, Issuer: issuer}
|
||||
}
|
||||
|
||||
// getIssuerFromLink requests the issuer certificate.
|
||||
func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) {
|
||||
if up == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.Infof("acme: Requesting issuer cert from %s", up)
|
||||
|
||||
cert, _, err := c.get(up, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = x509.ParseCertificate(cert.Cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert.Cert)), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ package api
|
|||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -74,56 +73,34 @@ rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2
|
|||
`
|
||||
|
||||
func TestCertificateService_Get_issuerRelUp(t *testing.T) {
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
|
||||
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`)
|
||||
_, err := w.Write([]byte(certResponseMock))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/issuer", func(w http.ResponseWriter, _ *http.Request) {
|
||||
p, _ := pem.Decode([]byte(issuerMock))
|
||||
_, err := w.Write(p.Bytes)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
server := tester.MockACMEServer().
|
||||
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
||||
core, err := New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, issuer, err := core.Certificates.Get(apiURL+"/certificate", true)
|
||||
cert, issuer, err := core.Certificates.Get(server.URL+"/certificate", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, certResponseMock, string(cert), "Certificate")
|
||||
assert.Equal(t, issuerMock, string(issuer), "IssuerCertificate")
|
||||
}
|
||||
|
||||
func TestCertificateService_Get_embeddedIssuer(t *testing.T) {
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
|
||||
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, err := w.Write([]byte(certResponseMock))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
server := tester.MockACMEServer().
|
||||
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
||||
core, err := New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, issuer, err := core.Certificates.Get(apiURL+"/certificate", true)
|
||||
cert, issuer, err := core.Certificates.Get(server.URL+"/certificate", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, certResponseMock, string(cert), "Certificate")
|
||||
assert.Equal(t, issuerMock, string(issuer), "IssuerCertificate")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
|
|||
// Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`.
|
||||
// We use an empty struct instance as the postJSON payload here to achieve this result.
|
||||
var chlng acme.ExtendedChallenge
|
||||
|
||||
resp, err := c.core.post(chlgURL, struct{}{}, &chlng)
|
||||
if err != nil {
|
||||
return acme.ExtendedChallenge{}, err
|
||||
|
|
@ -24,6 +25,7 @@ func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
|
|||
|
||||
chlng.AuthorizationURL = getLink(resp.Header, "up")
|
||||
chlng.RetryAfter = getRetryAfter(resp)
|
||||
|
||||
return chlng, nil
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +36,7 @@ func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
|
|||
}
|
||||
|
||||
var chlng acme.ExtendedChallenge
|
||||
|
||||
resp, err := c.core.postAsGet(chlgURL, &chlng)
|
||||
if err != nil {
|
||||
return acme.ExtendedChallenge{}, err
|
||||
|
|
@ -41,5 +44,6 @@ func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
|
|||
|
||||
chlng.AuthorizationURL = getLink(resp.Header, "up")
|
||||
chlng.RetryAfter = getRetryAfter(resp)
|
||||
|
||||
return chlng, nil
|
||||
}
|
||||
|
|
|
|||
52
acme/api/identifier.go
Normal file
52
acme/api/identifier.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"net"
|
||||
"slices"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
)
|
||||
|
||||
func createIdentifiers(domains []string) []acme.Identifier {
|
||||
uniqIdentifiers := make(map[string]struct{})
|
||||
|
||||
var identifiers []acme.Identifier
|
||||
|
||||
for _, domain := range domains {
|
||||
if _, ok := uniqIdentifiers[domain]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ident := acme.Identifier{Value: domain, Type: "dns"}
|
||||
|
||||
if net.ParseIP(domain) != nil {
|
||||
ident.Type = "ip"
|
||||
}
|
||||
|
||||
identifiers = append(identifiers, ident)
|
||||
|
||||
uniqIdentifiers[domain] = struct{}{}
|
||||
}
|
||||
|
||||
return identifiers
|
||||
}
|
||||
|
||||
// compareIdentifiers compares 2 slices of [acme.Identifier].
|
||||
func compareIdentifiers(a, b []acme.Identifier) int {
|
||||
// Clones slices to avoid modifying original slices.
|
||||
right := slices.Clone(a)
|
||||
left := slices.Clone(b)
|
||||
|
||||
slices.SortStableFunc(right, compareIdentifier)
|
||||
slices.SortStableFunc(left, compareIdentifier)
|
||||
|
||||
return slices.CompareFunc(right, left, compareIdentifier)
|
||||
}
|
||||
|
||||
func compareIdentifier(right, left acme.Identifier) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(right.Type, left.Type),
|
||||
cmp.Compare(right.Value, left.Value),
|
||||
)
|
||||
}
|
||||
111
acme/api/identifier_test.go
Normal file
111
acme/api/identifier_test.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_compareIdentifiers(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
a, b []acme.Identifier
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
desc: "identical identifiers",
|
||||
a: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
},
|
||||
b: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
desc: "identical identifiers but different order",
|
||||
a: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
},
|
||||
b: []acme.Identifier{
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
{Type: "dns", Value: "example.com"},
|
||||
},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
desc: "duplicate identifiers",
|
||||
a: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
},
|
||||
b: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "example.com"},
|
||||
},
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
desc: "different identifier values",
|
||||
a: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
},
|
||||
b: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.org"},
|
||||
},
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
desc: "different identifier types",
|
||||
a: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
},
|
||||
b: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "ip", Value: "*.example.com"},
|
||||
},
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
desc: "different number of identifiers a>b",
|
||||
a: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
{Type: "dns", Value: "example.org"},
|
||||
},
|
||||
b: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
},
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
desc: "different number of identifiers b>a",
|
||||
a: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
},
|
||||
b: []acme.Identifier{
|
||||
{Type: "dns", Value: "example.com"},
|
||||
{Type: "dns", Value: "*.example.com"},
|
||||
{Type: "dns", Value: "example.org"},
|
||||
},
|
||||
expected: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, test.expected, compareIdentifiers(test.a, test.b))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -11,10 +11,11 @@ import (
|
|||
|
||||
// Manager Manages nonces.
|
||||
type Manager struct {
|
||||
sync.Mutex
|
||||
|
||||
do *sender.Doer
|
||||
nonceURL string
|
||||
nonces []string
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewManager Creates a new Manager.
|
||||
|
|
@ -36,6 +37,7 @@ func (n *Manager) Pop() (string, bool) {
|
|||
|
||||
nonce := n.nonces[len(n.nonces)-1]
|
||||
n.nonces = n.nonces[:len(n.nonces)-1]
|
||||
|
||||
return nonce, true
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +45,7 @@ func (n *Manager) Pop() (string, bool) {
|
|||
func (n *Manager) Push(nonce string) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
n.nonces = append(n.nonces, nonce)
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +54,7 @@ func (n *Manager) Nonce() (string, error) {
|
|||
if nonce, ok := n.Pop(); ok {
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
return n.getNonce()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,45 +8,52 @@ import (
|
|||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/acme/api/internal/sender"
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
)
|
||||
|
||||
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
w.Header().Set("Replay-Nonce", "12345")
|
||||
w.Header().Set("Retry-After", "0")
|
||||
err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
manager := servermock.NewBuilder(
|
||||
func(server *httptest.Server) (*Manager, error) {
|
||||
doer := sender.NewDoer(server.Client(), "lego-test")
|
||||
|
||||
return NewManager(doer, server.URL), nil
|
||||
}).
|
||||
Route("HEAD /", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
rw.Header().Set("Replay-Nonce", "12345")
|
||||
rw.Header().Set("Retry-After", "0")
|
||||
|
||||
servermock.JSONEncode(&acme.Challenge{Type: "http-01", Status: "Valid", URL: "https://example.com/", Token: "token"}).ServeHTTP(rw, req)
|
||||
})).
|
||||
BuildHTTPS(t)
|
||||
|
||||
doer := sender.NewDoer(http.DefaultClient, "lego-test")
|
||||
j := NewManager(doer, server.URL)
|
||||
ch := make(chan bool)
|
||||
resultCh := make(chan bool)
|
||||
|
||||
go func() {
|
||||
_, errN := j.Nonce()
|
||||
_, errN := manager.Nonce()
|
||||
if errN != nil {
|
||||
t.Log(errN)
|
||||
}
|
||||
|
||||
ch <- true
|
||||
}()
|
||||
go func() {
|
||||
_, errN := j.Nonce()
|
||||
_, errN := manager.Nonce()
|
||||
if errN != nil {
|
||||
t.Log(errN)
|
||||
}
|
||||
|
||||
ch <- true
|
||||
}()
|
||||
go func() {
|
||||
<-ch
|
||||
<-ch
|
||||
|
||||
resultCh <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-resultCh:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ func (j *JWS) SetKid(kid string) {
|
|||
// SignContent Signs a content with the JWS.
|
||||
func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, error) {
|
||||
var alg jose.SignatureAlgorithm
|
||||
|
||||
switch k := j.privKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
alg = jose.RS256
|
||||
|
|
@ -54,7 +55,7 @@ func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, e
|
|||
|
||||
options := jose.SignerOptions{
|
||||
NonceSource: j.nonces,
|
||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
ExtraHeaders: map[jose.HeaderKey]any{
|
||||
"url": url,
|
||||
},
|
||||
}
|
||||
|
|
@ -72,12 +73,14 @@ func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, e
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign content: %w", err)
|
||||
}
|
||||
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
// SignEABContent Signs an external account binding content with the JWS.
|
||||
func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
|
||||
jwk := jose.JSONWebKey{Key: j.privKey}
|
||||
|
||||
jwkJSON, err := jwk.Public().MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme: error encoding eab jwk key: %w", err)
|
||||
|
|
@ -87,7 +90,7 @@ func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignatu
|
|||
jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
|
||||
&jose.SignerOptions{
|
||||
EmbedJWK: false,
|
||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
ExtraHeaders: map[jose.HeaderKey]any{
|
||||
"kid": kid,
|
||||
"url": url,
|
||||
},
|
||||
|
|
@ -108,6 +111,7 @@ func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignatu
|
|||
// GetKeyAuthorization Gets the key authorization for a token.
|
||||
func (j *JWS) GetKeyAuthorization(token string) (string, error) {
|
||||
var publicKey crypto.PublicKey
|
||||
|
||||
switch k := j.privKey.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
publicKey = k.Public()
|
||||
|
|
|
|||
|
|
@ -9,45 +9,52 @@ import (
|
|||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/acme/api/internal/nonces"
|
||||
"github.com/go-acme/lego/v4/acme/api/internal/sender"
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
)
|
||||
|
||||
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
w.Header().Set("Replay-Nonce", "12345")
|
||||
w.Header().Set("Retry-After", "0")
|
||||
err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
manager := servermock.NewBuilder(
|
||||
func(server *httptest.Server) (*nonces.Manager, error) {
|
||||
doer := sender.NewDoer(server.Client(), "lego-test")
|
||||
|
||||
return nonces.NewManager(doer, server.URL), nil
|
||||
}).
|
||||
Route("HEAD /", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
rw.Header().Set("Replay-Nonce", "12345")
|
||||
rw.Header().Set("Retry-After", "0")
|
||||
|
||||
servermock.JSONEncode(&acme.Challenge{Type: "http-01", Status: "Valid", URL: "https://example.com/", Token: "token"}).ServeHTTP(rw, req)
|
||||
})).
|
||||
BuildHTTPS(t)
|
||||
|
||||
doer := sender.NewDoer(http.DefaultClient, "lego-test")
|
||||
j := nonces.NewManager(doer, server.URL)
|
||||
ch := make(chan bool)
|
||||
resultCh := make(chan bool)
|
||||
|
||||
go func() {
|
||||
_, errN := j.Nonce()
|
||||
_, errN := manager.Nonce()
|
||||
if errN != nil {
|
||||
t.Log(errN)
|
||||
}
|
||||
|
||||
ch <- true
|
||||
}()
|
||||
go func() {
|
||||
_, errN := j.Nonce()
|
||||
_, errN := manager.Nonce()
|
||||
if errN != nil {
|
||||
t.Log(errN)
|
||||
}
|
||||
|
||||
ch <- true
|
||||
}()
|
||||
go func() {
|
||||
<-ch
|
||||
<-ch
|
||||
|
||||
resultCh <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-resultCh:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ type Doer struct {
|
|||
|
||||
// NewDoer Creates a new Doer.
|
||||
func NewDoer(client *http.Client, userAgent string) *Doer {
|
||||
client.Transport = newHTTPSOnly(client)
|
||||
|
||||
return &Doer{
|
||||
httpClient: client,
|
||||
userAgent: userAgent,
|
||||
|
|
@ -35,7 +37,7 @@ func NewDoer(client *http.Client, userAgent string) *Doer {
|
|||
|
||||
// Get performs a GET request with a proper User-Agent string.
|
||||
// If "response" is not provided, callers should close resp.Body when done reading from it.
|
||||
func (d *Doer) Get(url string, response interface{}) (*http.Response, error) {
|
||||
func (d *Doer) Get(url string, response any) (*http.Response, error) {
|
||||
req, err := d.newRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -57,7 +59,7 @@ func (d *Doer) Head(url string) (*http.Response, error) {
|
|||
|
||||
// Post performs a POST request with a proper User-Agent string.
|
||||
// If "response" is not provided, callers should close resp.Body when done reading from it.
|
||||
func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) {
|
||||
func (d *Doer) Post(url string, body io.Reader, bodyType string, response any) (*http.Response, error) {
|
||||
req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -84,7 +86,7 @@ func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOpt
|
|||
return req, nil
|
||||
}
|
||||
|
||||
func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) {
|
||||
func (d *Doer) do(req *http.Request, response any) (*http.Response, error) {
|
||||
resp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -118,31 +120,69 @@ func (d *Doer) formatUserAgent() string {
|
|||
}
|
||||
|
||||
func checkError(req *http.Request, resp *http.Response) error {
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err)
|
||||
}
|
||||
|
||||
var errorDetails *acme.ProblemDetails
|
||||
err = json.Unmarshal(body, &errorDetails)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body))
|
||||
}
|
||||
|
||||
errorDetails.Method = req.Method
|
||||
errorDetails.URL = req.URL.String()
|
||||
|
||||
if errorDetails.HTTPStatus == 0 {
|
||||
errorDetails.HTTPStatus = resp.StatusCode
|
||||
}
|
||||
|
||||
// Check for errors we handle specifically
|
||||
if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr {
|
||||
return &acme.NonceError{ProblemDetails: errorDetails}
|
||||
if resp.StatusCode < http.StatusBadRequest {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err)
|
||||
}
|
||||
|
||||
var errorDetails *acme.ProblemDetails
|
||||
|
||||
err = json.Unmarshal(body, &errorDetails)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body))
|
||||
}
|
||||
|
||||
errorDetails.Method = req.Method
|
||||
errorDetails.URL = req.URL.String()
|
||||
|
||||
if errorDetails.HTTPStatus == 0 {
|
||||
errorDetails.HTTPStatus = resp.StatusCode
|
||||
}
|
||||
|
||||
// Check for errors we handle specifically
|
||||
switch {
|
||||
case errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr:
|
||||
return &acme.NonceError{ProblemDetails: errorDetails}
|
||||
|
||||
case errorDetails.HTTPStatus == http.StatusConflict && errorDetails.Type == acme.AlreadyReplacedErr:
|
||||
return &acme.AlreadyReplacedError{ProblemDetails: errorDetails}
|
||||
|
||||
case errorDetails.HTTPStatus == http.StatusTooManyRequests && errorDetails.Type == acme.RateLimitedErr:
|
||||
return &acme.RateLimitedError{
|
||||
ProblemDetails: errorDetails,
|
||||
RetryAfter: resp.Header.Get("Retry-After"),
|
||||
}
|
||||
|
||||
default:
|
||||
return errorDetails
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type httpsOnly struct {
|
||||
rt http.RoundTripper
|
||||
}
|
||||
|
||||
func newHTTPSOnly(client *http.Client) *httpsOnly {
|
||||
if client.Transport == nil {
|
||||
return &httpsOnly{rt: http.DefaultTransport}
|
||||
}
|
||||
|
||||
return &httpsOnly{rt: client.Transport}
|
||||
}
|
||||
|
||||
// RoundTrip ensure HTTPS is used.
|
||||
// Each ACME function is accomplished by the client sending a sequence of HTTPS requests to the server [RFC2818],
|
||||
// carrying JSON messages [RFC8259].
|
||||
// Use of HTTPS is REQUIRED.
|
||||
// https://datatracker.ietf.org/doc/html/rfc8555#section-6.1
|
||||
func (r *httpsOnly) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.Scheme != "https" {
|
||||
return nil, fmt.Errorf("HTTPS is required: %s", req.URL)
|
||||
}
|
||||
|
||||
return r.rt.RoundTrip(req)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDo_UserAgentOnAllHTTPMethod(t *testing.T) {
|
||||
var ua, method string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
ua = r.Header.Get("User-Agent")
|
||||
method = r.Method
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
doer := NewDoer(http.DefaultClient, "")
|
||||
doer := NewDoer(server.Client(), "")
|
||||
|
||||
testCases := []struct {
|
||||
method string
|
||||
|
|
@ -60,8 +64,87 @@ func TestDo_CustomUserAgent(t *testing.T) {
|
|||
ua := doer.formatUserAgent()
|
||||
assert.Contains(t, ua, ourUserAgent)
|
||||
assert.Contains(t, ua, customUA)
|
||||
|
||||
if strings.HasSuffix(ua, " ") {
|
||||
t.Errorf("UA should not have trailing spaces; got '%s'", ua)
|
||||
}
|
||||
|
||||
assert.Len(t, strings.Split(ua, " "), 5)
|
||||
}
|
||||
|
||||
func TestDo_failWithHTTP(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
sender := NewDoer(server.Client(), "test")
|
||||
|
||||
_, err := sender.Post(server.URL, strings.NewReader("data"), "text/plain", nil)
|
||||
require.ErrorContains(t, err, "HTTPS is required: http://")
|
||||
}
|
||||
|
||||
func Test_checkError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
resp *http.Response
|
||||
assert func(t *testing.T, err error)
|
||||
}{
|
||||
{
|
||||
desc: "default",
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:example","detail":"message","status":404}`)),
|
||||
},
|
||||
assert: errorAs[*acme.ProblemDetails],
|
||||
},
|
||||
{
|
||||
desc: "badNonce",
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:badNonce","detail":"message","status":400}`)),
|
||||
},
|
||||
assert: errorAs[*acme.NonceError],
|
||||
},
|
||||
{
|
||||
desc: "alreadyReplaced",
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusConflict,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:alreadyReplaced","detail":"message","status":409}`)),
|
||||
},
|
||||
assert: errorAs[*acme.AlreadyReplacedError],
|
||||
},
|
||||
{
|
||||
desc: "rateLimited",
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusConflict,
|
||||
Header: http.Header{
|
||||
"Retry-After": []string{"1"},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"type":"urn:ietf:params:acme:error:rateLimited","detail":"message","status":429}`)),
|
||||
},
|
||||
assert: errorAs[*acme.RateLimitedError],
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "https://example.com", nil)
|
||||
|
||||
err := checkError(req, test.resp)
|
||||
require.Error(t, err)
|
||||
|
||||
pb := &acme.ProblemDetails{}
|
||||
assert.ErrorAs(t, err, &pb)
|
||||
|
||||
test.assert(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func errorAs[T error](t *testing.T, err error) {
|
||||
t.Helper()
|
||||
|
||||
var zero T
|
||||
assert.ErrorAs(t, err, &zero)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ package sender
|
|||
|
||||
const (
|
||||
// ourUserAgent is the User-Agent of this underlying library package.
|
||||
ourUserAgent = "xenolf-acme/4.20.2"
|
||||
ourUserAgent = "xenolf-acme/4.32.0"
|
||||
|
||||
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
|
||||
// values: detach|release
|
||||
// NOTE: Update this with each tagged release.
|
||||
ourUserAgentComment = "release"
|
||||
ourUserAgentComment = "detach"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ package api
|
|||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
|
|
@ -13,9 +14,15 @@ import (
|
|||
type OrderOptions struct {
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
|
||||
// A string uniquely identifying the profile
|
||||
// which will be used to affect issuance of the certificate requested by this Order.
|
||||
// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4
|
||||
Profile string
|
||||
|
||||
// A string uniquely identifying a previously-issued certificate which this
|
||||
// order is intended to replace.
|
||||
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
|
||||
// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5
|
||||
ReplacesCertID string
|
||||
}
|
||||
|
||||
|
|
@ -28,18 +35,7 @@ func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
|
|||
|
||||
// NewWithOptions Creates a new order.
|
||||
func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acme.ExtendedOrder, error) {
|
||||
var identifiers []acme.Identifier
|
||||
for _, domain := range domains {
|
||||
ident := acme.Identifier{Value: domain, Type: "dns"}
|
||||
|
||||
if net.ParseIP(domain) != nil {
|
||||
ident.Type = "ip"
|
||||
}
|
||||
|
||||
identifiers = append(identifiers, ident)
|
||||
}
|
||||
|
||||
orderReq := acme.Order{Identifiers: identifiers}
|
||||
orderReq := acme.Order{Identifiers: createIdentifiers(domains)}
|
||||
|
||||
if opts != nil {
|
||||
if !opts.NotAfter.IsZero() {
|
||||
|
|
@ -53,12 +49,50 @@ func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acm
|
|||
if o.core.GetDirectory().RenewalInfo != "" {
|
||||
orderReq.Replaces = opts.ReplacesCertID
|
||||
}
|
||||
|
||||
if opts.Profile != "" {
|
||||
orderReq.Profile = opts.Profile
|
||||
}
|
||||
}
|
||||
|
||||
var order acme.Order
|
||||
|
||||
resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
|
||||
if err != nil {
|
||||
return acme.ExtendedOrder{}, err
|
||||
are := &acme.AlreadyReplacedError{}
|
||||
if !errors.As(err, &are) {
|
||||
return acme.ExtendedOrder{}, err
|
||||
}
|
||||
|
||||
// If the Server rejects the request because the identified certificate has already been marked as replaced,
|
||||
// it MUST return an HTTP 409 (Conflict) with a problem document of type "alreadyReplaced" (see Section 7.4).
|
||||
// https://www.rfc-editor.org/rfc/rfc9773.html#section-5
|
||||
orderReq.Replaces = ""
|
||||
|
||||
resp, err = o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
|
||||
if err != nil {
|
||||
return acme.ExtendedOrder{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// The server MUST return an error if it cannot fulfill the request as specified,
|
||||
// and it MUST NOT issue a certificate with contents other than those requested.
|
||||
// If the server requires the request to be modified in a certain way,
|
||||
// it should indicate the required changes using an appropriate error type and description.
|
||||
// https://www.rfc-editor.org/rfc/rfc8555#section-7.4
|
||||
//
|
||||
// Some ACME servers don't return an error,
|
||||
// and/or change the order identifiers in the response,
|
||||
// so we need to ensure that the identifiers are the same as requested.
|
||||
// Deduplication by the server is allowed.
|
||||
if compareIdentifiers(orderReq.Identifiers, order.Identifiers) != 0 {
|
||||
// Sorts identifiers to avoid error message ambiguities about the order of the identifiers.
|
||||
slices.SortStableFunc(orderReq.Identifiers, compareIdentifier)
|
||||
slices.SortStableFunc(order.Identifiers, compareIdentifier)
|
||||
|
||||
return acme.ExtendedOrder{},
|
||||
fmt.Errorf("order identifiers have been modified by the ACME server (RFC8555 §7.4): %+v != %+v",
|
||||
orderReq.Identifiers, order.Identifiers)
|
||||
}
|
||||
|
||||
return acme.ExtendedOrder{
|
||||
|
|
@ -74,6 +108,7 @@ func (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) {
|
|||
}
|
||||
|
||||
var order acme.Order
|
||||
|
||||
_, err := o.core.postAsGet(orderURL, &order)
|
||||
if err != nil {
|
||||
return acme.ExtendedOrder{}, err
|
||||
|
|
@ -89,13 +124,14 @@ func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.ExtendedO
|
|||
}
|
||||
|
||||
var order acme.Order
|
||||
|
||||
_, err := o.core.post(orderURL, csrMsg, &order)
|
||||
if err != nil {
|
||||
return acme.ExtendedOrder{}, err
|
||||
}
|
||||
|
||||
if order.Status == acme.StatusInvalid {
|
||||
return acme.ExtendedOrder{}, order.Error
|
||||
return acme.ExtendedOrder{}, fmt.Errorf("invalid order: %w", order.Err())
|
||||
}
|
||||
|
||||
return acme.ExtendedOrder{Order: order}, nil
|
||||
|
|
|
|||
|
|
@ -11,55 +11,51 @@ import (
|
|||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOrderService_NewWithOptions(t *testing.T) {
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
|
||||
// small value keeps test fast
|
||||
privateKey, errK := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, errK := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, errK, "Could not generate test key")
|
||||
|
||||
mux.HandleFunc("/newOrder", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
server := tester.MockACMEServer().
|
||||
Route("POST /newOrder",
|
||||
http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
body, err := readSignedBody(req, privateKey)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := readSignedBody(r, privateKey)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
order := acme.Order{}
|
||||
|
||||
order := acme.Order{}
|
||||
err = json.Unmarshal(body, &order)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &order)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = tester.WriteJSONResponse(w, acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Expires: order.Expires,
|
||||
Identifiers: order.Identifiers,
|
||||
NotBefore: order.NotBefore,
|
||||
NotAfter: order.NotAfter,
|
||||
Error: order.Error,
|
||||
Authorizations: order.Authorizations,
|
||||
Finalize: order.Finalize,
|
||||
Certificate: order.Certificate,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
servermock.JSONEncode(acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Expires: order.Expires,
|
||||
Identifiers: order.Identifiers,
|
||||
Profile: order.Profile,
|
||||
NotBefore: order.NotBefore,
|
||||
NotAfter: order.NotAfter,
|
||||
Error: order.Error,
|
||||
Authorizations: order.Authorizations,
|
||||
Finalize: order.Finalize,
|
||||
Certificate: order.Certificate,
|
||||
Replaces: order.Replaces,
|
||||
}).ServeHTTP(rw, req)
|
||||
})).
|
||||
BuildHTTPS(t)
|
||||
|
||||
core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
|
|
@ -112,6 +108,7 @@ func readSignedBody(r *http.Request, privateKey *rsa.PrivateKey) ([]byte, error)
|
|||
}
|
||||
|
||||
sigAlgs := []jose.SignatureAlgorithm{jose.RS256}
|
||||
|
||||
jws, err := jose.ParseSigned(string(reqBody), sigAlgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ var ErrNoARI = errors.New("renewalInfo[get/post]: server does not advertise a re
|
|||
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
|
||||
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
|
||||
// https://www.rfc-editor.org/rfc/rfc9773.html
|
||||
func (c *CertificateService) GetRenewalInfo(certID string) (*http.Response, error) {
|
||||
if c.core.GetDirectory().RenewalInfo == "" {
|
||||
return nil, ErrNoARI
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
|
|
@ -23,11 +26,13 @@ func getLinks(header http.Header, rel string) []string {
|
|||
linkExpr := regexp.MustCompile(`<(.+?)>(?:;[^;]+)*?;\s*rel="(.+?)"`)
|
||||
|
||||
var links []string
|
||||
|
||||
for _, link := range header["Link"] {
|
||||
for _, m := range linkExpr.FindAllStringSubmatch(link, -1) {
|
||||
if len(m) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
if m[2] == rel {
|
||||
links = append(links, m[1])
|
||||
}
|
||||
|
|
@ -54,3 +59,29 @@ func getRetryAfter(resp *http.Response) string {
|
|||
|
||||
return resp.Header.Get("Retry-After")
|
||||
}
|
||||
|
||||
// ParseRetryAfter parses the Retry-After header value according to RFC 7231.
|
||||
// The header can be either delay-seconds (numeric) or HTTP-date (RFC 1123 format).
|
||||
// https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.3
|
||||
// Returns the duration until the retry time.
|
||||
// TODO(ldez): unexposed this function in v5.
|
||||
func ParseRetryAfter(value string) (time.Duration, error) {
|
||||
if value == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if seconds, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
return time.Duration(seconds) * time.Second, nil
|
||||
}
|
||||
|
||||
if retryTime, err := time.Parse(time.RFC1123, value); err == nil {
|
||||
duration := time.Until(retryTime)
|
||||
if duration < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("invalid Retry-After value: %q", value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_getLink(t *testing.T) {
|
||||
|
|
@ -53,3 +55,38 @@ func Test_getLink(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRetryAfter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
value string
|
||||
expected time.Duration
|
||||
}{
|
||||
{
|
||||
desc: "empty header value",
|
||||
value: "",
|
||||
expected: time.Duration(0),
|
||||
},
|
||||
{
|
||||
desc: "delay-seconds",
|
||||
value: "123",
|
||||
expected: 123 * time.Second,
|
||||
},
|
||||
{
|
||||
desc: "HTTP-date",
|
||||
value: time.Now().Add(3 * time.Second).Format(time.RFC1123),
|
||||
expected: 3 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt, err := ParseRetryAfter(test.value)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.InDelta(t, test.expected.Seconds(), rt.Seconds(), 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const (
|
|||
|
||||
// Directory the ACME directory object.
|
||||
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1
|
||||
// - https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
|
||||
// - https://www.rfc-editor.org/rfc/rfc9773.html
|
||||
type Directory struct {
|
||||
NewNonceURL string `json:"newNonce"`
|
||||
NewAccountURL string `json:"newAccount"`
|
||||
|
|
@ -74,11 +74,17 @@ type Meta struct {
|
|||
// then the CA requires that all new-account requests include an "externalAccountBinding" field
|
||||
// associating the new account with an external account.
|
||||
ExternalAccountRequired bool `json:"externalAccountRequired"`
|
||||
|
||||
// profiles (optional, object):
|
||||
// A map of profile names to human-readable descriptions of those profiles.
|
||||
// https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-3
|
||||
Profiles map[string]string `json:"profiles"`
|
||||
}
|
||||
|
||||
// ExtendedAccount an extended Account.
|
||||
type ExtendedAccount struct {
|
||||
Account
|
||||
|
||||
// Contains the value of the response header `Location`
|
||||
Location string `json:"-"`
|
||||
}
|
||||
|
|
@ -148,6 +154,12 @@ type Order struct {
|
|||
// An array of identifier objects that the order pertains to.
|
||||
Identifiers []Identifier `json:"identifiers"`
|
||||
|
||||
// profile (string, optional):
|
||||
// A string uniquely identifying the profile
|
||||
// which will be used to affect issuance of the certificate requested by this Order.
|
||||
// https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4
|
||||
Profile string `json:"profile,omitempty"`
|
||||
|
||||
// notBefore (optional, string):
|
||||
// The requested value of the notBefore field in the certificate,
|
||||
// in the date format defined in [RFC3339].
|
||||
|
|
@ -185,10 +197,18 @@ type Order struct {
|
|||
// replaces (optional, string):
|
||||
// replaces (string, optional): A string uniquely identifying a
|
||||
// previously-issued certificate which this order is intended to replace.
|
||||
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
|
||||
// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5
|
||||
Replaces string `json:"replaces,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Order) Err() error {
|
||||
if r.Error != nil {
|
||||
return r.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authorization the ACME authorization object.
|
||||
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4
|
||||
type Authorization struct {
|
||||
|
|
@ -201,11 +221,11 @@ type Authorization struct {
|
|||
// The timestamp after which the server will consider this authorization invalid,
|
||||
// encoded in the format specified in RFC 3339 [RFC3339].
|
||||
// This field is REQUIRED for objects with "valid" in the "status" field.
|
||||
Expires time.Time `json:"expires,omitempty"`
|
||||
Expires time.Time `json:"expires,omitzero"`
|
||||
|
||||
// identifier (required, object):
|
||||
// The identifier that the account is authorized to represent
|
||||
Identifier Identifier `json:"identifier,omitempty"`
|
||||
Identifier Identifier `json:"identifier"`
|
||||
|
||||
// challenges (required, array of objects):
|
||||
// For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier.
|
||||
|
|
@ -225,6 +245,7 @@ type Authorization struct {
|
|||
// ExtendedChallenge a extended Challenge.
|
||||
type ExtendedChallenge struct {
|
||||
Challenge
|
||||
|
||||
// Contains the value of the response header `Retry-After`
|
||||
RetryAfter string `json:"-"`
|
||||
// Contains the value of the response header `Link` rel="up"
|
||||
|
|
@ -251,7 +272,7 @@ type Challenge struct {
|
|||
// The time at which the server validated this challenge,
|
||||
// encoded in the format specified in RFC 3339 [RFC3339].
|
||||
// This field is REQUIRED if the "status" field is "valid".
|
||||
Validated time.Time `json:"validated,omitempty"`
|
||||
Validated time.Time `json:"validated,omitzero"`
|
||||
|
||||
// error (optional, object):
|
||||
// Error that occurred while the server was validating the challenge, if any,
|
||||
|
|
@ -274,6 +295,14 @@ type Challenge struct {
|
|||
KeyAuthorization string `json:"keyAuthorization"`
|
||||
}
|
||||
|
||||
func (c *Challenge) Err() error {
|
||||
if c.Error != nil {
|
||||
return c.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Identifier the ACME identifier object.
|
||||
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-9.7.7
|
||||
type Identifier struct {
|
||||
|
|
@ -322,7 +351,7 @@ type Window struct {
|
|||
}
|
||||
|
||||
// RenewalInfoResponse is the response to GET requests made the renewalInfo endpoint.
|
||||
// - (4.1. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
|
||||
// - (4.1. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html
|
||||
type RenewalInfoResponse struct {
|
||||
// SuggestedWindow contains two fields, start and end,
|
||||
// whose values are timestamps which bound the window of time in which the CA recommends renewing the certificate.
|
||||
|
|
@ -335,11 +364,11 @@ type RenewalInfoResponse struct {
|
|||
}
|
||||
|
||||
// RenewalInfoUpdateRequest is the JWS payload for POST requests made to the renewalInfo endpoint.
|
||||
// - (4.2. RenewalInfo Objects) https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.2
|
||||
// - (4.2. RenewalInfo Objects) https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2
|
||||
type RenewalInfoUpdateRequest struct {
|
||||
// CertID is a composite string in the format: base64url(AKI) || '.' || base64url(Serial), where AKI is the
|
||||
// certificate's authority key identifier and Serial is the certificate's serial number. For details, see:
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.1
|
||||
// https://www.rfc-editor.org/rfc/rfc9773.html#section-4.1
|
||||
CertID string `json:"certID"`
|
||||
// Replaced is required and indicates whether or not the client considers the certificate to have been replaced.
|
||||
// A certificate is considered replaced when its revocation would not disrupt any ongoing services,
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ package acme
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Errors types.
|
||||
const (
|
||||
errNS = "urn:ietf:params:acme:error:"
|
||||
BadNonceErr = errNS + "badNonce"
|
||||
errNS = "urn:ietf:params:acme:error:"
|
||||
BadNonceErr = errNS + "badNonce"
|
||||
AlreadyReplacedErr = errNS + "alreadyReplaced"
|
||||
RateLimitedErr = errNS + "rateLimited"
|
||||
)
|
||||
|
||||
// ProblemDetails the problem details object.
|
||||
|
|
@ -25,30 +28,34 @@ type ProblemDetails struct {
|
|||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ProblemDetails) Error() string {
|
||||
msg := new(strings.Builder)
|
||||
|
||||
_, _ = fmt.Fprintf(msg, "acme: error: %d", p.HTTPStatus)
|
||||
|
||||
if p.Method != "" || p.URL != "" {
|
||||
_, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Method, p.URL)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(msg, " :: %s :: %s", p.Type, p.Detail)
|
||||
|
||||
for _, sub := range p.SubProblems {
|
||||
_, _ = fmt.Fprintf(msg, ", problem: %q :: %s", sub.Type, sub.Detail)
|
||||
}
|
||||
|
||||
if p.Instance != "" {
|
||||
msg.WriteString(", url: " + p.Instance)
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
// SubProblem a "subproblems".
|
||||
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.7.1
|
||||
type SubProblem struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Identifier Identifier `json:"identifier,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProblemDetails) Error() string {
|
||||
msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus)
|
||||
if p.Method != "" || p.URL != "" {
|
||||
msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL)
|
||||
}
|
||||
msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail)
|
||||
|
||||
for _, sub := range p.SubProblems {
|
||||
msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail)
|
||||
}
|
||||
|
||||
if p.Instance != "" {
|
||||
msg += ", url: " + p.Instance
|
||||
}
|
||||
|
||||
return msg
|
||||
Identifier Identifier `json:"identifier"`
|
||||
}
|
||||
|
||||
// NonceError represents the error which is returned
|
||||
|
|
@ -56,3 +63,31 @@ func (p ProblemDetails) Error() string {
|
|||
type NonceError struct {
|
||||
*ProblemDetails
|
||||
}
|
||||
|
||||
func (e *NonceError) Unwrap() error {
|
||||
return e.ProblemDetails
|
||||
}
|
||||
|
||||
// AlreadyReplacedError represents the error which is returned
|
||||
// if the Server rejects the request because the identified certificate has already been marked as replaced.
|
||||
// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5
|
||||
type AlreadyReplacedError struct {
|
||||
*ProblemDetails
|
||||
}
|
||||
|
||||
func (e *AlreadyReplacedError) Unwrap() error {
|
||||
return e.ProblemDetails
|
||||
}
|
||||
|
||||
// RateLimitedError represents the error which is returned
|
||||
// if the server rejects the request because the client has exceeded the rate limit.
|
||||
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.6
|
||||
type RateLimitedError struct {
|
||||
*ProblemDetails
|
||||
|
||||
RetryAfter string
|
||||
}
|
||||
|
||||
func (e *RateLimitedError) Unwrap() error {
|
||||
return e.ProblemDetails
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
# syntax=docker/dockerfile:1.4
|
||||
FROM alpine:3
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apk --no-cache --no-progress add git ca-certificates tzdata \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
COPY lego /
|
||||
COPY $TARGETPLATFORM/lego /
|
||||
|
||||
ENTRYPOINT ["/lego"]
|
||||
EXPOSE 80
|
||||
|
|
|
|||
|
|
@ -57,8 +57,10 @@ type DERCertificateBytes []byte
|
|||
// ParsePEMBundle parses a certificate bundle from top to bottom and returns
|
||||
// a slice of x509 certificates. This function will error if no certificates are found.
|
||||
func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
||||
var certificates []*x509.Certificate
|
||||
var certDERBlock *pem.Block
|
||||
var (
|
||||
certificates []*x509.Certificate
|
||||
certDERBlock *pem.Block
|
||||
)
|
||||
|
||||
for {
|
||||
certDERBlock, bundle = pem.Decode(bundle)
|
||||
|
|
@ -71,6 +73,7 @@ func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificates = append(certificates, cert)
|
||||
}
|
||||
}
|
||||
|
|
@ -135,10 +138,29 @@ func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
|
|||
return nil, fmt.Errorf("invalid KeyType: %s", keyType)
|
||||
}
|
||||
|
||||
// Deprecated: uses [CreateCSR] instead.
|
||||
func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
|
||||
var dnsNames []string
|
||||
var ipAddresses []net.IP
|
||||
for _, altname := range san {
|
||||
return CreateCSR(privateKey, CSROptions{
|
||||
Domain: domain,
|
||||
SAN: san,
|
||||
MustStaple: mustStaple,
|
||||
})
|
||||
}
|
||||
|
||||
type CSROptions struct {
|
||||
Domain string
|
||||
SAN []string
|
||||
MustStaple bool
|
||||
EmailAddresses []string
|
||||
}
|
||||
|
||||
func CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) {
|
||||
var (
|
||||
dnsNames []string
|
||||
ipAddresses []net.IP
|
||||
)
|
||||
|
||||
for _, altname := range opts.SAN {
|
||||
if ip := net.ParseIP(altname); ip != nil {
|
||||
ipAddresses = append(ipAddresses, ip)
|
||||
} else {
|
||||
|
|
@ -147,12 +169,13 @@ func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, must
|
|||
}
|
||||
|
||||
template := x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: domain},
|
||||
DNSNames: dnsNames,
|
||||
IPAddresses: ipAddresses,
|
||||
Subject: pkix.Name{CommonName: opts.Domain},
|
||||
DNSNames: dnsNames,
|
||||
EmailAddresses: opts.EmailAddresses,
|
||||
IPAddresses: ipAddresses,
|
||||
}
|
||||
|
||||
if mustStaple {
|
||||
if opts.MustStaple {
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
|
||||
Id: tlsFeatureExtensionOID,
|
||||
Value: ocspMustStapleFeature,
|
||||
|
|
@ -162,12 +185,13 @@ func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, must
|
|||
return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
|
||||
}
|
||||
|
||||
func PEMEncode(data interface{}) []byte {
|
||||
func PEMEncode(data any) []byte {
|
||||
return pem.EncodeToMemory(PEMBlock(data))
|
||||
}
|
||||
|
||||
func PEMBlock(data interface{}) *pem.Block {
|
||||
func PEMBlock(data any) *pem.Block {
|
||||
var pemBlock *pem.Block
|
||||
|
||||
switch key := data.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
keyBytes, _ := x509.MarshalECPrivateKey(key)
|
||||
|
|
@ -218,15 +242,15 @@ func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) {
|
|||
}
|
||||
|
||||
func GetCertificateMainDomain(cert *x509.Certificate) (string, error) {
|
||||
return getMainDomain(cert.Subject, cert.DNSNames)
|
||||
return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses)
|
||||
}
|
||||
|
||||
func GetCSRMainDomain(cert *x509.CertificateRequest) (string, error) {
|
||||
return getMainDomain(cert.Subject, cert.DNSNames)
|
||||
return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses)
|
||||
}
|
||||
|
||||
func getMainDomain(subject pkix.Name, dnsNames []string) (string, error) {
|
||||
if subject.CommonName == "" && len(dnsNames) == 0 {
|
||||
func getMainDomain(subject pkix.Name, dnsNames []string, ips []net.IP) (string, error) {
|
||||
if subject.CommonName == "" && len(dnsNames) == 0 && len(ips) == 0 {
|
||||
return "", errors.New("missing domain")
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +258,11 @@ func getMainDomain(subject pkix.Name, dnsNames []string) (string, error) {
|
|||
return subject.CommonName, nil
|
||||
}
|
||||
|
||||
return dnsNames[0], nil
|
||||
if len(dnsNames) > 0 {
|
||||
return dnsNames[0], nil
|
||||
}
|
||||
|
||||
return ips[0].String(), nil
|
||||
}
|
||||
|
||||
func ExtractDomains(cert *x509.Certificate) []string {
|
||||
|
|
@ -248,6 +276,7 @@ func ExtractDomains(cert *x509.Certificate) []string {
|
|||
if sanDomain == cert.Subject.CommonName {
|
||||
continue
|
||||
}
|
||||
|
||||
domains = append(domains, sanDomain)
|
||||
}
|
||||
|
||||
|
|
@ -299,6 +328,7 @@ func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pki
|
|||
|
||||
func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/pem"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -14,6 +13,13 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
testDomain1 = "lego.example"
|
||||
testDomain2 = "a.lego.example"
|
||||
testDomain3 = "b.lego.example"
|
||||
testDomain4 = "c.lego.example"
|
||||
)
|
||||
|
||||
func TestGeneratePrivateKey(t *testing.T) {
|
||||
key, err := GeneratePrivateKey(RSA2048)
|
||||
require.NoError(t, err, "Error generating private key")
|
||||
|
|
@ -22,7 +28,7 @@ func TestGeneratePrivateKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateCSR(t *testing.T) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err, "Error generating private key")
|
||||
|
||||
type expected struct {
|
||||
|
|
@ -33,55 +39,75 @@ func TestGenerateCSR(t *testing.T) {
|
|||
testCases := []struct {
|
||||
desc string
|
||||
privateKey crypto.PrivateKey
|
||||
domain string
|
||||
san []string
|
||||
mustStaple bool
|
||||
opts CSROptions
|
||||
expected expected
|
||||
}{
|
||||
{
|
||||
desc: "without SAN (nil)",
|
||||
privateKey: privateKey,
|
||||
domain: "lego.acme",
|
||||
mustStaple: true,
|
||||
expected: expected{len: 245},
|
||||
opts: CSROptions{
|
||||
Domain: testDomain1,
|
||||
MustStaple: true,
|
||||
},
|
||||
expected: expected{len: 382},
|
||||
},
|
||||
{
|
||||
desc: "without SAN (empty)",
|
||||
privateKey: privateKey,
|
||||
domain: "lego.acme",
|
||||
san: []string{},
|
||||
mustStaple: true,
|
||||
expected: expected{len: 245},
|
||||
opts: CSROptions{
|
||||
Domain: testDomain1,
|
||||
SAN: []string{},
|
||||
MustStaple: true,
|
||||
},
|
||||
expected: expected{len: 382},
|
||||
},
|
||||
{
|
||||
desc: "with SAN",
|
||||
privateKey: privateKey,
|
||||
domain: "lego.acme",
|
||||
san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
|
||||
mustStaple: true,
|
||||
expected: expected{len: 296},
|
||||
opts: CSROptions{
|
||||
Domain: testDomain1,
|
||||
SAN: []string{testDomain2, testDomain3, testDomain4},
|
||||
MustStaple: true,
|
||||
},
|
||||
expected: expected{len: 442},
|
||||
},
|
||||
{
|
||||
desc: "no domain",
|
||||
privateKey: privateKey,
|
||||
domain: "",
|
||||
mustStaple: true,
|
||||
expected: expected{len: 225},
|
||||
opts: CSROptions{
|
||||
Domain: "",
|
||||
MustStaple: true,
|
||||
},
|
||||
expected: expected{len: 359},
|
||||
},
|
||||
{
|
||||
desc: "no domain with SAN",
|
||||
privateKey: privateKey,
|
||||
domain: "",
|
||||
san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
|
||||
mustStaple: true,
|
||||
expected: expected{len: 276},
|
||||
opts: CSROptions{
|
||||
Domain: "",
|
||||
SAN: []string{testDomain2, testDomain3, testDomain4},
|
||||
MustStaple: true,
|
||||
},
|
||||
expected: expected{len: 419},
|
||||
},
|
||||
{
|
||||
desc: "private key nil",
|
||||
privateKey: nil,
|
||||
domain: "fizz.buzz",
|
||||
mustStaple: true,
|
||||
expected: expected{error: true},
|
||||
opts: CSROptions{
|
||||
Domain: testDomain1,
|
||||
MustStaple: true,
|
||||
},
|
||||
expected: expected{error: true},
|
||||
},
|
||||
{
|
||||
desc: "with email addresses",
|
||||
privateKey: privateKey,
|
||||
opts: CSROptions{
|
||||
Domain: "example.com",
|
||||
SAN: []string{"example.org"},
|
||||
EmailAddresses: []string{"foo@example.com", "bar@example.com"},
|
||||
},
|
||||
expected: expected{len: 421},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +115,7 @@ func TestGenerateCSR(t *testing.T) {
|
|||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
csr, err := GenerateCSR(test.privateKey, test.domain, test.san, test.mustStaple)
|
||||
csr, err := CreateCSR(test.privateKey, test.opts)
|
||||
|
||||
if test.expected.error {
|
||||
require.Error(t, err)
|
||||
|
|
@ -104,17 +130,17 @@ func TestGenerateCSR(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPEMEncode(t *testing.T) {
|
||||
buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
|
||||
|
||||
reader := MockRandReader{b: buf}
|
||||
key, err := rsa.GenerateKey(reader, 32)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err, "Error generating private key")
|
||||
|
||||
data := PEMEncode(key)
|
||||
require.NotNil(t, data)
|
||||
|
||||
exp := regexp.MustCompile(`^-----BEGIN RSA PRIVATE KEY-----\s+\S{60,}\s+-----END RSA PRIVATE KEY-----\s+`)
|
||||
assert.Regexp(t, exp, string(data))
|
||||
p, rest := pem.Decode(data)
|
||||
|
||||
assert.Equal(t, "RSA PRIVATE KEY", p.Type)
|
||||
assert.Empty(t, rest)
|
||||
assert.Empty(t, p.Headers)
|
||||
}
|
||||
|
||||
func TestParsePEMCertificate(t *testing.T) {
|
||||
|
|
@ -149,10 +175,13 @@ func TestParsePEMPrivateKey(t *testing.T) {
|
|||
|
||||
pemPrivateKey := PEMEncode(privateKey)
|
||||
|
||||
// Decoding a key should work and create an identical key to the original
|
||||
// Decoding a key should work and create an identical RSA key to the original,
|
||||
// ignoring precomputed values.
|
||||
decoded, err := ParsePEMPrivateKey(pemPrivateKey)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, decoded, privateKey)
|
||||
|
||||
decodedRsaPrivateKey := decoded.(*rsa.PrivateKey)
|
||||
require.True(t, decodedRsaPrivateKey.Equal(privateKey))
|
||||
|
||||
// Decoding a PEM block that doesn't contain a private key should error
|
||||
_, err = ParsePEMPrivateKey(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE"}))
|
||||
|
|
@ -166,11 +195,3 @@ func TestParsePEMPrivateKey(t *testing.T) {
|
|||
_, err = ParsePEMPrivateKey([]byte("This is not PEM"))
|
||||
require.Errorf(t, err, "Expected to return an error for non-PEM input")
|
||||
}
|
||||
|
||||
type MockRandReader struct {
|
||||
b *bytes.Buffer
|
||||
}
|
||||
|
||||
func (r MockRandReader) Read(p []byte) (int, error) {
|
||||
return r.b.Read(p)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authoriz
|
|||
var responses []acme.Authorization
|
||||
|
||||
failures := newObtainError()
|
||||
|
||||
for range len(order.Authorizations) {
|
||||
select {
|
||||
case res := <-resc:
|
||||
|
|
@ -52,7 +53,7 @@ func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force boo
|
|||
for _, authzURL := range order.Authorizations {
|
||||
auth, err := c.core.Authorizations.Get(authzURL)
|
||||
if err != nil {
|
||||
log.Infof("Unable to get the authorization for: %s", authzURL)
|
||||
log.Infof("Unable to get the authorization for %s: %v", authzURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force boo
|
|||
}
|
||||
|
||||
log.Infof("Deactivating auth: %s", authzURL)
|
||||
|
||||
if c.core.Authorizations.Deactivate(authzURL) != nil {
|
||||
log.Infof("Unable to deactivate the authorization: %s", authzURL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,18 +65,26 @@ type Resource struct {
|
|||
// If `AlwaysDeactivateAuthorizations` is true, the authorizations are also relinquished if the obtain request was successful.
|
||||
// See https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2.
|
||||
type ObtainRequest struct {
|
||||
Domains []string
|
||||
PrivateKey crypto.PrivateKey
|
||||
MustStaple bool
|
||||
Domains []string
|
||||
PrivateKey crypto.PrivateKey
|
||||
MustStaple bool
|
||||
EmailAddresses []string
|
||||
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
|
||||
// A string uniquely identifying the profile
|
||||
// which will be used to affect issuance of the certificate requested by this Order.
|
||||
// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4
|
||||
Profile string
|
||||
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
AlwaysDeactivateAuthorizations bool
|
||||
|
||||
// A string uniquely identifying a previously-issued certificate which this
|
||||
// order is intended to replace.
|
||||
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
|
||||
// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5
|
||||
ReplacesCertID string
|
||||
}
|
||||
|
||||
|
|
@ -89,14 +97,23 @@ type ObtainRequest struct {
|
|||
type ObtainForCSRRequest struct {
|
||||
CSR *x509.CertificateRequest
|
||||
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
PrivateKey crypto.PrivateKey
|
||||
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
|
||||
// A string uniquely identifying the profile
|
||||
// which will be used to affect issuance of the certificate requested by this Order.
|
||||
// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4
|
||||
Profile string
|
||||
|
||||
AlwaysDeactivateAuthorizations bool
|
||||
|
||||
// A string uniquely identifying a previously-issued certificate which this
|
||||
// order is intended to replace.
|
||||
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
|
||||
// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5
|
||||
ReplacesCertID string
|
||||
}
|
||||
|
||||
|
|
@ -108,6 +125,7 @@ type CertifierOptions struct {
|
|||
KeyType certcrypto.KeyType
|
||||
Timeout time.Duration
|
||||
OverallRequestLimit int
|
||||
DisableCommonName bool
|
||||
}
|
||||
|
||||
// Certifier A service to obtain/renew/revoke certificates.
|
||||
|
|
@ -154,6 +172,7 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
|
|||
orderOpts := &api.OrderOptions{
|
||||
NotBefore: request.NotBefore,
|
||||
NotAfter: request.NotAfter,
|
||||
Profile: request.Profile,
|
||||
ReplacesCertID: request.ReplacesCertID,
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +198,8 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
|
|||
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||
|
||||
failures := newObtainError()
|
||||
cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain)
|
||||
|
||||
cert, err := c.getForOrder(domains, order, request)
|
||||
if err != nil {
|
||||
for _, auth := range authz {
|
||||
failures.Add(challenge.GetTargetedDomain(auth), err)
|
||||
|
|
@ -220,6 +240,7 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
|
|||
orderOpts := &api.OrderOptions{
|
||||
NotBefore: request.NotBefore,
|
||||
NotAfter: request.NotAfter,
|
||||
Profile: request.Profile,
|
||||
ReplacesCertID: request.ReplacesCertID,
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +266,13 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
|
|||
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||
|
||||
failures := newObtainError()
|
||||
cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, nil, request.PreferredChain)
|
||||
|
||||
var privateKey []byte
|
||||
if request.PrivateKey != nil {
|
||||
privateKey = certcrypto.PEMEncode(request.PrivateKey)
|
||||
}
|
||||
|
||||
cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, privateKey, request.PreferredChain)
|
||||
if err != nil {
|
||||
for _, auth := range authz {
|
||||
failures.Add(challenge.GetTargetedDomain(auth), err)
|
||||
|
|
@ -264,9 +291,12 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
|
|||
return cert, failures.Join()
|
||||
}
|
||||
|
||||
func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) {
|
||||
func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, request ObtainRequest) (*Resource, error) {
|
||||
privateKey := request.PrivateKey
|
||||
|
||||
if privateKey == nil {
|
||||
var err error
|
||||
|
||||
privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -274,7 +304,7 @@ func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bund
|
|||
}
|
||||
|
||||
commonName := ""
|
||||
if len(domains[0]) <= 64 {
|
||||
if len(domains[0]) <= 64 && !c.options.DisableCommonName {
|
||||
commonName = domains[0]
|
||||
}
|
||||
|
||||
|
|
@ -296,13 +326,19 @@ func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bund
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: should the CSR be customizable?
|
||||
csr, err := certcrypto.GenerateCSR(privateKey, commonName, san, mustStaple)
|
||||
csrOptions := certcrypto.CSROptions{
|
||||
Domain: commonName,
|
||||
SAN: san,
|
||||
MustStaple: request.MustStaple,
|
||||
EmailAddresses: request.EmailAddresses,
|
||||
}
|
||||
|
||||
csr, err := certcrypto.CreateCSR(privateKey, csrOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey), preferredChain)
|
||||
return c.getForCSR(domains, order, request.Bundle, csr, certcrypto.PEMEncode(privateKey), request.PreferredChain)
|
||||
}
|
||||
|
||||
func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) {
|
||||
|
|
@ -435,11 +471,15 @@ type RenewOptions struct {
|
|||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
// If true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
|
||||
Profile string
|
||||
|
||||
AlwaysDeactivateAuthorizations bool
|
||||
// Not supported for CSR request.
|
||||
MustStaple bool
|
||||
MustStaple bool
|
||||
EmailAddresses []string
|
||||
}
|
||||
|
||||
// Renew takes a Resource and tries to renew the certificate.
|
||||
|
|
@ -452,6 +492,7 @@ type RenewOptions struct {
|
|||
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
|
||||
//
|
||||
// For private key reuse the PrivateKey property of the passed in Resource should be non-nil.
|
||||
//
|
||||
// Deprecated: use RenewWithOptions instead.
|
||||
func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredChain string) (*Resource, error) {
|
||||
return c.RenewWithOptions(certRes, &RenewOptions{
|
||||
|
|
@ -505,6 +546,7 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*
|
|||
request.NotAfter = options.NotAfter
|
||||
request.Bundle = options.Bundle
|
||||
request.PreferredChain = options.PreferredChain
|
||||
request.Profile = options.Profile
|
||||
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
|
||||
}
|
||||
|
||||
|
|
@ -530,6 +572,8 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*
|
|||
request.NotAfter = options.NotAfter
|
||||
request.Bundle = options.Bundle
|
||||
request.PreferredChain = options.PreferredChain
|
||||
request.EmailAddresses = options.EmailAddresses
|
||||
request.Profile = options.Profile
|
||||
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
|
||||
}
|
||||
|
||||
|
|
@ -668,7 +712,7 @@ func checkOrderStatus(order acme.ExtendedOrder) (bool, error) {
|
|||
case acme.StatusValid:
|
||||
return true, nil
|
||||
case acme.StatusInvalid:
|
||||
return false, order.Error
|
||||
return false, fmt.Errorf("invalid order: %w", order.Err())
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -681,6 +725,7 @@ func checkOrderStatus(order acme.ExtendedOrder) (bool, error) {
|
|||
// https://www.rfc-editor.org/rfc/rfc5280.html#section-7
|
||||
func sanitizeDomain(domains []string) []string {
|
||||
var sanitizedDomains []string
|
||||
|
||||
for _, domain := range domains {
|
||||
sanitizedDomain, err := idna.ToASCII(domain)
|
||||
if err != nil {
|
||||
|
|
@ -689,5 +734,6 @@ func sanitizeDomain(domains []string) []string {
|
|||
sanitizedDomains = append(sanitizedDomains, sanitizedDomain)
|
||||
}
|
||||
}
|
||||
|
||||
return sanitizedDomains
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package certificate
|
|||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
|
@ -12,6 +11,7 @@ import (
|
|||
"github.com/go-acme/lego/v4/acme/api"
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -175,20 +175,14 @@ Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
|
|||
`
|
||||
|
||||
func Test_checkResponse(t *testing.T) {
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
|
||||
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, err := w.Write([]byte(certResponseMock))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
server := tester.MockACMEServer().
|
||||
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
|
@ -196,7 +190,7 @@ func Test_checkResponse(t *testing.T) {
|
|||
order := acme.ExtendedOrder{
|
||||
Order: acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
Certificate: server.URL + "/certificate",
|
||||
},
|
||||
}
|
||||
certRes := &Resource{}
|
||||
|
|
@ -205,7 +199,7 @@ func Test_checkResponse(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.True(t, valid)
|
||||
assert.NotNil(t, certRes)
|
||||
assert.Equal(t, "", certRes.Domain)
|
||||
assert.Empty(t, certRes.Domain)
|
||||
assert.Contains(t, certRes.CertStableURL, "/certificate")
|
||||
assert.Contains(t, certRes.CertURL, "/certificate")
|
||||
assert.Nil(t, certRes.CSR)
|
||||
|
|
@ -215,30 +209,14 @@ func Test_checkResponse(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_checkResponse_issuerRelUp(t *testing.T) {
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
|
||||
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`)
|
||||
_, err := w.Write([]byte(certResponseMock))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/issuer", func(w http.ResponseWriter, _ *http.Request) {
|
||||
p, _ := pem.Decode([]byte(issuerMock))
|
||||
_, err := w.Write(p.Bytes)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
server := tester.MockACMEServer().
|
||||
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
|
@ -246,7 +224,7 @@ func Test_checkResponse_issuerRelUp(t *testing.T) {
|
|||
order := acme.ExtendedOrder{
|
||||
Order: acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
Certificate: server.URL + "/certificate",
|
||||
},
|
||||
}
|
||||
certRes := &Resource{}
|
||||
|
|
@ -255,7 +233,7 @@ func Test_checkResponse_issuerRelUp(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.True(t, valid)
|
||||
assert.NotNil(t, certRes)
|
||||
assert.Equal(t, "", certRes.Domain)
|
||||
assert.Empty(t, certRes.Domain)
|
||||
assert.Contains(t, certRes.CertStableURL, "/certificate")
|
||||
assert.Contains(t, certRes.CertURL, "/certificate")
|
||||
assert.Nil(t, certRes.CSR)
|
||||
|
|
@ -265,20 +243,14 @@ func Test_checkResponse_issuerRelUp(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_checkResponse_no_bundle(t *testing.T) {
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
|
||||
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, err := w.Write([]byte(certResponseMock))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
server := tester.MockACMEServer().
|
||||
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
|
@ -286,7 +258,7 @@ func Test_checkResponse_no_bundle(t *testing.T) {
|
|||
order := acme.ExtendedOrder{
|
||||
Order: acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
Certificate: server.URL + "/certificate",
|
||||
},
|
||||
}
|
||||
certRes := &Resource{}
|
||||
|
|
@ -295,7 +267,7 @@ func Test_checkResponse_no_bundle(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.True(t, valid)
|
||||
assert.NotNil(t, certRes)
|
||||
assert.Equal(t, "", certRes.Domain)
|
||||
assert.Empty(t, certRes.Domain)
|
||||
assert.Contains(t, certRes.CertStableURL, "/certificate")
|
||||
assert.Contains(t, certRes.CertURL, "/certificate")
|
||||
assert.Nil(t, certRes.CSR)
|
||||
|
|
@ -305,30 +277,21 @@ func Test_checkResponse_no_bundle(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_checkResponse_alternate(t *testing.T) {
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().
|
||||
Route("POST /certificate",
|
||||
http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Add("Link",
|
||||
fmt.Sprintf(`<https://%s/certificate/1>;title="foo";rel="alternate"`, req.Context().Value(http.LocalAddrContextKey)))
|
||||
|
||||
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Add("Link", fmt.Sprintf(`<%s/certificate/1>;title="foo";rel="alternate"`, apiURL))
|
||||
|
||||
_, err := w.Write([]byte(certResponseMock))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/certificate/1", func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, err := w.Write([]byte(certResponseMock2))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
servermock.RawStringResponse(certResponseMock).ServeHTTP(rw, req)
|
||||
})).
|
||||
Route("/certificate/1", servermock.RawStringResponse(certResponseMock2)).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
|
@ -336,7 +299,7 @@ func Test_checkResponse_alternate(t *testing.T) {
|
|||
order := acme.ExtendedOrder{
|
||||
Order: acme.Order{
|
||||
Status: acme.StatusValid,
|
||||
Certificate: apiURL + "/certificate",
|
||||
Certificate: server.URL + "/certificate",
|
||||
},
|
||||
}
|
||||
certRes := &Resource{
|
||||
|
|
@ -358,37 +321,76 @@ func Test_checkResponse_alternate(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_Get(t *testing.T) {
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
|
||||
mux.HandleFunc("/acme/cert/test-cert", func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, err := w.Write([]byte(certResponseMock))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
server := tester.MockACMEServer().
|
||||
Route("POST /acme/cert/test-cert", servermock.RawStringResponse(certResponseMock)).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
||||
certRes, err := certifier.Get(apiURL+"/acme/cert/test-cert", true)
|
||||
certRes, err := certifier.Get(server.URL+"/acme/cert/test-cert", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, certRes)
|
||||
assert.Equal(t, "acme.wtf", certRes.Domain)
|
||||
assert.Equal(t, apiURL+"/acme/cert/test-cert", certRes.CertStableURL)
|
||||
assert.Equal(t, apiURL+"/acme/cert/test-cert", certRes.CertURL)
|
||||
assert.Equal(t, server.URL+"/acme/cert/test-cert", certRes.CertStableURL)
|
||||
assert.Equal(t, server.URL+"/acme/cert/test-cert", certRes.CertURL)
|
||||
assert.Nil(t, certRes.CSR)
|
||||
assert.Nil(t, certRes.PrivateKey)
|
||||
assert.Equal(t, certResponseMock, string(certRes.Certificate), "Certificate")
|
||||
assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate")
|
||||
}
|
||||
|
||||
func Test_checkOrderStatus(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
order acme.Order
|
||||
requireErr require.ErrorAssertionFunc
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "status valid",
|
||||
order: acme.Order{Status: acme.StatusValid},
|
||||
requireErr: require.NoError,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "status invalid",
|
||||
order: acme.Order{Status: acme.StatusInvalid},
|
||||
requireErr: require.Error,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status invalid with error",
|
||||
order: acme.Order{Status: acme.StatusInvalid, Error: &acme.ProblemDetails{}},
|
||||
requireErr: require.Error,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "unknown status",
|
||||
order: acme.Order{Status: "foo"},
|
||||
requireErr: require.NoError,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
status, err := checkOrderStatus(acme.ExtendedOrder{Order: test.order})
|
||||
test.requireErr(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type resolverMock struct {
|
||||
error error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/acme/api"
|
||||
)
|
||||
|
||||
// RenewalInfoRequest contains the necessary renewal information.
|
||||
|
|
@ -25,15 +26,15 @@ type RenewalInfoResponse struct {
|
|||
// RetryAfter header indicating the polling interval that the ACME server recommends.
|
||||
// Conforming clients SHOULD query the renewalInfo URL again after the RetryAfter period has passed,
|
||||
// as the server may provide a different suggestedWindow.
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.2
|
||||
// https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2
|
||||
RetryAfter time.Duration
|
||||
}
|
||||
|
||||
// ShouldRenewAt determines the optimal renewal time based on the current time (UTC),renewal window suggest by ARI, and the client's willingness to sleep.
|
||||
// It returns a pointer to a time.Time value indicating when the renewal should be attempted or nil if deferred until the next normal wake time.
|
||||
// This method implements the RECOMMENDED algorithm described in draft-ietf-acme-ari.
|
||||
// This method implements the RECOMMENDED algorithm described in RFC 9773.
|
||||
//
|
||||
// - (4.1-11. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
|
||||
// - (4.1-11. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html
|
||||
func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.Duration) *time.Time {
|
||||
// Explicitly convert all times to UTC.
|
||||
now = now.UTC()
|
||||
|
|
@ -71,7 +72,7 @@ func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.D
|
|||
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
|
||||
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
|
||||
// https://www.rfc-editor.org/rfc/rfc9773.html
|
||||
func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse, error) {
|
||||
certID, err := MakeARICertID(req.Cert)
|
||||
if err != nil {
|
||||
|
|
@ -85,22 +86,23 @@ func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse
|
|||
defer resp.Body.Close()
|
||||
|
||||
var info RenewalInfoResponse
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if retry := resp.Header.Get("Retry-After"); retry != "" {
|
||||
info.RetryAfter, err = time.ParseDuration(retry + "s")
|
||||
info.RetryAfter, err = api.ParseRetryAfter(retry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to parse Retry-After header: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// MakeARICertID constructs a certificate identifier as described in draft-ietf-acme-ari-03, section 4.1.
|
||||
// MakeARICertID constructs a certificate identifier as described in RFC 9773, section 4.1.
|
||||
func MakeARICertID(leaf *x509.Certificate) (string, error) {
|
||||
if leaf == nil {
|
||||
return "", errors.New("leaf certificate is nil")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/go-acme/lego/v4/acme/api"
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -42,31 +43,24 @@ func TestCertifier_GetRenewalInfo(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Test with a fake API.
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
mux.HandleFunc("/renewalInfo/"+ariLeafCertID, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Retry-After", "21600")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, wErr := w.Write([]byte(`{
|
||||
server := tester.MockACMEServer().
|
||||
Route("GET /renewalInfo/"+ariLeafCertID,
|
||||
servermock.RawStringResponse(`{
|
||||
"suggestedWindow": {
|
||||
"start": "2020-03-17T17:51:09Z",
|
||||
"end": "2020-03-17T18:21:09Z"
|
||||
},
|
||||
"explanationUrl": "https://aricapable.ca/docs/renewal-advice/"
|
||||
"explanationUrl": "https://aricapable.ca.example/docs/renewal-advice/"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, wErr)
|
||||
})
|
||||
}`).
|
||||
WithHeader("Content-Type", "application/json").
|
||||
WithHeader("Retry-After", "21600")).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
|
@ -76,10 +70,46 @@ func TestCertifier_GetRenewalInfo(t *testing.T) {
|
|||
require.NotNil(t, ri)
|
||||
assert.Equal(t, "2020-03-17T17:51:09Z", ri.SuggestedWindow.Start.Format(time.RFC3339))
|
||||
assert.Equal(t, "2020-03-17T18:21:09Z", ri.SuggestedWindow.End.Format(time.RFC3339))
|
||||
assert.Equal(t, "https://aricapable.ca/docs/renewal-advice/", ri.ExplanationURL)
|
||||
assert.Equal(t, "https://aricapable.ca.example/docs/renewal-advice/", ri.ExplanationURL)
|
||||
assert.Equal(t, time.Duration(21600000000000), ri.RetryAfter)
|
||||
}
|
||||
|
||||
func TestCertifier_GetRenewalInfo_retryAfter(t *testing.T) {
|
||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
||||
require.NoError(t, err)
|
||||
|
||||
server := tester.MockACMEServer().
|
||||
Route("GET /renewalInfo/"+ariLeafCertID,
|
||||
servermock.RawStringResponse(`{
|
||||
"suggestedWindow": {
|
||||
"start": "2020-03-17T17:51:09Z",
|
||||
"end": "2020-03-17T18:21:09Z"
|
||||
},
|
||||
"explanationUrl": "https://aricapable.ca.example/docs/renewal-advice/"
|
||||
}
|
||||
}`).
|
||||
WithHeader("Content-Type", "application/json").
|
||||
WithHeader("Retry-After", time.Now().UTC().Add(6*time.Hour).Format(time.RFC1123))).
|
||||
BuildHTTPS(t)
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
||||
ri, err := certifier.GetRenewalInfo(RenewalInfoRequest{leaf})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ri)
|
||||
assert.Equal(t, "2020-03-17T17:51:09Z", ri.SuggestedWindow.Start.Format(time.RFC3339))
|
||||
assert.Equal(t, "2020-03-17T18:21:09Z", ri.SuggestedWindow.End.Format(time.RFC3339))
|
||||
assert.Equal(t, "https://aricapable.ca.example/docs/renewal-advice/", ri.ExplanationURL)
|
||||
|
||||
assert.InDelta(t, 6, ri.RetryAfter.Hours(), 0.001)
|
||||
}
|
||||
|
||||
func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
||||
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
|
||||
require.NoError(t, err)
|
||||
|
|
@ -88,24 +118,23 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
|||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
httpClient *http.Client
|
||||
request RenewalInfoRequest
|
||||
handler http.HandlerFunc
|
||||
desc string
|
||||
timeout time.Duration
|
||||
request RenewalInfoRequest
|
||||
handler http.HandlerFunc
|
||||
}{
|
||||
{
|
||||
desc: "API timeout",
|
||||
httpClient: &http.Client{Timeout: 500 * time.Millisecond}, // HTTP client that times out after 500ms.
|
||||
request: RenewalInfoRequest{leaf},
|
||||
desc: "API timeout",
|
||||
timeout: 500 * time.Millisecond, // HTTP client that times out after 500ms.
|
||||
request: RenewalInfoRequest{leaf},
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
// API that takes 2ms to respond.
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "API error",
|
||||
httpClient: http.DefaultClient,
|
||||
request: RenewalInfoRequest{leaf},
|
||||
desc: "API error",
|
||||
request: RenewalInfoRequest{leaf},
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
// API that responds with error instead of renewal info.
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
|
|
@ -117,10 +146,17 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
|
|||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
mux.HandleFunc("/renewalInfo/"+ariLeafCertID, test.handler)
|
||||
server := tester.MockACMEServer().
|
||||
Route("GET /renewalInfo/"+ariLeafCertID, test.handler).
|
||||
BuildHTTPS(t)
|
||||
|
||||
core, err := api.New(test.httpClient, "lego-test", apiURL+"/dir", "", key)
|
||||
client := server.Client()
|
||||
|
||||
if test.timeout != 0 {
|
||||
client.Timeout = test.timeout
|
||||
}
|
||||
|
||||
core, err := api.New(client, "lego-test", server.URL+"/dir", "", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
|
||||
|
|
|
|||
|
|
@ -40,5 +40,6 @@ func GetTargetedDomain(authz acme.Authorization) string {
|
|||
if authz.Wildcard {
|
||||
return "*." + authz.Identifier.Value
|
||||
}
|
||||
|
||||
return authz.Identifier.Value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ func CondOption(condition bool, opt ChallengeOption) ChallengeOption {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +119,7 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
|
|||
info := GetChallengeInfo(authz.Identifier.Value, keyAuth)
|
||||
|
||||
var timeout, interval time.Duration
|
||||
|
||||
switch provider := c.provider.(type) {
|
||||
case challenge.ProviderTimeout:
|
||||
timeout, interval = provider.Timeout()
|
||||
|
|
@ -134,6 +136,7 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
|
|||
if !stop || errP != nil {
|
||||
log.Infof("[%s] acme: Waiting for DNS record propagation.", domain)
|
||||
}
|
||||
|
||||
return stop, errP
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -141,6 +144,7 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
|
|||
}
|
||||
|
||||
chlng.KeyAuthorization = keyAuth
|
||||
|
||||
return c.validate(c.core, domain, chlng)
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +169,7 @@ func (c *Challenge) Sequential() (bool, time.Duration) {
|
|||
if p, ok := c.provider.(sequential); ok {
|
||||
return ok, p.Sequential()
|
||||
}
|
||||
|
||||
return false, 0
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +178,7 @@ type sequential interface {
|
|||
}
|
||||
|
||||
// GetRecord returns a DNS record which will fulfill the `dns-01` challenge.
|
||||
//
|
||||
// Deprecated: use GetChallengeInfo instead.
|
||||
func GetRecord(domain, keyAuth string) (fqdn, value string) {
|
||||
info := GetChallengeInfo(domain, keyAuth)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,14 @@ const (
|
|||
)
|
||||
|
||||
// DNSProviderManual is an implementation of the ChallengeProvider interface.
|
||||
// TODO(ldez): move this to providers/dns/manual
|
||||
//
|
||||
// Deprecated: Use the manual.DNSProvider instead.
|
||||
type DNSProviderManual struct{}
|
||||
|
||||
// NewDNSProviderManual returns a DNSProviderManual instance.
|
||||
//
|
||||
// Deprecated: Use the manual.NewDNSProvider instead.
|
||||
func NewDNSProviderManual() (*DNSProviderManual, error) {
|
||||
return &DNSProviderManual{}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -12,6 +11,8 @@ import (
|
|||
"github.com/go-acme/lego/v4/acme/api"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/dnsmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -32,12 +33,12 @@ func (p *providerTimeoutMock) CleanUp(domain, token, keyAuth string) error { ret
|
|||
func (p *providerTimeoutMock) Timeout() (time.Duration, time.Duration) { return p.timeout, p.interval }
|
||||
|
||||
func TestChallenge_PreSolve(t *testing.T) {
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err)
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
|
|
@ -114,12 +115,16 @@ func TestChallenge_PreSolve(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChallenge_Solve(t *testing.T) {
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
useAsNameserver(t, dnsmock.NewServer().
|
||||
Query("_acme-challenge.example.com. CNAME", dnsmock.Noop).
|
||||
Build(t))
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err)
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
|
|
@ -179,6 +184,7 @@ func TestChallenge_Solve(t *testing.T) {
|
|||
if test.preCheck != nil {
|
||||
options = append(options, WrapPreCheck(test.preCheck))
|
||||
}
|
||||
|
||||
chlg := NewChallenge(core, test.validate, test.provider, options...)
|
||||
|
||||
authz := acme.Authorization{
|
||||
|
|
@ -201,12 +207,12 @@ func TestChallenge_Solve(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChallenge_CleanUp(t *testing.T) {
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err)
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
|
|
@ -281,3 +287,55 @@ func TestChallenge_CleanUp(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChallengeInfo(t *testing.T) {
|
||||
useAsNameserver(t, dnsmock.NewServer().
|
||||
Query("_acme-challenge.example.com. CNAME", dnsmock.Noop).
|
||||
Build(t))
|
||||
|
||||
info := GetChallengeInfo("example.com", "123")
|
||||
|
||||
expected := ChallengeInfo{
|
||||
FQDN: "_acme-challenge.example.com.",
|
||||
EffectiveFQDN: "_acme-challenge.example.com.",
|
||||
Value: "pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, info)
|
||||
}
|
||||
|
||||
func TestGetChallengeInfo_CNAME(t *testing.T) {
|
||||
useAsNameserver(t, dnsmock.NewServer().
|
||||
Query("_acme-challenge.example.com. CNAME", dnsmock.CNAME("example.org.")).
|
||||
Query("example.org. CNAME", dnsmock.Noop).
|
||||
Build(t))
|
||||
|
||||
info := GetChallengeInfo("example.com", "123")
|
||||
|
||||
expected := ChallengeInfo{
|
||||
FQDN: "_acme-challenge.example.com.",
|
||||
EffectiveFQDN: "example.org.",
|
||||
Value: "pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, info)
|
||||
}
|
||||
|
||||
func TestGetChallengeInfo_CNAME_disabled(t *testing.T) {
|
||||
useAsNameserver(t, dnsmock.NewServer().
|
||||
// Never called when the env var works.
|
||||
Query("_acme-challenge.example.com. CNAME", dnsmock.CNAME("example.org.")).
|
||||
Build(t))
|
||||
|
||||
t.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
|
||||
|
||||
info := GetChallengeInfo("example.com", "123")
|
||||
|
||||
expected := ChallengeInfo{
|
||||
FQDN: "_acme-challenge.example.com.",
|
||||
EffectiveFQDN: "_acme-challenge.example.com.",
|
||||
Value: "pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, info)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
domain company.com
|
||||
domain example.com
|
||||
nameserver 10.200.3.249
|
||||
nameserver 10.200.3.250:5353
|
||||
nameserver 2001:4860:4860::8844
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package dns01
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ToFqdn converts the name into a fqdn appending a trailing dot.
|
||||
//
|
||||
// Deprecated: Use [github.com/miekg/dns.Fqdn] directly.
|
||||
func ToFqdn(name string) string {
|
||||
n := len(name)
|
||||
if n == 0 || name[n-1] == '.' {
|
||||
return name
|
||||
}
|
||||
return name + "."
|
||||
return dns.Fqdn(name)
|
||||
}
|
||||
|
||||
// UnFqdn converts the fqdn into a name removing the trailing dot.
|
||||
|
|
@ -15,5 +19,36 @@ func UnFqdn(name string) string {
|
|||
if n != 0 && name[n-1] == '.' {
|
||||
return name[:n-1]
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// UnFqdnDomainsSeq generates a sequence of "unFQDNed" domain names derived from a domain (FQDN or not) in descending order.
|
||||
func UnFqdnDomainsSeq(fqdn string) iter.Seq[string] {
|
||||
return func(yield func(string) bool) {
|
||||
if fqdn == "" {
|
||||
return
|
||||
}
|
||||
|
||||
for _, index := range dns.Split(fqdn) {
|
||||
if !yield(UnFqdn(fqdn[index:])) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DomainsSeq generates a sequence of domain names derived from a domain (FQDN or not) in descending order.
|
||||
func DomainsSeq(fqdn string) iter.Seq[string] {
|
||||
return func(yield func(string) bool) {
|
||||
if fqdn == "" {
|
||||
return
|
||||
}
|
||||
|
||||
for _, index := range dns.Split(fqdn) {
|
||||
if !yield(fqdn[index:]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,12 @@
|
|||
package dns01
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToFqdn(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
domain string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "simple",
|
||||
domain: "foo.example.com",
|
||||
expected: "foo.example.com.",
|
||||
},
|
||||
{
|
||||
desc: "already FQDN",
|
||||
domain: "foo.example.com.",
|
||||
expected: "foo.example.com.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fqdn := ToFqdn(test.domain)
|
||||
assert.Equal(t, test.expected, fqdn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnFqdn(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
|
@ -62,3 +35,103 @@ func TestUnFqdn(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnFqdnDomainsSeq(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fqdn string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
fqdn: "",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
desc: "TLD",
|
||||
fqdn: "com",
|
||||
expected: []string{"com"},
|
||||
},
|
||||
{
|
||||
desc: "2 levels",
|
||||
fqdn: "example.com",
|
||||
expected: []string{"example.com", "com"},
|
||||
},
|
||||
{
|
||||
desc: "3 levels",
|
||||
fqdn: "foo.example.com",
|
||||
expected: []string{"foo.example.com", "example.com", "com"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
for name, suffix := range map[string]string{"": "", " FQDN": "."} { //nolint:gocritic
|
||||
t.Run(test.desc+name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := slices.Collect(UnFqdnDomainsSeq(test.fqdn + suffix))
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainsSeq(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fqdn string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
fqdn: "",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty FQDN",
|
||||
fqdn: ".",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
desc: "TLD FQDN",
|
||||
fqdn: "com",
|
||||
expected: []string{"com"},
|
||||
},
|
||||
{
|
||||
desc: "TLD",
|
||||
fqdn: "com.",
|
||||
expected: []string{"com."},
|
||||
},
|
||||
{
|
||||
desc: "2 levels",
|
||||
fqdn: "example.com",
|
||||
expected: []string{"example.com", "com"},
|
||||
},
|
||||
{
|
||||
desc: "2 levels FQDN",
|
||||
fqdn: "example.com.",
|
||||
expected: []string{"example.com.", "com."},
|
||||
},
|
||||
{
|
||||
desc: "3 levels",
|
||||
fqdn: "foo.example.com",
|
||||
expected: []string{"foo.example.com", "example.com", "com"},
|
||||
},
|
||||
{
|
||||
desc: "3 levels FQDN",
|
||||
fqdn: "foo.example.com.",
|
||||
expected: []string{"foo.example.com.", "example.com.", "com."},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := slices.Collect(DomainsSeq(test.fqdn))
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
81
challenge/dns01/mock_test.go
Normal file
81
challenge/dns01/mock_test.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package dns01
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func fakeNS(name, ns string) *dns.NS {
|
||||
return &dns.NS{
|
||||
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 172800},
|
||||
Ns: ns,
|
||||
}
|
||||
}
|
||||
|
||||
func fakeA(name, ip string) *dns.A {
|
||||
return &dns.A{
|
||||
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10},
|
||||
A: net.ParseIP(ip),
|
||||
}
|
||||
}
|
||||
|
||||
func fakeTXT(name, value string) *dns.TXT {
|
||||
return &dns.TXT{
|
||||
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 10},
|
||||
Txt: []string{value},
|
||||
}
|
||||
}
|
||||
|
||||
// mockResolver modifies the default DNS resolver to use a custom network address during the test execution.
|
||||
// IMPORTANT: it modifying global variables.
|
||||
func mockResolver(t *testing.T, addr net.Addr) {
|
||||
t.Helper()
|
||||
|
||||
_, port, err := net.SplitHostPort(addr.String())
|
||||
require.NoError(t, err)
|
||||
|
||||
originalDefaultNameserverPort := defaultNameserverPort
|
||||
|
||||
t.Cleanup(func() {
|
||||
defaultNameserverPort = originalDefaultNameserverPort
|
||||
})
|
||||
|
||||
defaultNameserverPort = port
|
||||
|
||||
originalResolver := net.DefaultResolver
|
||||
|
||||
t.Cleanup(func() {
|
||||
net.DefaultResolver = originalResolver
|
||||
})
|
||||
|
||||
net.DefaultResolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{Timeout: 1 * time.Second}
|
||||
|
||||
return d.DialContext(ctx, network, addr.String())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func useAsNameserver(t *testing.T, addr net.Addr) {
|
||||
t.Helper()
|
||||
|
||||
ClearFqdnCache()
|
||||
t.Cleanup(func() {
|
||||
ClearFqdnCache()
|
||||
})
|
||||
|
||||
originalRecursiveNameservers := recursiveNameservers
|
||||
|
||||
t.Cleanup(func() {
|
||||
recursiveNameservers = originalRecursiveNameservers
|
||||
})
|
||||
|
||||
recursiveNameservers = ParseNameservers([]string{addr.String()})
|
||||
}
|
||||
|
|
@ -81,6 +81,7 @@ func getNameservers(path string, defaults []string) []string {
|
|||
|
||||
func ParseNameservers(servers []string) []string {
|
||||
var resolvers []string
|
||||
|
||||
for _, resolver := range servers {
|
||||
// ensure all servers have a port number
|
||||
if _, _, err := net.SplitHostPort(resolver); err != nil {
|
||||
|
|
@ -89,6 +90,7 @@ func ParseNameservers(servers []string) []string {
|
|||
resolvers = append(resolvers, resolver)
|
||||
}
|
||||
}
|
||||
|
||||
return resolvers
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +134,7 @@ func FindPrimaryNsByFqdnCustom(fqdn string, nameservers []string) (string, error
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err)
|
||||
}
|
||||
|
||||
return soa.primaryNs, nil
|
||||
}
|
||||
|
||||
|
|
@ -148,6 +151,7 @@ func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) {
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err)
|
||||
}
|
||||
|
||||
return soa.zone, nil
|
||||
}
|
||||
|
||||
|
|
@ -172,13 +176,12 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error)
|
|||
}
|
||||
|
||||
func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
|
||||
var err error
|
||||
var r *dns.Msg
|
||||
|
||||
labelIndexes := dns.Split(fqdn)
|
||||
for _, index := range labelIndexes {
|
||||
domain := fqdn[index:]
|
||||
var (
|
||||
err error
|
||||
r *dns.Msg
|
||||
)
|
||||
|
||||
for domain := range DomainsSeq(fqdn) {
|
||||
r, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
|
||||
if err != nil {
|
||||
continue
|
||||
|
|
@ -232,9 +235,11 @@ func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (
|
|||
return nil, &DNSError{Message: "empty list of nameservers"}
|
||||
}
|
||||
|
||||
var r *dns.Msg
|
||||
var err error
|
||||
var errAll error
|
||||
var (
|
||||
r *dns.Msg
|
||||
err error
|
||||
errAll error
|
||||
)
|
||||
|
||||
for _, ns := range nameservers {
|
||||
r, err = sendDNSQuery(m, ns)
|
||||
|
|
@ -267,6 +272,7 @@ func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
|
|||
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
|
||||
if ok, _ := strconv.ParseBool(os.Getenv("LEGO_EXPERIMENTAL_DNS_TCP_ONLY")); ok {
|
||||
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
|
||||
|
||||
r, _, err := tcp.Exchange(m, ns)
|
||||
if err != nil {
|
||||
return r, &DNSError{Message: "DNS call error", MsgIn: m, NS: ns, Err: err}
|
||||
|
|
|
|||
|
|
@ -5,138 +5,237 @@ import (
|
|||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester/dnsmock"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLookupNameserversOK(t *testing.T) {
|
||||
func Test_lookupNameserversOK(t *testing.T) {
|
||||
testCases := []struct {
|
||||
fqdn string
|
||||
nss []string
|
||||
desc string
|
||||
fakeDNSServer *dnsmock.Builder
|
||||
fqdn string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
fqdn: "en.wikipedia.org.",
|
||||
nss: []string{"ns0.wikimedia.org.", "ns1.wikimedia.org.", "ns2.wikimedia.org."},
|
||||
fqdn: "en.wikipedia.org.localhost.",
|
||||
fakeDNSServer: dnsmock.NewServer().
|
||||
Query("en.wikipedia.org.localhost SOA", dnsmock.CNAME("dyna.wikimedia.org.localhost")).
|
||||
Query("wikipedia.org.localhost SOA", dnsmock.SOA("")).
|
||||
Query("wikipedia.org.localhost NS",
|
||||
dnsmock.Answer(
|
||||
fakeNS("wikipedia.org.localhost.", "ns0.wikimedia.org.localhost."),
|
||||
fakeNS("wikipedia.org.localhost.", "ns1.wikimedia.org.localhost."),
|
||||
fakeNS("wikipedia.org.localhost.", "ns2.wikimedia.org.localhost."),
|
||||
),
|
||||
),
|
||||
expected: []string{"ns0.wikimedia.org.localhost.", "ns1.wikimedia.org.localhost.", "ns2.wikimedia.org.localhost."},
|
||||
},
|
||||
{
|
||||
fqdn: "www.google.com.",
|
||||
nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
|
||||
fqdn: "www.google.com.localhost.",
|
||||
fakeDNSServer: dnsmock.NewServer().
|
||||
Query("www.google.com.localhost. SOA", dnsmock.Noop).
|
||||
Query("google.com.localhost. SOA", dnsmock.SOA("")).
|
||||
Query("google.com.localhost. NS",
|
||||
dnsmock.Answer(
|
||||
fakeNS("google.com.localhost.", "ns1.google.com.localhost."),
|
||||
fakeNS("google.com.localhost.", "ns2.google.com.localhost."),
|
||||
fakeNS("google.com.localhost.", "ns3.google.com.localhost."),
|
||||
fakeNS("google.com.localhost.", "ns4.google.com.localhost."),
|
||||
),
|
||||
),
|
||||
expected: []string{"ns1.google.com.localhost.", "ns2.google.com.localhost.", "ns3.google.com.localhost.", "ns4.google.com.localhost."},
|
||||
},
|
||||
{
|
||||
fqdn: "physics.georgetown.edu.",
|
||||
nss: []string{"ns4.georgetown.edu.", "ns5.georgetown.edu.", "ns6.georgetown.edu."},
|
||||
fqdn: "mail.proton.me.localhost.",
|
||||
fakeDNSServer: dnsmock.NewServer().
|
||||
Query("mail.proton.me.localhost. SOA", dnsmock.Noop).
|
||||
Query("proton.me.localhost. SOA", dnsmock.SOA("")).
|
||||
Query("proton.me.localhost. NS",
|
||||
dnsmock.Answer(
|
||||
fakeNS("proton.me.localhost.", "ns1.proton.me.localhost."),
|
||||
fakeNS("proton.me.localhost.", "ns2.proton.me.localhost."),
|
||||
fakeNS("proton.me.localhost.", "ns3.proton.me.localhost."),
|
||||
),
|
||||
),
|
||||
expected: []string{"ns1.proton.me.localhost.", "ns2.proton.me.localhost.", "ns3.proton.me.localhost."},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.fqdn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
useAsNameserver(t, test.fakeDNSServer.Build(t))
|
||||
|
||||
nss, err := lookupNameservers(test.fqdn)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Strings(nss)
|
||||
sort.Strings(test.nss)
|
||||
sort.Strings(test.expected)
|
||||
|
||||
assert.EqualValues(t, test.nss, nss)
|
||||
assert.Equal(t, test.expected, nss)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupNameserversErr(t *testing.T) {
|
||||
func Test_lookupNameserversErr(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fqdn string
|
||||
error string
|
||||
desc string
|
||||
fqdn string
|
||||
fakeDNSServer *dnsmock.Builder
|
||||
error string
|
||||
}{
|
||||
{
|
||||
desc: "invalid tld",
|
||||
fqdn: "_null.n0n0.",
|
||||
error: "could not find zone",
|
||||
desc: "NXDOMAIN",
|
||||
fqdn: "example.invalid.",
|
||||
fakeDNSServer: dnsmock.NewServer().
|
||||
Query(". SOA", dnsmock.Error(dns.RcodeNameError)),
|
||||
error: "could not find zone: [fqdn=example.invalid.] could not find the start of authority for 'example.invalid.' [question='invalid. IN SOA', code=NXDOMAIN]",
|
||||
},
|
||||
{
|
||||
desc: "NS error",
|
||||
fqdn: "example.com.",
|
||||
fakeDNSServer: dnsmock.NewServer().
|
||||
Query("example.com. SOA", dnsmock.SOA("")).
|
||||
Query("example.com. NS", dnsmock.Error(dns.RcodeServerFailure)),
|
||||
error: "[zone=example.com.] could not determine authoritative nameservers",
|
||||
},
|
||||
{
|
||||
desc: "empty NS",
|
||||
fqdn: "example.com.",
|
||||
fakeDNSServer: dnsmock.NewServer().
|
||||
Query("example.com. SOA", dnsmock.SOA("")).
|
||||
Query("example.me NS", dnsmock.Noop),
|
||||
error: "[zone=example.com.] could not determine authoritative nameservers",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
useAsNameserver(t, test.fakeDNSServer.Build(t))
|
||||
|
||||
_, err := lookupNameservers(test.fqdn)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), test.error)
|
||||
assert.EqualError(t, err, test.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var findXByFqdnTestCases = []struct {
|
||||
type lookupSoaByFqdnTestCase struct {
|
||||
desc string
|
||||
fqdn string
|
||||
zone string
|
||||
primaryNs string
|
||||
nameservers []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
desc: "domain is a CNAME",
|
||||
fqdn: "mail.google.com.",
|
||||
zone: "google.com.",
|
||||
primaryNs: "ns1.google.com.",
|
||||
nameservers: recursiveNameservers,
|
||||
},
|
||||
{
|
||||
desc: "domain is a non-existent subdomain",
|
||||
fqdn: "foo.google.com.",
|
||||
zone: "google.com.",
|
||||
primaryNs: "ns1.google.com.",
|
||||
nameservers: recursiveNameservers,
|
||||
},
|
||||
{
|
||||
desc: "domain is a eTLD",
|
||||
fqdn: "example.com.ac.",
|
||||
zone: "ac.",
|
||||
primaryNs: "a0.nic.ac.",
|
||||
nameservers: recursiveNameservers,
|
||||
},
|
||||
{
|
||||
desc: "domain is a cross-zone CNAME",
|
||||
fqdn: "cross-zone-example.assets.sh.",
|
||||
zone: "assets.sh.",
|
||||
primaryNs: "gina.ns.cloudflare.com.",
|
||||
nameservers: recursiveNameservers,
|
||||
},
|
||||
{
|
||||
desc: "NXDOMAIN",
|
||||
fqdn: "test.lego.zz.",
|
||||
zone: "lego.zz.",
|
||||
nameservers: []string{"8.8.8.8:53"},
|
||||
expectedError: "[fqdn=test.lego.zz.] could not find the start of authority for 'test.lego.zz.' [question='zz. IN SOA', code=NXDOMAIN]",
|
||||
},
|
||||
{
|
||||
desc: "several non existent nameservers",
|
||||
fqdn: "mail.google.com.",
|
||||
zone: "google.com.",
|
||||
primaryNs: "ns1.google.com.",
|
||||
nameservers: []string{":7053", ":8053", "8.8.8.8:53"},
|
||||
},
|
||||
{
|
||||
desc: "only non-existent nameservers",
|
||||
fqdn: "mail.google.com.",
|
||||
zone: "google.com.",
|
||||
nameservers: []string{":7053", ":8053", ":9053"},
|
||||
// use only the start of the message because the port changes with each call: 127.0.0.1:XXXXX->127.0.0.1:7053.
|
||||
expectedError: "[fqdn=mail.google.com.] could not find the start of authority for 'mail.google.com.': DNS call error: read udp ",
|
||||
},
|
||||
{
|
||||
desc: "no nameservers",
|
||||
fqdn: "test.ldez.com.",
|
||||
zone: "ldez.com.",
|
||||
nameservers: []string{},
|
||||
expectedError: "[fqdn=test.ldez.com.] could not find the start of authority for 'test.ldez.com.': empty list of nameservers",
|
||||
},
|
||||
}
|
||||
|
||||
func lookupSoaByFqdnTestCases(t *testing.T) []lookupSoaByFqdnTestCase {
|
||||
t.Helper()
|
||||
|
||||
return []lookupSoaByFqdnTestCase{
|
||||
{
|
||||
desc: "domain is a CNAME",
|
||||
fqdn: "mail.example.com.",
|
||||
zone: "example.com.",
|
||||
primaryNs: "ns1.example.com.",
|
||||
nameservers: []string{
|
||||
dnsmock.NewServer().
|
||||
Query("mail.example.com. SOA", dnsmock.CNAME("example.com.")).
|
||||
Query("example.com. SOA", dnsmock.SOA("")).
|
||||
Build(t).
|
||||
String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "domain is a non-existent subdomain",
|
||||
fqdn: "foo.example.com.",
|
||||
zone: "example.com.",
|
||||
primaryNs: "ns1.example.com.",
|
||||
nameservers: []string{
|
||||
dnsmock.NewServer().
|
||||
Query("foo.example.com. SOA", dnsmock.Error(dns.RcodeNameError)).
|
||||
Query("example.com. SOA", dnsmock.SOA("")).
|
||||
Build(t).
|
||||
String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "domain is a eTLD",
|
||||
fqdn: "example.com.ac.",
|
||||
zone: "ac.",
|
||||
primaryNs: "ns1.nic.ac.",
|
||||
nameservers: []string{
|
||||
dnsmock.NewServer().
|
||||
Query("example.com.ac. SOA", dnsmock.Error(dns.RcodeNameError)).
|
||||
Query("com.ac. SOA", dnsmock.Error(dns.RcodeNameError)).
|
||||
Query("ac. SOA", dnsmock.SOA("")).
|
||||
Build(t).
|
||||
String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "domain is a cross-zone CNAME",
|
||||
fqdn: "cross-zone-example.example.com.",
|
||||
zone: "example.com.",
|
||||
primaryNs: "ns1.example.com.",
|
||||
nameservers: []string{
|
||||
dnsmock.NewServer().
|
||||
Query("cross-zone-example.example.com. SOA", dnsmock.CNAME("example.org.")).
|
||||
Query("example.com. SOA", dnsmock.SOA("")).
|
||||
Build(t).
|
||||
String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "NXDOMAIN",
|
||||
fqdn: "test.lego.invalid.",
|
||||
zone: "lego.invalid.",
|
||||
nameservers: []string{
|
||||
dnsmock.NewServer().
|
||||
Query("test.lego.invalid. SOA", dnsmock.Error(dns.RcodeNameError)).
|
||||
Query("lego.invalid. SOA", dnsmock.Error(dns.RcodeNameError)).
|
||||
Query("invalid. SOA", dnsmock.Error(dns.RcodeNameError)).
|
||||
Build(t).
|
||||
String(),
|
||||
},
|
||||
expectedError: `[fqdn=test.lego.invalid.] could not find the start of authority for 'test.lego.invalid.' [question='invalid. IN SOA', code=NXDOMAIN]`,
|
||||
},
|
||||
{
|
||||
desc: "several non existent nameservers",
|
||||
fqdn: "mail.example.com.",
|
||||
zone: "example.com.",
|
||||
primaryNs: "ns1.example.com.",
|
||||
nameservers: []string{
|
||||
":7053",
|
||||
":8053",
|
||||
dnsmock.NewServer().
|
||||
Query("mail.example.com. SOA", dnsmock.CNAME("example.com.")).
|
||||
Query("example.com. SOA", dnsmock.SOA("")).
|
||||
Build(t).
|
||||
String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "only non-existent nameservers",
|
||||
fqdn: "mail.example.com.",
|
||||
zone: "example.com.",
|
||||
nameservers: []string{":7053", ":8053", ":9053"},
|
||||
// use only the start of the message because the port changes with each call: 127.0.0.1:XXXXX->127.0.0.1:7053.
|
||||
expectedError: "[fqdn=mail.example.com.] could not find the start of authority for 'mail.example.com.': DNS call error: read udp ",
|
||||
},
|
||||
{
|
||||
desc: "no nameservers",
|
||||
fqdn: "test.example.com.",
|
||||
zone: "example.com.",
|
||||
nameservers: []string{},
|
||||
expectedError: "[fqdn=test.example.com.] could not find the start of authority for 'test.example.com.': empty list of nameservers",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindZoneByFqdnCustom(t *testing.T) {
|
||||
for _, test := range findXByFqdnTestCases {
|
||||
for _, test := range lookupSoaByFqdnTestCases(t) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
ClearFqdnCache()
|
||||
|
||||
|
|
@ -153,7 +252,7 @@ func TestFindZoneByFqdnCustom(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFindPrimaryNsByFqdnCustom(t *testing.T) {
|
||||
for _, test := range findXByFqdnTestCases {
|
||||
for _, test := range lookupSoaByFqdnTestCases(t) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
ClearFqdnCache()
|
||||
|
||||
|
|
@ -169,7 +268,7 @@ func TestFindPrimaryNsByFqdnCustom(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveConfServers(t *testing.T) {
|
||||
func Test_getNameservers_ResolveConfServers(t *testing.T) {
|
||||
testCases := []struct {
|
||||
fixture string
|
||||
expected []string
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import (
|
|||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// defaultNameserverPort used by authoritative NS.
|
||||
// This is for tests only.
|
||||
var defaultNameserverPort = "53"
|
||||
|
||||
// PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready.
|
||||
type PreCheckFunc func(fqdn, value string) (bool, error)
|
||||
|
||||
|
|
@ -25,6 +29,7 @@ func WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption {
|
|||
}
|
||||
|
||||
// DisableCompletePropagationRequirement obsolete.
|
||||
//
|
||||
// Deprecated: use DisableAuthoritativeNssPropagationRequirement instead.
|
||||
func DisableCompletePropagationRequirement() ChallengeOption {
|
||||
return DisableAuthoritativeNssPropagationRequirement()
|
||||
|
|
@ -121,7 +126,7 @@ func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) {
|
|||
func checkNameserversPropagation(fqdn, value string, nameservers []string, addPort bool) (bool, error) {
|
||||
for _, ns := range nameservers {
|
||||
if addPort {
|
||||
ns = net.JoinHostPort(ns, "53")
|
||||
ns = net.JoinHostPort(ns, defaultNameserverPort)
|
||||
}
|
||||
|
||||
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, false)
|
||||
|
|
@ -136,9 +141,11 @@ func checkNameserversPropagation(fqdn, value string, nameservers []string, addPo
|
|||
var records []string
|
||||
|
||||
var found bool
|
||||
|
||||
for _, rr := range r.Answer {
|
||||
if txt, ok := rr.(*dns.TXT); ok {
|
||||
record := strings.Join(txt.Txt, "")
|
||||
|
||||
records = append(records, record)
|
||||
if record == value {
|
||||
found = true
|
||||
|
|
|
|||
|
|
@ -3,40 +3,73 @@ package dns01
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester/dnsmock"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCheckDNSPropagation(t *testing.T) {
|
||||
func Test_preCheck_checkDNSPropagation(t *testing.T) {
|
||||
mockResolver(t,
|
||||
dnsmock.NewServer().
|
||||
Query("ns0.lego.localhost. A",
|
||||
dnsmock.Answer(fakeA("ns0.lego.localhost.", "127.0.0.1"))).
|
||||
Query("ns1.lego.localhost. A",
|
||||
dnsmock.Answer(fakeA("ns1.lego.localhost.", "127.0.0.1"))).
|
||||
Query("example.com. TXT",
|
||||
dnsmock.Answer(
|
||||
fakeTXT("example.com.", "one"),
|
||||
fakeTXT("example.com.", "two"),
|
||||
fakeTXT("example.com.", "three"),
|
||||
fakeTXT("example.com.", "four"),
|
||||
fakeTXT("example.com.", "five"),
|
||||
),
|
||||
).
|
||||
Build(t),
|
||||
)
|
||||
|
||||
useAsNameserver(t,
|
||||
dnsmock.NewServer().
|
||||
Query("acme-staging.api.example.com. SOA", dnsmock.Error(dns.RcodeNameError)).
|
||||
Query("api.example.com. SOA", dnsmock.Error(dns.RcodeNameError)).
|
||||
Query("example.com. SOA", dnsmock.SOA("")).
|
||||
Query("example.com. NS",
|
||||
dnsmock.Answer(
|
||||
fakeNS("example.com.", "ns0.lego.localhost."),
|
||||
fakeNS("example.com.", "ns1.lego.localhost."),
|
||||
),
|
||||
).
|
||||
Build(t),
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fqdn string
|
||||
value string
|
||||
expectError bool
|
||||
desc string
|
||||
fqdn string
|
||||
value string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
fqdn: "postman-echo.com.",
|
||||
value: "postman-domain-verification=c85de626cb79d941310696e06558e2e790223802f3697dfbdcaf65510152d52c",
|
||||
fqdn: "example.com.",
|
||||
value: "four",
|
||||
},
|
||||
{
|
||||
desc: "no TXT record",
|
||||
fqdn: "acme-staging.api.letsencrypt.org.",
|
||||
value: "fe01=",
|
||||
expectError: true,
|
||||
desc: "no matching TXT record",
|
||||
fqdn: "acme-staging.api.example.com.",
|
||||
value: "fe01=",
|
||||
expectedError: "did not return the expected TXT record [fqdn: acme-staging.api.example.com., value: fe01=]: one ,two ,three ,four ,five",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ClearFqdnCache()
|
||||
|
||||
check := newPreCheck()
|
||||
|
||||
ok, err := check.checkDNSPropagation(test.fqdn, test.value)
|
||||
if test.expectError {
|
||||
assert.Errorf(t, err, "PreCheckDNS must fail for %s", test.fqdn)
|
||||
if test.expectedError != "" {
|
||||
assert.ErrorContainsf(t, err, test.expectedError, "PreCheckDNS must fail for %s", test.fqdn)
|
||||
assert.False(t, ok, "PreCheckDNS must fail for %s", test.fqdn)
|
||||
} else {
|
||||
assert.NoErrorf(t, err, "PreCheckDNS failed for %s", test.fqdn)
|
||||
|
|
@ -46,69 +79,67 @@ func TestCheckDNSPropagation(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCheckAuthoritativeNss(t *testing.T) {
|
||||
func Test_checkNameserversPropagation_authoritativeNss(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fqdn, value string
|
||||
ns []string
|
||||
expected bool
|
||||
desc string
|
||||
fqdn, value string
|
||||
fakeDNSServer *dnsmock.Builder
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
desc: "TXT RR w/ expected value",
|
||||
fqdn: "8.8.8.8.asn.routeviews.org.",
|
||||
value: "151698.8.8.024",
|
||||
ns: []string{"asnums.routeviews.org."},
|
||||
expected: true,
|
||||
desc: "TXT RR w/ expected value",
|
||||
// NS: asnums.routeviews.org.
|
||||
fqdn: "8.8.8.8.asn.routeviews.org.",
|
||||
value: "151698.8.8.024",
|
||||
fakeDNSServer: dnsmock.NewServer().
|
||||
Query("8.8.8.8.asn.routeviews.org. TXT",
|
||||
dnsmock.Answer(
|
||||
fakeTXT("8.8.8.8.asn.routeviews.org.", "151698.8.8.024"),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
desc: "TXT RR w/ unexpected value",
|
||||
// NS: asnums.routeviews.org.
|
||||
fqdn: "8.8.8.8.asn.routeviews.org.",
|
||||
value: "fe01=",
|
||||
fakeDNSServer: dnsmock.NewServer().
|
||||
Query("8.8.8.8.asn.routeviews.org. TXT",
|
||||
dnsmock.Answer(
|
||||
fakeTXT("8.8.8.8.asn.routeviews.org.", "15169"),
|
||||
fakeTXT("8.8.8.8.asn.routeviews.org.", "8.8.8.0"),
|
||||
fakeTXT("8.8.8.8.asn.routeviews.org.", "24"),
|
||||
),
|
||||
),
|
||||
expectedError: "did not return the expected TXT record [fqdn: 8.8.8.8.asn.routeviews.org., value: fe01=]: 15169 ,8.8.8.0 ,24",
|
||||
},
|
||||
{
|
||||
desc: "No TXT RR",
|
||||
fqdn: "ns1.google.com.",
|
||||
ns: []string{"ns2.google.com."},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ClearFqdnCache()
|
||||
|
||||
ok, _ := checkNameserversPropagation(test.fqdn, test.value, test.ns, true)
|
||||
assert.Equal(t, test.expected, ok, test.fqdn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAuthoritativeNssErr(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fqdn, value string
|
||||
ns []string
|
||||
error string
|
||||
}{
|
||||
{
|
||||
desc: "TXT RR /w unexpected value",
|
||||
fqdn: "8.8.8.8.asn.routeviews.org.",
|
||||
value: "fe01=",
|
||||
ns: []string{"asnums.routeviews.org."},
|
||||
error: "did not return the expected TXT record",
|
||||
},
|
||||
{
|
||||
desc: "No TXT RR",
|
||||
// NS: ns2.google.com.
|
||||
fqdn: "ns1.google.com.",
|
||||
value: "fe01=",
|
||||
ns: []string{"ns2.google.com."},
|
||||
error: "did not return the expected TXT record",
|
||||
fakeDNSServer: dnsmock.NewServer().
|
||||
Query("ns1.google.com.", dnsmock.Noop),
|
||||
expectedError: "did not return the expected TXT record [fqdn: ns1.google.com., value: fe01=]: ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ClearFqdnCache()
|
||||
|
||||
_, err := checkNameserversPropagation(test.fqdn, test.value, test.ns, true)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), test.error)
|
||||
addr := test.fakeDNSServer.Build(t)
|
||||
|
||||
ok, err := checkNameserversPropagation(test.fqdn, test.value, []string{addr.String()}, false)
|
||||
|
||||
if test.expectedError == "" {
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, test.expectedError)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package http01
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ func (m *hostMatcher) name() string {
|
|||
}
|
||||
|
||||
func (m *hostMatcher) matches(r *http.Request, domain string) bool {
|
||||
return strings.HasPrefix(r.Host, domain)
|
||||
return matchDomain(r.Host, domain)
|
||||
}
|
||||
|
||||
// arbitraryMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name.
|
||||
|
|
@ -65,7 +66,7 @@ func (m arbitraryMatcher) name() string {
|
|||
}
|
||||
|
||||
func (m arbitraryMatcher) matches(r *http.Request, domain string) bool {
|
||||
return strings.HasPrefix(r.Header.Get(m.name()), domain)
|
||||
return matchDomain(r.Header.Get(m.name()), domain)
|
||||
}
|
||||
|
||||
// forwardedMatcher checks whether the Forwarded header contains a "host" element starting with a domain name.
|
||||
|
|
@ -87,7 +88,8 @@ func (m *forwardedMatcher) matches(r *http.Request, domain string) bool {
|
|||
}
|
||||
|
||||
host := fwds[0]["host"]
|
||||
return strings.HasPrefix(host, domain)
|
||||
|
||||
return matchDomain(host, domain)
|
||||
}
|
||||
|
||||
// parsing requires some form of state machine.
|
||||
|
|
@ -98,6 +100,7 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
|
|||
inquote := false
|
||||
|
||||
pos := 0
|
||||
|
||||
l := len(s)
|
||||
for i := 0; i < l; i++ {
|
||||
r := rune(s[i])
|
||||
|
|
@ -109,6 +112,7 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
|
|||
pos = i
|
||||
inquote = false
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +121,7 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
|
|||
if key == "" {
|
||||
return nil, fmt.Errorf("unexpected quoted string as pos %d", i)
|
||||
}
|
||||
|
||||
inquote = true
|
||||
pos = i + 1
|
||||
|
||||
|
|
@ -133,11 +138,10 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
|
|||
|
||||
case r == ',': // end of forwarded-element
|
||||
if key != "" {
|
||||
if val == "" {
|
||||
val = s[pos:i]
|
||||
}
|
||||
val = s[pos:i]
|
||||
cur[key] = val
|
||||
}
|
||||
|
||||
elements = append(elements, cur)
|
||||
cur = make(map[string]string)
|
||||
key = ""
|
||||
|
|
@ -160,11 +164,14 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
|
|||
if pos < len(s) {
|
||||
val = s[pos:]
|
||||
}
|
||||
|
||||
cur[key] = val
|
||||
}
|
||||
|
||||
if len(cur) > 0 {
|
||||
elements = append(elements, cur)
|
||||
}
|
||||
|
||||
return elements, nil
|
||||
}
|
||||
|
||||
|
|
@ -179,9 +186,19 @@ func skipWS(s string, i int) int {
|
|||
for isWS(rune(s[i+1])) {
|
||||
i++
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func isWS(r rune) bool {
|
||||
return strings.ContainsRune(" \t\v\r\n", r)
|
||||
}
|
||||
|
||||
func matchDomain(src, domain string) bool {
|
||||
addr, err := netip.ParseAddr(domain)
|
||||
if err == nil && addr.Is6() {
|
||||
domain = "[" + domain + "]"
|
||||
}
|
||||
|
||||
return strings.HasPrefix(src, domain)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
package http01
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseForwardedHeader(t *testing.T) {
|
||||
func Test_parseForwardedHeader(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -75,7 +77,7 @@ func TestParseForwardedHeader(t *testing.T) {
|
|||
actual, err := parseForwardedHeader(test.input)
|
||||
if test.err == "" {
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, test.want, actual)
|
||||
assert.Equal(t, test.want, actual)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), test.err)
|
||||
|
|
@ -83,3 +85,54 @@ func TestParseForwardedHeader(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_hostMatcher_matches(t *testing.T) {
|
||||
hm := &hostMatcher{}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
domain string
|
||||
req *http.Request
|
||||
expected assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "exact domain",
|
||||
domain: "example.com",
|
||||
req: httptest.NewRequest(http.MethodGet, "http://example.com", nil),
|
||||
expected: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "request with path",
|
||||
domain: "example.com",
|
||||
req: httptest.NewRequest(http.MethodGet, "http://example.com/foo/bar", nil),
|
||||
expected: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "ipv4",
|
||||
domain: "127.0.0.1",
|
||||
req: httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil),
|
||||
expected: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "ipv6",
|
||||
domain: "2001:db8::1",
|
||||
req: httptest.NewRequest(http.MethodGet, "http://[2001:db8::1]", nil),
|
||||
expected: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "ipv6 with brackets",
|
||||
domain: "[2001:db8::1]",
|
||||
req: httptest.NewRequest(http.MethodGet, "http://[2001:db8::1]", nil),
|
||||
expected: assert.True,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
hm.matches(test.req, test.domain)
|
||||
|
||||
test.expected(t, hm.matches(test.req, test.domain))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package http01
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/acme/api"
|
||||
|
|
@ -11,6 +12,16 @@ import (
|
|||
|
||||
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
|
||||
|
||||
type ChallengeOption func(*Challenge) error
|
||||
|
||||
// SetDelay sets a delay between the start of the HTTP server and the challenge validation.
|
||||
func SetDelay(delay time.Duration) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
chlg.delay = delay
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ChallengePath returns the URL path for the `http-01` challenge.
|
||||
func ChallengePath(token string) string {
|
||||
return "/.well-known/acme-challenge/" + token
|
||||
|
|
@ -20,14 +31,24 @@ type Challenge struct {
|
|||
core *api.Core
|
||||
validate ValidateFunc
|
||||
provider challenge.Provider
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
|
||||
return &Challenge{
|
||||
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {
|
||||
chlg := &Challenge{
|
||||
core: core,
|
||||
validate: validate,
|
||||
provider: provider,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
err := opt(chlg)
|
||||
if err != nil {
|
||||
log.Infof("challenge option error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return chlg
|
||||
}
|
||||
|
||||
func (c *Challenge) SetProvider(provider challenge.Provider) {
|
||||
|
|
@ -53,6 +74,7 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
|
|
@ -60,6 +82,11 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
|
|||
}
|
||||
}()
|
||||
|
||||
if c.delay > 0 {
|
||||
time.Sleep(c.delay)
|
||||
}
|
||||
|
||||
chlng.KeyAuthorization = keyAuth
|
||||
|
||||
return c.validate(c.core, domain, chlng)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ func NewUnixProviderServer(socketPath string, mode fs.FileMode) *ProviderServer
|
|||
// Present starts a web server and makes the token available at `ChallengePath(token)` for web requests.
|
||||
func (s *ProviderServer) Present(domain, token, keyAuth string) error {
|
||||
var err error
|
||||
|
||||
s.listener, err = net.Listen(s.network, s.GetAddress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start HTTP server for challenge: %w", err)
|
||||
|
|
@ -56,7 +57,9 @@ func (s *ProviderServer) Present(domain, token, keyAuth string) error {
|
|||
}
|
||||
|
||||
s.done = make(chan bool)
|
||||
|
||||
go s.serve(domain, token, keyAuth)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -69,8 +72,11 @@ func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {
|
|||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.listener.Close()
|
||||
|
||||
<-s.done
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -107,19 +113,24 @@ func (s *ProviderServer) serve(domain, token, keyAuth string) {
|
|||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet && s.matcher.matches(r, domain) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
_, err := w.Write([]byte(keyAuth))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("[%s] Served key authentication", domain)
|
||||
} else {
|
||||
log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure you are passing the %s header properly.", r.Host, r.Method, s.matcher.name())
|
||||
_, err := w.Write([]byte("TEST"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure you are passing the %s header properly.", r.Host, r.Method, s.matcher.name())
|
||||
|
||||
_, err := w.Write([]byte("TEST"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -133,5 +144,6 @@ func (s *ProviderServer) serve(domain, token, keyAuth string) {
|
|||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
s.done <- true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func TestProviderServer_GetAddress(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChallenge(t *testing.T) {
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
providerServer := NewProviderServer("", "23457")
|
||||
|
||||
|
|
@ -88,6 +88,7 @@ func TestChallenge(t *testing.T) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
if bodyStr != chlng.KeyAuthorization {
|
||||
|
|
@ -97,10 +98,10 @@ func TestChallenge(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
solver := NewChallenge(core, validate, providerServer)
|
||||
|
|
@ -123,7 +124,7 @@ func TestChallengeUnix(t *testing.T) {
|
|||
t.Skip("only for UNIX systems")
|
||||
}
|
||||
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
dir := t.TempDir()
|
||||
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
||||
|
|
@ -157,6 +158,7 @@ func TestChallengeUnix(t *testing.T) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
if bodyStr != chlng.KeyAuthorization {
|
||||
|
|
@ -166,10 +168,10 @@ func TestChallengeUnix(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
solver := NewChallenge(core, validate, providerServer)
|
||||
|
|
@ -188,12 +190,12 @@ func TestChallengeUnix(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChallengeInvalidPort(t *testing.T) {
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 128)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
validate := func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }
|
||||
|
|
@ -224,6 +226,7 @@ func (h *testProxyHeader) update(r *http.Request) {
|
|||
if h == nil || len(h.values) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if h.name == "Host" {
|
||||
r.Host = h.values[0]
|
||||
} else if h.name != "" {
|
||||
|
|
@ -371,7 +374,7 @@ func TestChallengeWithProxy(t *testing.T) {
|
|||
func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectError bool) {
|
||||
t.Helper()
|
||||
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
providerServer := NewProviderServer("localhost", "23457")
|
||||
if header != nil {
|
||||
|
|
@ -385,6 +388,7 @@ func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectErro
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.update(req)
|
||||
extra.update(req)
|
||||
|
||||
|
|
@ -402,6 +406,7 @@ func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectErro
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
if bodyStr != chlng.KeyAuthorization {
|
||||
|
|
@ -411,10 +416,10 @@ func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectErro
|
|||
return nil
|
||||
}
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
solver := NewChallenge(core, validate, providerServer)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package resolver
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
)
|
||||
|
||||
|
|
@ -16,10 +18,16 @@ func (e obtainError) Error() string {
|
|||
for domain := range e {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
|
||||
sort.Strings(domains)
|
||||
|
||||
for _, domain := range domains {
|
||||
_, _ = fmt.Fprintf(buffer, "[%s] %s\n", domain, e[domain])
|
||||
}
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (e obtainError) Unwrap() []error {
|
||||
return slices.AppendSeq(make([]error, 0, len(e)), maps.Values(e))
|
||||
}
|
||||
|
|
|
|||
70
challenge/resolver/errors_test.go
Normal file
70
challenge/resolver/errors_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_obtainError_Error(t *testing.T) {
|
||||
err := obtainError{
|
||||
"a": &acme.ProblemDetails{Type: "001"},
|
||||
"b": errors.New("oops"),
|
||||
"c": errors.New("I did it again"),
|
||||
}
|
||||
|
||||
require.EqualError(t, err, `error: one or more domains had a problem:
|
||||
[a] acme: error: 0 :: 001 ::
|
||||
[b] oops
|
||||
[c] I did it again
|
||||
`)
|
||||
}
|
||||
|
||||
func Test_obtainError_Unwrap(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
err obtainError
|
||||
assert assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "one ok",
|
||||
err: obtainError{
|
||||
"a": &acme.ProblemDetails{},
|
||||
"b": errors.New("oops"),
|
||||
"c": errors.New("I did it again"),
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "all ok",
|
||||
err: obtainError{
|
||||
"a": &acme.ProblemDetails{Type: "001"},
|
||||
"b": &acme.ProblemDetails{Type: "002"},
|
||||
"c": &acme.ProblemDetails{Type: "002"},
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "nope",
|
||||
err: obtainError{
|
||||
"a": errors.New("hello"),
|
||||
"b": errors.New("oops"),
|
||||
"c": errors.New("I did it again"),
|
||||
},
|
||||
assert: assert.False,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var pd *acme.ProblemDetails
|
||||
|
||||
test.assert(t, errors.As(test.err, &pd))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -50,11 +50,14 @@ func NewProber(solverManager *SolverManager) *Prober {
|
|||
func (p *Prober) Solve(authorizations []acme.Authorization) error {
|
||||
failures := make(obtainError)
|
||||
|
||||
var authSolvers []*selectedAuthSolver
|
||||
var authSolversSequential []*selectedAuthSolver
|
||||
var (
|
||||
authSolvers []*selectedAuthSolver
|
||||
authSolversSequential []*selectedAuthSolver
|
||||
)
|
||||
|
||||
// Loop through the resources, basically through the domains.
|
||||
// First pass just selects a solver for each authz.
|
||||
|
||||
for _, authz := range authorizations {
|
||||
domain := challenge.GetTargetedDomain(authz)
|
||||
if authz.Status == acme.StatusValid {
|
||||
|
|
@ -90,47 +93,88 @@ func (p *Prober) Solve(authorizations []acme.Authorization) error {
|
|||
if len(failures) > 0 {
|
||||
return failures
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
|
||||
// Some CA are using the same token,
|
||||
// this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records.
|
||||
// In the sequential mode, this is not a problem because we can solve the challenges in order.
|
||||
// But it can reduce the number of call the DNS provider APIs.
|
||||
uniq := make(map[string]struct{})
|
||||
|
||||
for i, authSolver := range authSolvers {
|
||||
// Submit the challenge
|
||||
domain := challenge.GetTargetedDomain(authSolver.authz)
|
||||
|
||||
chlg, _ := challenge.FindChallenge(challenge.DNS01, authSolver.authz)
|
||||
|
||||
if solvr, ok := authSolver.solver.(preSolver); ok {
|
||||
if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok && chlg.Token != "" {
|
||||
log.Infof("acme: duplicate token for %q (DNS-01); skipping pre-solve.", authSolver.authz.Identifier.Value)
|
||||
continue
|
||||
}
|
||||
|
||||
err := solvr.PreSolve(authSolver.authz)
|
||||
if err != nil {
|
||||
failures[domain] = err
|
||||
|
||||
cleanUp(authSolver.solver, authSolver.authz)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
uniq[authSolver.authz.Identifier.Value+chlg.Token] = struct{}{}
|
||||
}
|
||||
|
||||
// Solve challenge
|
||||
err := authSolver.solver.Solve(authSolver.authz)
|
||||
if err != nil {
|
||||
failures[domain] = err
|
||||
|
||||
cleanUp(authSolver.solver, authSolver.authz)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Clean challenge
|
||||
cleanUp(authSolver.solver, authSolver.authz)
|
||||
if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok || chlg.Token == "" {
|
||||
// Clean challenge
|
||||
cleanUp(authSolver.solver, authSolver.authz)
|
||||
|
||||
if len(authSolvers)-1 > i {
|
||||
solvr := authSolver.solver.(sequential)
|
||||
_, interval := solvr.Sequential()
|
||||
log.Infof("sequence: wait for %s", interval)
|
||||
time.Sleep(interval)
|
||||
if len(authSolvers)-1 > i {
|
||||
solvr := authSolver.solver.(sequential)
|
||||
_, interval := solvr.Sequential()
|
||||
log.Infof("sequence: wait for %s", interval)
|
||||
time.Sleep(interval)
|
||||
}
|
||||
|
||||
delete(uniq, authSolver.authz.Identifier.Value+chlg.Token)
|
||||
} else {
|
||||
log.Infof("acme: duplicate token for %q (DNS-01); skipping cleanup.", authSolver.authz.Identifier.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
|
||||
// Some CA are using the same token,
|
||||
// this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records.
|
||||
uniq := make(map[string]struct{})
|
||||
|
||||
// For all valid preSolvers, first submit the challenges, so they have max time to propagate
|
||||
for _, authSolver := range authSolvers {
|
||||
authz := authSolver.authz
|
||||
|
||||
chlg, err := challenge.FindChallenge(challenge.DNS01, authz)
|
||||
if err == nil {
|
||||
if _, ok := uniq[authz.Identifier.Value+chlg.Token]; ok {
|
||||
log.Infof("acme: duplicate token for %q (DNS-01); skipping pre-solve.", authSolver.authz.Identifier.Value)
|
||||
continue
|
||||
}
|
||||
|
||||
uniq[authz.Identifier.Value+chlg.Token] = struct{}{}
|
||||
}
|
||||
|
||||
if solvr, ok := authSolver.solver.(preSolver); ok {
|
||||
err := solvr.PreSolve(authz)
|
||||
if err != nil {
|
||||
|
|
@ -142,6 +186,16 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
|
|||
defer func() {
|
||||
// Clean all created TXT records
|
||||
for _, authSolver := range authSolvers {
|
||||
chlg, err := challenge.FindChallenge(challenge.DNS01, authSolver.authz)
|
||||
if err == nil {
|
||||
if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok {
|
||||
delete(uniq, authSolver.authz.Identifier.Value+chlg.Token)
|
||||
} else {
|
||||
log.Infof("acme: duplicate token for %q (DNS-01); skipping cleanup.", authSolver.authz.Identifier.Value)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
cleanUp(authSolver.solver, authSolver.authz)
|
||||
}
|
||||
}()
|
||||
|
|
@ -149,6 +203,7 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
|
|||
// Finally solve all challenges for real
|
||||
for _, authSolver := range authSolvers {
|
||||
authz := authSolver.authz
|
||||
|
||||
domain := challenge.GetTargetedDomain(authz)
|
||||
if failures[domain] != nil {
|
||||
// already failed in previous loop
|
||||
|
|
@ -165,6 +220,7 @@ func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
|
|||
func cleanUp(solvr solver, authz acme.Authorization) {
|
||||
if solvr, ok := solvr.(cleanup); ok {
|
||||
domain := challenge.GetTargetedDomain(authz)
|
||||
|
||||
err := solvr.CleanUp(authz)
|
||||
if err != nil {
|
||||
log.Warnf("[%s] acme: cleaning up failed: %v ", domain, err)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
|
|
@ -11,34 +12,68 @@ type preSolverMock struct {
|
|||
preSolve map[string]error
|
||||
solve map[string]error
|
||||
cleanUp map[string]error
|
||||
|
||||
preSolveCounter int
|
||||
solveCounter int
|
||||
cleanUpCounter int
|
||||
}
|
||||
|
||||
func (s *preSolverMock) PreSolve(authorization acme.Authorization) error {
|
||||
s.preSolveCounter++
|
||||
|
||||
return s.preSolve[authorization.Identifier.Value]
|
||||
}
|
||||
|
||||
func (s *preSolverMock) Solve(authorization acme.Authorization) error {
|
||||
s.solveCounter++
|
||||
|
||||
return s.solve[authorization.Identifier.Value]
|
||||
}
|
||||
|
||||
func (s *preSolverMock) CleanUp(authorization acme.Authorization) error {
|
||||
s.cleanUpCounter++
|
||||
|
||||
return s.cleanUp[authorization.Identifier.Value]
|
||||
}
|
||||
|
||||
func (s *preSolverMock) String() string {
|
||||
return fmt.Sprintf("PreSolve: %d, Solve: %d, CleanUp: %d", s.preSolveCounter, s.solveCounter, s.cleanUpCounter)
|
||||
}
|
||||
|
||||
func createStubAuthorizationHTTP01(domain, status string) acme.Authorization {
|
||||
return createStubAuthorization(domain, status, false, acme.Challenge{
|
||||
Type: challenge.HTTP01.String(),
|
||||
Validated: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func createStubAuthorizationDNS01(domain string, wildcard bool) acme.Authorization {
|
||||
var chlgs []acme.Challenge
|
||||
|
||||
if wildcard {
|
||||
chlgs = append(chlgs, acme.Challenge{
|
||||
Type: challenge.HTTP01.String(),
|
||||
Validated: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
chlgs = append(chlgs, acme.Challenge{
|
||||
Type: challenge.DNS01.String(),
|
||||
Validated: time.Now(),
|
||||
})
|
||||
|
||||
return createStubAuthorization(domain, acme.StatusProcessing, wildcard, chlgs...)
|
||||
}
|
||||
|
||||
func createStubAuthorization(domain, status string, wildcard bool, chlgs ...acme.Challenge) acme.Authorization {
|
||||
return acme.Authorization{
|
||||
Status: status,
|
||||
Expires: time.Now(),
|
||||
Wildcard: wildcard,
|
||||
Status: status,
|
||||
Expires: time.Now(),
|
||||
Identifier: acme.Identifier{
|
||||
Type: challenge.HTTP01.String(),
|
||||
Type: "dns",
|
||||
Value: domain,
|
||||
},
|
||||
Challenges: []acme.Challenge{
|
||||
{
|
||||
Type: challenge.HTTP01.String(),
|
||||
Validated: time.Now(),
|
||||
Error: nil,
|
||||
},
|
||||
},
|
||||
Challenges: chlgs,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,22 @@ package resolver
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProber_Solve(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
solvers map[challenge.Type]solver
|
||||
authz []acme.Authorization
|
||||
expectedError string
|
||||
desc string
|
||||
solvers map[challenge.Type]solver
|
||||
authz []acme.Authorization
|
||||
expectedError string
|
||||
expectedCounters map[challenge.Type]string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
|
|
@ -26,9 +29,33 @@ func TestProber_Solve(t *testing.T) {
|
|||
},
|
||||
},
|
||||
authz: []acme.Authorization{
|
||||
createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.com", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.org", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.net", acme.StatusProcessing),
|
||||
},
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.HTTP01: "PreSolve: 3, Solve: 3, CleanUp: 3",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "DNS-01 deduplicate",
|
||||
solvers: map[challenge.Type]solver{
|
||||
challenge.DNS01: &preSolverMock{
|
||||
preSolve: map[string]error{},
|
||||
solve: map[string]error{},
|
||||
cleanUp: map[string]error{},
|
||||
},
|
||||
},
|
||||
authz: []acme.Authorization{
|
||||
createStubAuthorizationDNS01("a.example", false),
|
||||
createStubAuthorizationDNS01("a.example", true),
|
||||
createStubAuthorizationDNS01("b.example", false),
|
||||
createStubAuthorizationDNS01("b.example", true),
|
||||
createStubAuthorizationDNS01("c.example", true),
|
||||
createStubAuthorizationDNS01("d.example", false),
|
||||
},
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.DNS01: "PreSolve: 4, Solve: 6, CleanUp: 4",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -41,9 +68,12 @@ func TestProber_Solve(t *testing.T) {
|
|||
},
|
||||
},
|
||||
authz: []acme.Authorization{
|
||||
createStubAuthorizationHTTP01("acme.wtf", acme.StatusValid),
|
||||
createStubAuthorizationHTTP01("lego.wtf", acme.StatusValid),
|
||||
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusValid),
|
||||
createStubAuthorizationHTTP01("example.com", acme.StatusValid),
|
||||
createStubAuthorizationHTTP01("example.org", acme.StatusValid),
|
||||
createStubAuthorizationHTTP01("example.net", acme.StatusValid),
|
||||
},
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.HTTP01: "PreSolve: 0, Solve: 0, CleanUp: 0",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -51,50 +81,56 @@ func TestProber_Solve(t *testing.T) {
|
|||
solvers: map[challenge.Type]solver{
|
||||
challenge.HTTP01: &preSolverMock{
|
||||
preSolve: map[string]error{
|
||||
"acme.wtf": errors.New("preSolve error acme.wtf"),
|
||||
"example.com": errors.New("preSolve error example.com"),
|
||||
},
|
||||
solve: map[string]error{
|
||||
"acme.wtf": errors.New("solve error acme.wtf"),
|
||||
"example.com": errors.New("solve error example.com"),
|
||||
},
|
||||
cleanUp: map[string]error{
|
||||
"acme.wtf": errors.New("clean error acme.wtf"),
|
||||
"example.com": errors.New("clean error example.com"),
|
||||
},
|
||||
},
|
||||
},
|
||||
authz: []acme.Authorization{
|
||||
createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.com", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.org", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.net", acme.StatusProcessing),
|
||||
},
|
||||
expectedError: `error: one or more domains had a problem:
|
||||
[acme.wtf] preSolve error acme.wtf
|
||||
[example.com] preSolve error example.com
|
||||
`,
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.HTTP01: "PreSolve: 3, Solve: 2, CleanUp: 3",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "errors at different stages",
|
||||
solvers: map[challenge.Type]solver{
|
||||
challenge.HTTP01: &preSolverMock{
|
||||
preSolve: map[string]error{
|
||||
"acme.wtf": errors.New("preSolve error acme.wtf"),
|
||||
"example.com": errors.New("preSolve error example.com"),
|
||||
},
|
||||
solve: map[string]error{
|
||||
"acme.wtf": errors.New("solve error acme.wtf"),
|
||||
"lego.wtf": errors.New("solve error lego.wtf"),
|
||||
"example.com": errors.New("solve error example.com"),
|
||||
"example.org": errors.New("solve error example.org"),
|
||||
},
|
||||
cleanUp: map[string]error{
|
||||
"mydomain.wtf": errors.New("clean error mydomain.wtf"),
|
||||
"example.net": errors.New("clean error example.net"),
|
||||
},
|
||||
},
|
||||
},
|
||||
authz: []acme.Authorization{
|
||||
createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.com", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.org", acme.StatusProcessing),
|
||||
createStubAuthorizationHTTP01("example.net", acme.StatusProcessing),
|
||||
},
|
||||
expectedError: `error: one or more domains had a problem:
|
||||
[acme.wtf] preSolve error acme.wtf
|
||||
[lego.wtf] solve error lego.wtf
|
||||
[example.com] preSolve error example.com
|
||||
[example.org] solve error example.org
|
||||
`,
|
||||
expectedCounters: map[challenge.Type]string{
|
||||
challenge.HTTP01: "PreSolve: 3, Solve: 2, CleanUp: 3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +148,10 @@ func TestProber_Solve(t *testing.T) {
|
|||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
for n, s := range test.solvers {
|
||||
assert.Equal(t, test.expectedCounters[n], fmt.Sprintf("%s", s))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/acme/api"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/go-acme/lego/v4/challenge/http01"
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/go-acme/lego/v4/log"
|
||||
"github.com/go-acme/lego/v4/platform/wait"
|
||||
)
|
||||
|
||||
type byType []acme.Challenge
|
||||
|
|
@ -36,14 +37,14 @@ func NewSolversManager(core *api.Core) *SolverManager {
|
|||
}
|
||||
|
||||
// SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge.
|
||||
func (c *SolverManager) SetHTTP01Provider(p challenge.Provider) error {
|
||||
c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p)
|
||||
func (c *SolverManager) SetHTTP01Provider(p challenge.Provider, opts ...http01.ChallengeOption) error {
|
||||
c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p, opts...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge.
|
||||
func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider) error {
|
||||
c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p)
|
||||
func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider, opts ...tlsalpn01.ChallengeOption) error {
|
||||
c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p, opts...)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ func (c *SolverManager) chooseSolver(authz acme.Authorization) solver {
|
|||
log.Infof("[%s] acme: use %s solver", domain, chlg.Type)
|
||||
return solvr
|
||||
}
|
||||
|
||||
log.Infof("[%s] acme: Could not find solver for: %s", domain, chlg.Type)
|
||||
}
|
||||
|
||||
|
|
@ -91,20 +93,20 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
ra, err := strconv.Atoi(chlng.RetryAfter)
|
||||
if err != nil {
|
||||
retryAfter, err := api.ParseRetryAfter(chlng.RetryAfter)
|
||||
if err != nil || retryAfter == 0 {
|
||||
// The ACME server MUST return a Retry-After.
|
||||
// If it doesn't, we'll just poll hard.
|
||||
// If it doesn't, or if it's invalid, we'll just poll hard.
|
||||
// Boulder does not implement the ability to retry challenges or the Retry-After header.
|
||||
// https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82
|
||||
ra = 5
|
||||
retryAfter = 5 * time.Second
|
||||
}
|
||||
initialInterval := time.Duration(ra) * time.Second
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
bo := backoff.NewExponentialBackOff()
|
||||
bo.InitialInterval = initialInterval
|
||||
bo.MaxInterval = 10 * initialInterval
|
||||
bo.MaxElapsedTime = 100 * initialInterval
|
||||
bo.InitialInterval = retryAfter
|
||||
bo.MaxInterval = 10 * retryAfter
|
||||
|
||||
// After the path is sent, the ACME server will access our server.
|
||||
// Repeatedly check the server for an updated status on our request.
|
||||
|
|
@ -124,10 +126,12 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
return errors.New("the server didn't respond to our request")
|
||||
return fmt.Errorf("the server didn't respond to our request (status=%s)", authz.Status)
|
||||
}
|
||||
|
||||
return backoff.Retry(operation, bo)
|
||||
return wait.Retry(ctx, operation,
|
||||
backoff.WithBackOff(bo),
|
||||
backoff.WithMaxElapsedTime(100*retryAfter))
|
||||
}
|
||||
|
||||
func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
|
||||
|
|
@ -137,9 +141,9 @@ func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
|
|||
case acme.StatusPending, acme.StatusProcessing:
|
||||
return false, nil
|
||||
case acme.StatusInvalid:
|
||||
return false, chlng.Error
|
||||
return false, fmt.Errorf("invalid challenge: %w", chlng.Err())
|
||||
default:
|
||||
return false, errors.New("the server returned an unexpected state")
|
||||
return false, fmt.Errorf("the server returned an unexpected challenge status: %s", chlng.Status)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,11 +158,12 @@ func checkAuthorizationStatus(authz acme.Authorization) (bool, error) {
|
|||
case acme.StatusInvalid:
|
||||
for _, chlg := range authz.Challenges {
|
||||
if chlg.Status == acme.StatusInvalid && chlg.Error != nil {
|
||||
return false, chlg.Error
|
||||
return false, fmt.Errorf("invalid authorization: %w", chlg.Err())
|
||||
}
|
||||
}
|
||||
return false, fmt.Errorf("the authorization state %s", authz.Status)
|
||||
|
||||
return false, errors.New("invalid authorization")
|
||||
default:
|
||||
return false, errors.New("the server returned an unexpected state")
|
||||
return false, fmt.Errorf("the server returned an unexpected authorization status: %s", authz.Status)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/acme/api"
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/go-acme/lego/v4/platform/tester/servermock"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -32,70 +33,50 @@ func TestByType(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
mux, apiURL := tester.SetupFakeAPI(t)
|
||||
|
||||
var statuses []string
|
||||
|
||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 1024)
|
||||
|
||||
mux.HandleFunc("/chlg", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
server := tester.MockACMEServer().
|
||||
Route("POST /chlg",
|
||||
http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if err := validateNoBody(privateKey, req); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateNoBody(privateKey, r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Link",
|
||||
fmt.Sprintf(`<https://%s/my-authz>; rel="up"`, req.Context().Value(http.LocalAddrContextKey)))
|
||||
|
||||
w.Header().Set("Link", "<"+apiURL+`/my-authz>; rel="up"`)
|
||||
st := statuses[0]
|
||||
statuses = statuses[1:]
|
||||
|
||||
st := statuses[0]
|
||||
statuses = statuses[1:]
|
||||
chlg := &acme.Challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}
|
||||
|
||||
chlg := &acme.Challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}
|
||||
if st == acme.StatusInvalid {
|
||||
chlg.Error = &acme.ProblemDetails{}
|
||||
}
|
||||
servermock.JSONEncode(chlg).ServeHTTP(rw, req)
|
||||
})).
|
||||
Route("POST /my-authz",
|
||||
http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
st := statuses[0]
|
||||
statuses = statuses[1:]
|
||||
|
||||
err := tester.WriteJSONResponse(w, chlg)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
authorization := acme.Authorization{
|
||||
Status: st,
|
||||
Challenges: []acme.Challenge{},
|
||||
}
|
||||
|
||||
mux.HandleFunc("/my-authz", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if st == acme.StatusInvalid {
|
||||
chlg := acme.Challenge{
|
||||
Status: acme.StatusInvalid,
|
||||
}
|
||||
authorization.Challenges = append(authorization.Challenges, chlg)
|
||||
}
|
||||
|
||||
st := statuses[0]
|
||||
statuses = statuses[1:]
|
||||
servermock.JSONEncode(authorization).ServeHTTP(rw, req)
|
||||
})).
|
||||
BuildHTTPS(t)
|
||||
|
||||
authorization := acme.Authorization{
|
||||
Status: st,
|
||||
Challenges: []acme.Challenge{},
|
||||
}
|
||||
|
||||
if st == acme.StatusInvalid {
|
||||
chlg := acme.Challenge{
|
||||
Status: acme.StatusInvalid,
|
||||
Error: &acme.ProblemDetails{},
|
||||
}
|
||||
authorization.Challenges = append(authorization.Challenges, chlg)
|
||||
}
|
||||
|
||||
err := tester.WriteJSONResponse(w, authorization)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
|
|
@ -106,7 +87,7 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
name: "POST-unexpected",
|
||||
statuses: []string{"weird"},
|
||||
want: "unexpected",
|
||||
want: "the server returned an unexpected challenge status: weird",
|
||||
},
|
||||
{
|
||||
name: "POST-valid",
|
||||
|
|
@ -115,12 +96,12 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
name: "POST-invalid",
|
||||
statuses: []string{acme.StatusInvalid},
|
||||
want: "error",
|
||||
want: "invalid challenge:",
|
||||
},
|
||||
{
|
||||
name: "POST-pending-unexpected",
|
||||
statuses: []string{acme.StatusPending, "weird"},
|
||||
want: "unexpected",
|
||||
want: "the server returned an unexpected authorization status: weird",
|
||||
},
|
||||
{
|
||||
name: "POST-pending-valid",
|
||||
|
|
@ -129,7 +110,7 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
name: "POST-pending-invalid",
|
||||
statuses: []string{acme.StatusPending, acme.StatusInvalid},
|
||||
want: "error",
|
||||
want: "invalid authorization",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +118,7 @@ func TestValidate(t *testing.T) {
|
|||
t.Run(test.name, func(t *testing.T) {
|
||||
statuses = test.statuses
|
||||
|
||||
err := validate(core, "example.com", acme.Challenge{Type: "http-01", Token: "token", URL: apiURL + "/chlg"})
|
||||
err := validate(core, "example.com", acme.Challenge{Type: "http-01", Token: "token", URL: server.URL + "/chlg"})
|
||||
if test.want == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
|
|
@ -148,6 +129,126 @@ func TestValidate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_checkChallengeStatus(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
challenge acme.Challenge
|
||||
requireErr require.ErrorAssertionFunc
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "status valid",
|
||||
challenge: acme.Challenge{Status: acme.StatusValid},
|
||||
requireErr: require.NoError,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "status invalid",
|
||||
challenge: acme.Challenge{Status: acme.StatusInvalid},
|
||||
requireErr: require.Error,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status invalid with error",
|
||||
challenge: acme.Challenge{Status: acme.StatusInvalid, Error: &acme.ProblemDetails{}},
|
||||
requireErr: require.Error,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status pending",
|
||||
challenge: acme.Challenge{Status: acme.StatusPending},
|
||||
requireErr: require.NoError,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status processing",
|
||||
challenge: acme.Challenge{Status: acme.StatusProcessing},
|
||||
requireErr: require.NoError,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
status, err := checkChallengeStatus(acme.ExtendedChallenge{Challenge: test.challenge})
|
||||
test.requireErr(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_checkAuthorizationStatus(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
authorization acme.Authorization
|
||||
requireErr require.ErrorAssertionFunc
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "status valid",
|
||||
authorization: acme.Authorization{Status: acme.StatusValid},
|
||||
requireErr: require.NoError,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "status invalid",
|
||||
authorization: acme.Authorization{Status: acme.StatusInvalid},
|
||||
requireErr: require.Error,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status invalid with error",
|
||||
authorization: acme.Authorization{Status: acme.StatusInvalid, Challenges: []acme.Challenge{{Error: &acme.ProblemDetails{}}}},
|
||||
requireErr: require.Error,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status pending",
|
||||
authorization: acme.Authorization{Status: acme.StatusPending},
|
||||
requireErr: require.NoError,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status processing",
|
||||
authorization: acme.Authorization{Status: acme.StatusProcessing},
|
||||
requireErr: require.NoError,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status deactivated",
|
||||
authorization: acme.Authorization{Status: acme.StatusDeactivated},
|
||||
requireErr: require.Error,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status expired",
|
||||
authorization: acme.Authorization{Status: acme.StatusExpired},
|
||||
requireErr: require.Error,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "status revoked",
|
||||
authorization: acme.Authorization{Status: acme.StatusRevoked},
|
||||
requireErr: require.Error,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
status, err := checkAuthorizationStatus(test.authorization)
|
||||
test.requireErr(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// validateNoBody reads the http.Request POST body, parses the JWS and validates it to read the body.
|
||||
// If there is an error doing this,
|
||||
// or if the JWS body is not the empty JSON payload "{}" or a POST-as-GET payload "" an error is returned.
|
||||
|
|
@ -159,6 +260,7 @@ func validateNoBody(privateKey *rsa.PrivateKey, r *http.Request) error {
|
|||
}
|
||||
|
||||
sigAlgs := []jose.SignatureAlgorithm{jose.RS256}
|
||||
|
||||
jws, err := jose.ParseSigned(string(reqBody), sigAlgs)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -175,5 +277,6 @@ func validateNoBody(privateKey *rsa.PrivateKey, r *http.Request) error {
|
|||
if bodyStr := string(body); bodyStr != "{}" && bodyStr != "" {
|
||||
return fmt.Errorf(`expected JWS POST body "{}" or "", got %q`, bodyStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/acme/api"
|
||||
|
|
@ -21,18 +22,38 @@ var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
|
|||
|
||||
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
|
||||
|
||||
type ChallengeOption func(*Challenge) error
|
||||
|
||||
// SetDelay sets a delay between the start of the TLS listener and the challenge validation.
|
||||
func SetDelay(delay time.Duration) ChallengeOption {
|
||||
return func(chlg *Challenge) error {
|
||||
chlg.delay = delay
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type Challenge struct {
|
||||
core *api.Core
|
||||
validate ValidateFunc
|
||||
provider challenge.Provider
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
|
||||
return &Challenge{
|
||||
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {
|
||||
chlg := &Challenge{
|
||||
core: core,
|
||||
validate: validate,
|
||||
provider: provider,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
err := opt(chlg)
|
||||
if err != nil {
|
||||
log.Infof("challenge option error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return chlg
|
||||
}
|
||||
|
||||
func (c *Challenge) SetProvider(provider challenge.Provider) {
|
||||
|
|
@ -59,6 +80,7 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("[%s] acme: error presenting token: %w", challenge.GetTargetedDomain(authz), err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := c.provider.CleanUp(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
|
|
@ -66,7 +88,12 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
|
|||
}
|
||||
}()
|
||||
|
||||
if c.delay > 0 {
|
||||
time.Sleep(c.delay)
|
||||
}
|
||||
|
||||
chlng.KeyAuthorization = keyAuth
|
||||
|
||||
return c.validate(c.core, domain, chlng)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"crypto/tls"
|
||||
"encoding/asn1"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
|
|
@ -21,7 +20,7 @@ import (
|
|||
)
|
||||
|
||||
func TestChallenge(t *testing.T) {
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
domain := "localhost"
|
||||
port := "24457"
|
||||
|
|
@ -43,6 +42,7 @@ func TestChallenge(t *testing.T) {
|
|||
assert.NotEmpty(t, remoteCert.Extensions, "Expected the challenge certificate to contain extensions")
|
||||
|
||||
idx := -1
|
||||
|
||||
for i, ext := range remoteCert.Extensions {
|
||||
if idPeAcmeIdentifierV1.Equal(ext.Id) {
|
||||
idx = i
|
||||
|
|
@ -66,10 +66,10 @@ func TestChallenge(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
solver := NewChallenge(
|
||||
|
|
@ -93,12 +93,12 @@ func TestChallenge(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChallengeInvalidPort(t *testing.T) {
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 128)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
solver := NewChallenge(
|
||||
|
|
@ -123,7 +123,7 @@ func TestChallengeInvalidPort(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChallengeIPaddress(t *testing.T) {
|
||||
_, apiURL := tester.SetupFakeAPI(t)
|
||||
server := tester.MockACMEServer().BuildHTTPS(t)
|
||||
|
||||
domain := "127.0.0.1"
|
||||
port := "24457"
|
||||
|
|
@ -146,31 +146,37 @@ func TestChallengeIPaddress(t *testing.T) {
|
|||
assert.True(t, net.ParseIP("127.0.0.1").Equal(remoteCert.IPAddresses[0]), "challenge certificate IPAddress ")
|
||||
assert.NotEmpty(t, remoteCert.Extensions, "Expected the challenge certificate to contain extensions")
|
||||
|
||||
var foundAcmeIdentifier bool
|
||||
var extValue []byte
|
||||
var (
|
||||
foundAcmeIdentifier bool
|
||||
extValue []byte
|
||||
)
|
||||
|
||||
for _, ext := range remoteCert.Extensions {
|
||||
if idPeAcmeIdentifierV1.Equal(ext.Id) {
|
||||
assert.True(t, ext.Critical, "Expected the challenge certificate id-pe-acmeIdentifier extension to be marked as critical")
|
||||
|
||||
foundAcmeIdentifier = true
|
||||
extValue = ext.Value
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.True(t, foundAcmeIdentifier, "Expected the challenge certificate to contain an extension with the id-pe-acmeIdentifier id,")
|
||||
|
||||
zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))
|
||||
value, err := asn1.Marshal(zBytes[:sha256.Size])
|
||||
require.NoError(t, err, "Expected marshaling of the keyAuth to return no error")
|
||||
|
||||
require.EqualValues(t, value, extValue, "Expected the challenge certificate id-pe-acmeIdentifier extension to contain the SHA-256 digest of the keyAuth")
|
||||
require.Equal(t, value, extValue, "Expected the challenge certificate id-pe-acmeIdentifier extension to contain the SHA-256 digest of the keyAuth")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
|
||||
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
solver := NewChallenge(
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ package cmd
|
|||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -18,6 +16,8 @@ import (
|
|||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const userIDPlaceholder = "noemail@example.com"
|
||||
|
||||
const (
|
||||
baseAccountsRootFolderName = "accounts"
|
||||
baseKeysFolderName = "keys"
|
||||
|
|
@ -34,7 +34,7 @@ const (
|
|||
//
|
||||
// rootUserPath:
|
||||
//
|
||||
// ./.lego/accounts/localhost_14000/hubert@hubert.com/
|
||||
// ./.lego/accounts/localhost_14000/foo@example.com/
|
||||
// │ │ │ └── userID ("email" option)
|
||||
// │ │ └── CA server ("server" option)
|
||||
// │ └── root accounts directory
|
||||
|
|
@ -42,7 +42,7 @@ const (
|
|||
//
|
||||
// keysPath:
|
||||
//
|
||||
// ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/
|
||||
// ./.lego/accounts/localhost_14000/foo@example.com/keys/
|
||||
// │ │ │ │ └── root keys directory
|
||||
// │ │ │ └── userID ("email" option)
|
||||
// │ │ └── CA server ("server" option)
|
||||
|
|
@ -51,7 +51,7 @@ const (
|
|||
//
|
||||
// accountFilePath:
|
||||
//
|
||||
// ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json
|
||||
// ./.lego/accounts/localhost_14000/foo@example.com/account.json
|
||||
// │ │ │ │ └── account file
|
||||
// │ │ │ └── userID ("email" option)
|
||||
// │ │ └── CA server ("server" option)
|
||||
|
|
@ -59,6 +59,7 @@ const (
|
|||
// └── "path" option
|
||||
type AccountsStorage struct {
|
||||
userID string
|
||||
email string
|
||||
rootPath string
|
||||
rootUserPath string
|
||||
keysPath string
|
||||
|
|
@ -68,8 +69,13 @@ type AccountsStorage struct {
|
|||
|
||||
// NewAccountsStorage Creates a new AccountsStorage.
|
||||
func NewAccountsStorage(ctx *cli.Context) *AccountsStorage {
|
||||
// TODO: move to account struct? Currently MUST pass email.
|
||||
email := getEmail(ctx)
|
||||
// TODO: move to account struct?
|
||||
email := ctx.String(flgEmail)
|
||||
|
||||
userID := email
|
||||
if userID == "" {
|
||||
userID = userIDPlaceholder
|
||||
}
|
||||
|
||||
serverURL, err := url.Parse(ctx.String(flgServer))
|
||||
if err != nil {
|
||||
|
|
@ -79,10 +85,11 @@ func NewAccountsStorage(ctx *cli.Context) *AccountsStorage {
|
|||
rootPath := filepath.Join(ctx.String(flgPath), baseAccountsRootFolderName)
|
||||
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
|
||||
accountsPath := filepath.Join(rootPath, serverPath)
|
||||
rootUserPath := filepath.Join(accountsPath, email)
|
||||
rootUserPath := filepath.Join(accountsPath, userID)
|
||||
|
||||
return &AccountsStorage{
|
||||
userID: email,
|
||||
userID: userID,
|
||||
email: email,
|
||||
rootPath: rootPath,
|
||||
rootUserPath: rootUserPath,
|
||||
keysPath: filepath.Join(rootUserPath, baseKeysFolderName),
|
||||
|
|
@ -98,6 +105,7 @@ func (s *AccountsStorage) ExistsAccountFilePath() bool {
|
|||
} else if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +121,10 @@ func (s *AccountsStorage) GetUserID() string {
|
|||
return s.userID
|
||||
}
|
||||
|
||||
func (s *AccountsStorage) GetEmail() string {
|
||||
return s.email
|
||||
}
|
||||
|
||||
func (s *AccountsStorage) Save(account *Account) error {
|
||||
jsonBytes, err := json.MarshalIndent(account, "", "\t")
|
||||
if err != nil {
|
||||
|
|
@ -125,13 +137,14 @@ func (s *AccountsStorage) Save(account *Account) error {
|
|||
func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account {
|
||||
fileBytes, err := os.ReadFile(s.accountFilePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not load file for account %s: %v", s.userID, err)
|
||||
log.Fatalf("Could not load file for account %s: %v", s.GetUserID(), err)
|
||||
}
|
||||
|
||||
var account Account
|
||||
|
||||
err = json.Unmarshal(fileBytes, &account)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse file for account %s: %v", s.userID, err)
|
||||
log.Fatalf("Could not parse file for account %s: %v", s.GetUserID(), err)
|
||||
}
|
||||
|
||||
account.key = privateKey
|
||||
|
|
@ -139,13 +152,14 @@ func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account {
|
|||
if account.Registration == nil || account.Registration.Body.Status == "" {
|
||||
reg, err := tryRecoverRegistration(s.ctx, privateKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not load account for %s. Registration is nil: %#v", s.userID, err)
|
||||
log.Fatalf("Could not load account for %s. Registration is nil: %#v", s.GetUserID(), err)
|
||||
}
|
||||
|
||||
account.Registration = reg
|
||||
|
||||
err = s.Save(&account)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.userID, err)
|
||||
log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.GetUserID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,18 +167,19 @@ func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account {
|
|||
}
|
||||
|
||||
func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey {
|
||||
accKeyPath := filepath.Join(s.keysPath, s.userID+".key")
|
||||
accKeyPath := filepath.Join(s.keysPath, s.GetUserID()+".key")
|
||||
|
||||
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
|
||||
log.Printf("No key found for account %s. Generating a %s key.", s.userID, keyType)
|
||||
log.Printf("No key found for account %s. Generating a %s key.", s.GetUserID(), keyType)
|
||||
s.createKeysFolder()
|
||||
|
||||
privateKey, err := generatePrivateKey(accKeyPath, keyType)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not generate RSA private account key for account %s: %v", s.userID, err)
|
||||
log.Fatalf("Could not generate RSA private account key for account %s: %v", s.GetUserID(), err)
|
||||
}
|
||||
|
||||
log.Printf("Saved key to %s", accKeyPath)
|
||||
|
||||
return privateKey
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +193,7 @@ func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.Priva
|
|||
|
||||
func (s *AccountsStorage) createKeysFolder() {
|
||||
if err := createNonExistingFolder(s.keysPath); err != nil {
|
||||
log.Fatalf("Could not check/create directory for account %s: %v", s.userID, err)
|
||||
log.Fatalf("Could not check/create directory for account %s: %v", s.GetUserID(), err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,6 +210,7 @@ func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.Private
|
|||
defer certOut.Close()
|
||||
|
||||
pemKey := certcrypto.PEMBlock(privateKey)
|
||||
|
||||
err = pem.Encode(certOut, pemKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -209,16 +225,12 @@ func loadPrivateKey(file string) (crypto.PrivateKey, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
keyBlock, _ := pem.Decode(keyBytes)
|
||||
|
||||
switch keyBlock.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||
privateKey, err := certcrypto.ParsePEMPrivateKey(keyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown private key type")
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) {
|
||||
|
|
@ -236,5 +248,6 @@ func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*re
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package cmd
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
|
|
@ -159,6 +158,7 @@ func (s *CertificatesStorage) ExistsFile(domain, extension string) bool {
|
|||
} else if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -233,27 +233,9 @@ func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.R
|
|||
return fmt.Errorf("unable to get certificate chain for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
keyPemBlock, _ := pem.Decode(certRes.PrivateKey)
|
||||
if keyPemBlock == nil {
|
||||
return fmt.Errorf("unable to parse PrivateKey for domain %s", domain)
|
||||
}
|
||||
|
||||
var privateKey crypto.Signer
|
||||
var keyErr error
|
||||
|
||||
switch keyPemBlock.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
privateKey, keyErr = x509.ParsePKCS1PrivateKey(keyPemBlock.Bytes)
|
||||
if keyErr != nil {
|
||||
return fmt.Errorf("unable to load RSA PrivateKey for domain %s: %w", domain, keyErr)
|
||||
}
|
||||
case "EC PRIVATE KEY":
|
||||
privateKey, keyErr = x509.ParseECPrivateKey(keyPemBlock.Bytes)
|
||||
if keyErr != nil {
|
||||
return fmt.Errorf("unable to load EC PrivateKey for domain %s: %w", domain, keyErr)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported PrivateKey type '%s' for domain %s", keyPemBlock.Type, domain)
|
||||
privateKey, err := certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse PrivateKey for domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
encoder, err := getPFXEncoder(s.pfxFormat)
|
||||
|
|
@ -302,6 +284,7 @@ func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, er
|
|||
}
|
||||
|
||||
var certChain []*x509.Certificate
|
||||
|
||||
for chainCertPemBlock != nil {
|
||||
chainCert, err := x509.ParseCertificate(chainCertPemBlock.Bytes)
|
||||
if err != nil {
|
||||
|
|
@ -317,6 +300,7 @@ func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, er
|
|||
|
||||
func getPFXEncoder(pfxFormat string) (*pkcs12.Encoder, error) {
|
||||
var encoder *pkcs12.Encoder
|
||||
|
||||
switch pfxFormat {
|
||||
case "SHA256":
|
||||
encoder = pkcs12.Modern2023
|
||||
|
|
@ -337,5 +321,6 @@ func sanitizedDomain(domain string) string {
|
|||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return safe
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ type errWriter struct {
|
|||
err error
|
||||
}
|
||||
|
||||
func (ew *errWriter) writeln(a ...interface{}) {
|
||||
func (ew *errWriter) writeln(a ...any) {
|
||||
if ew.err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ func (ew *errWriter) writeln(a ...interface{}) {
|
|||
_, ew.err = fmt.Fprintln(ew.w, a...)
|
||||
}
|
||||
|
||||
func (ew *errWriter) writef(format string, a ...interface{}) {
|
||||
func (ew *errWriter) writef(format string, a ...any) {
|
||||
if ew.err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cmd
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -36,7 +37,7 @@ func createList() *cli.Command {
|
|||
// fake email, needed by NewAccountsStorage
|
||||
&cli.StringFlag{
|
||||
Name: flgEmail,
|
||||
Value: "unknown",
|
||||
Value: "",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -67,6 +68,7 @@ func listCertificates(ctx *cli.Context) error {
|
|||
if !names {
|
||||
fmt.Println("No certificates found.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +101,11 @@ func listCertificates(ctx *cli.Context) error {
|
|||
} else {
|
||||
fmt.Println(" Certificate Name:", name)
|
||||
fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", "))
|
||||
|
||||
if len(pCert.IPAddresses) > 0 {
|
||||
fmt.Println(" IPs:", formatIPAddresses(pCert.IPAddresses))
|
||||
}
|
||||
|
||||
fmt.Println(" Expiry Date:", pCert.NotAfter)
|
||||
fmt.Println(" Certificate Path:", filename)
|
||||
fmt.Println()
|
||||
|
|
@ -122,6 +129,7 @@ func listAccount(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
fmt.Println("Found the following accounts:")
|
||||
|
||||
for _, filename := range matches {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
|
|
@ -129,6 +137,7 @@ func listAccount(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
var account Account
|
||||
|
||||
err = json.Unmarshal(data, &account)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -147,3 +156,12 @@ func listAccount(ctx *cli.Context) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatIPAddresses(ipAddresses []net.IP) string {
|
||||
var ips []string
|
||||
for _, ip := range ipAddresses {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
|
||||
return strings.Join(ips, ", ")
|
||||
}
|
||||
|
|
|
|||
187
cmd/cmd_renew.go
187
cmd/cmd_renew.go
|
|
@ -20,22 +20,15 @@ import (
|
|||
|
||||
// Flag names.
|
||||
const (
|
||||
flgDays = "days"
|
||||
flgRenewDays = "days"
|
||||
flgRenewDynamic = "dynamic"
|
||||
flgARIDisable = "ari-disable"
|
||||
flgARIWaitToRenewDuration = "ari-wait-to-renew-duration"
|
||||
flgReuseKey = "reuse-key"
|
||||
flgRenewHook = "renew-hook"
|
||||
flgRenewHookTimeout = "renew-hook-timeout"
|
||||
flgNoRandomSleep = "no-random-sleep"
|
||||
)
|
||||
|
||||
const (
|
||||
renewEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
|
||||
renewEnvCertDomain = "LEGO_CERT_DOMAIN"
|
||||
renewEnvCertPath = "LEGO_CERT_PATH"
|
||||
renewEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
|
||||
renewEnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH"
|
||||
renewEnvCertPEMPath = "LEGO_CERT_PEM_PATH"
|
||||
renewEnvCertPFXPath = "LEGO_CERT_PFX_PATH"
|
||||
flgForceCertDomains = "force-cert-domains"
|
||||
)
|
||||
|
||||
func createRenew() *cli.Command {
|
||||
|
|
@ -46,24 +39,37 @@ func createRenew() *cli.Command {
|
|||
Before: func(ctx *cli.Context) error {
|
||||
// we require either domains or csr, but not both
|
||||
hasDomains := len(ctx.StringSlice(flgDomains)) > 0
|
||||
|
||||
hasCsr := ctx.String(flgCSR) != ""
|
||||
if hasDomains && hasCsr {
|
||||
log.Fatal("Please specify either --%s/-d or --%s/-c, but not both", flgDomains, flgCSR)
|
||||
log.Fatalf("Please specify either --%s/-d or --%s/-c, but not both", flgDomains, flgCSR)
|
||||
}
|
||||
|
||||
if !hasDomains && !hasCsr {
|
||||
log.Fatal("Please specify --%s/-d (or --%s/-c if you already have a CSR)", flgDomains, flgCSR)
|
||||
log.Fatalf("Please specify --%s/-d (or --%s/-c if you already have a CSR)", flgDomains, flgCSR)
|
||||
}
|
||||
|
||||
if ctx.Bool(flgForceCertDomains) && hasCsr {
|
||||
log.Fatalf("--%s only works with --%s/-d, --%s/-c doesn't support this option.", flgForceCertDomains, flgDomains, flgCSR)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: flgDays,
|
||||
Name: flgRenewDays,
|
||||
Value: 30,
|
||||
Usage: "The number of days left on a certificate to renew it.",
|
||||
},
|
||||
// TODO(ldez): in v5, remove this flag, use this behavior as default.
|
||||
&cli.BoolFlag{
|
||||
Name: flgRenewDynamic,
|
||||
Value: false,
|
||||
Usage: "Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgARIDisable,
|
||||
Usage: "Do not use the renewalInfo endpoint (draft-ietf-acme-ari) to check if a certificate should be renewed.",
|
||||
Usage: "Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgARIWaitToRenewDuration,
|
||||
|
|
@ -97,6 +103,10 @@ func createRenew() *cli.Command {
|
|||
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
|
||||
" If no match, the default offered chain will be used.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgProfile,
|
||||
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgAlwaysDeactivateAuthorizations,
|
||||
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
|
||||
|
|
@ -105,18 +115,26 @@ func createRenew() *cli.Command {
|
|||
Name: flgRenewHook,
|
||||
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgRenewHookTimeout,
|
||||
Usage: "Define the timeout for the hook execution.",
|
||||
Value: 2 * time.Minute,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgNoRandomSleep,
|
||||
Usage: "Do not add a random sleep before the renewal." +
|
||||
" We do not recommend using this flag if you are doing your renewals in an automated way.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgForceCertDomains,
|
||||
Usage: "Check and ensure that the cert's domain list matches those passed in the domains argument.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func renew(ctx *cli.Context) error {
|
||||
account, client := setup(ctx, NewAccountsStorage(ctx))
|
||||
setupChallenges(ctx, client)
|
||||
account, keyType := setupAccount(ctx, NewAccountsStorage(ctx))
|
||||
|
||||
if account.Registration == nil {
|
||||
log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email)
|
||||
|
|
@ -126,18 +144,20 @@ func renew(ctx *cli.Context) error {
|
|||
|
||||
bundle := !ctx.Bool(flgNoBundle)
|
||||
|
||||
meta := map[string]string{renewEnvAccountEmail: account.Email}
|
||||
meta := map[string]string{
|
||||
hookEnvAccountEmail: account.Email,
|
||||
}
|
||||
|
||||
// CSR
|
||||
if ctx.IsSet(flgCSR) {
|
||||
return renewForCSR(ctx, client, certsStorage, bundle, meta)
|
||||
return renewForCSR(ctx, account, keyType, certsStorage, bundle, meta)
|
||||
}
|
||||
|
||||
// Domains
|
||||
return renewForDomains(ctx, client, certsStorage, bundle, meta)
|
||||
return renewForDomains(ctx, account, keyType, certsStorage, bundle, meta)
|
||||
}
|
||||
|
||||
func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
|
||||
func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
|
||||
domains := ctx.StringSlice(flgDomains)
|
||||
domain := domains[0]
|
||||
|
||||
|
|
@ -151,10 +171,16 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
|||
|
||||
cert := certificates[0]
|
||||
|
||||
var ariRenewalTime *time.Time
|
||||
var replacesCertID string
|
||||
var (
|
||||
ariRenewalTime *time.Time
|
||||
replacesCertID string
|
||||
)
|
||||
|
||||
var client *lego.Client
|
||||
|
||||
if !ctx.Bool(flgARIDisable) {
|
||||
client = setupClient(ctx, account, keyType)
|
||||
|
||||
ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)
|
||||
if ariRenewalTime != nil {
|
||||
now := time.Now().UTC()
|
||||
|
|
@ -172,17 +198,25 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
|||
}
|
||||
}
|
||||
|
||||
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) {
|
||||
forceDomains := ctx.Bool(flgForceCertDomains)
|
||||
|
||||
certDomains := certcrypto.ExtractDomains(cert)
|
||||
|
||||
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) &&
|
||||
(!forceDomains || slices.Equal(certDomains, domains)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if client == nil {
|
||||
client = setupClient(ctx, account, keyType)
|
||||
}
|
||||
|
||||
// This is just meant to be informal for the user.
|
||||
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
||||
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
|
||||
|
||||
certDomains := certcrypto.ExtractDomains(cert)
|
||||
|
||||
var privateKey crypto.PrivateKey
|
||||
|
||||
if ctx.Bool(flgReuseKey) {
|
||||
keyBytes, errR := certsStorage.ReadFile(domain, keyExt)
|
||||
if errR != nil {
|
||||
|
|
@ -200,6 +234,7 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
|||
if !isatty.IsTerminal(os.Stdout.Fd()) && !ctx.Bool(flgNoRandomSleep) {
|
||||
// https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L472
|
||||
const jitter = 8 * time.Minute
|
||||
|
||||
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
sleepTime := time.Duration(rnd.Int63n(int64(jitter)))
|
||||
|
||||
|
|
@ -207,14 +242,20 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
|||
time.Sleep(sleepTime)
|
||||
}
|
||||
|
||||
renewalDomains := slices.Clone(domains)
|
||||
if !forceDomains {
|
||||
renewalDomains = merge(certDomains, domains)
|
||||
}
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: merge(certDomains, domains),
|
||||
Domains: renewalDomains,
|
||||
PrivateKey: privateKey,
|
||||
MustStaple: ctx.Bool(flgMustStaple),
|
||||
NotBefore: getTime(ctx, flgNotBefore),
|
||||
NotAfter: getTime(ctx, flgNotAfter),
|
||||
Bundle: bundle,
|
||||
PreferredChain: ctx.String(flgPreferredChain),
|
||||
Profile: ctx.String(flgProfile),
|
||||
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
|
||||
}
|
||||
|
||||
|
|
@ -227,14 +268,16 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
certRes.Domain = domain
|
||||
|
||||
certsStorage.SaveResource(certRes)
|
||||
|
||||
addPathToMetadata(meta, domain, certRes, certsStorage)
|
||||
|
||||
return launchHook(ctx.String(flgRenewHook), meta)
|
||||
return launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta)
|
||||
}
|
||||
|
||||
func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
|
||||
func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
|
||||
csr, err := readCSRFile(ctx.String(flgCSR))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
@ -255,10 +298,16 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
|
|||
|
||||
cert := certificates[0]
|
||||
|
||||
var ariRenewalTime *time.Time
|
||||
var replacesCertID string
|
||||
var (
|
||||
ariRenewalTime *time.Time
|
||||
replacesCertID string
|
||||
)
|
||||
|
||||
var client *lego.Client
|
||||
|
||||
if !ctx.Bool(flgARIDisable) {
|
||||
client = setupClient(ctx, account, keyType)
|
||||
|
||||
ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)
|
||||
if ariRenewalTime != nil {
|
||||
now := time.Now().UTC()
|
||||
|
|
@ -276,10 +325,14 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
|
|||
}
|
||||
}
|
||||
|
||||
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) {
|
||||
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if client == nil {
|
||||
client = setupClient(ctx, account, keyType)
|
||||
}
|
||||
|
||||
// This is just meant to be informal for the user.
|
||||
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
||||
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
|
||||
|
|
@ -290,6 +343,7 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
|
|||
NotAfter: getTime(ctx, flgNotAfter),
|
||||
Bundle: bundle,
|
||||
PreferredChain: ctx.String(flgPreferredChain),
|
||||
Profile: ctx.String(flgProfile),
|
||||
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
|
||||
}
|
||||
|
||||
|
|
@ -306,24 +360,51 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat
|
|||
|
||||
addPathToMetadata(meta, domain, certRes, certsStorage)
|
||||
|
||||
return launchHook(ctx.String(flgRenewHook), meta)
|
||||
return launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta)
|
||||
}
|
||||
|
||||
func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool {
|
||||
func needRenewal(x509Cert *x509.Certificate, domain string, days int, dynamic bool) bool {
|
||||
if x509Cert.IsCA {
|
||||
log.Fatalf("[%s] Certificate bundle starts with a CA certificate", domain)
|
||||
}
|
||||
|
||||
if days >= 0 {
|
||||
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
|
||||
if notAfter > days {
|
||||
log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.",
|
||||
domain, notAfter, days)
|
||||
return false
|
||||
}
|
||||
if dynamic {
|
||||
return needRenewalDynamic(x509Cert, domain, time.Now())
|
||||
}
|
||||
|
||||
return true
|
||||
if days < 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)
|
||||
if notAfter <= days {
|
||||
return true
|
||||
}
|
||||
|
||||
log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.",
|
||||
domain, notAfter, days)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func needRenewalDynamic(x509Cert *x509.Certificate, domain string, now time.Time) bool {
|
||||
lifetime := x509Cert.NotAfter.Sub(x509Cert.NotBefore)
|
||||
|
||||
var divisor int64 = 3
|
||||
if lifetime.Round(24*time.Hour).Hours()/24.0 <= 10 {
|
||||
divisor = 2
|
||||
}
|
||||
|
||||
dueDate := x509Cert.NotAfter.Add(-1 * time.Duration(lifetime.Nanoseconds()/divisor))
|
||||
|
||||
if dueDate.Before(now) {
|
||||
return true
|
||||
}
|
||||
|
||||
log.Infof("[%s] The certificate expires at %s, the renewal can be performed in %s: no renewal.",
|
||||
domain, x509Cert.NotAfter.Format(time.RFC3339), dueDate.Sub(now))
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint.
|
||||
|
|
@ -339,16 +420,20 @@ func getARIRenewalTime(ctx *cli.Context, cert *x509.Certificate, domain string,
|
|||
log.Warnf("[%s] acme: %v", domain, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warnf("[%s] acme: calling renewal info endpoint: %v", domain, err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
renewalTime := renewalInfo.ShouldRenewAt(now, ctx.Duration(flgARIWaitToRenewDuration))
|
||||
if renewalTime == nil {
|
||||
log.Infof("[%s] acme: renewalInfo endpoint indicates that renewal is not needed", domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("[%s] acme: renewalInfo endpoint indicates that renewal is needed", domain)
|
||||
|
||||
if renewalInfo.ExplanationURL != "" {
|
||||
|
|
@ -358,24 +443,6 @@ func getARIRenewalTime(ctx *cli.Context, cert *x509.Certificate, domain string,
|
|||
return renewalTime
|
||||
}
|
||||
|
||||
func addPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *CertificatesStorage) {
|
||||
meta[renewEnvCertDomain] = domain
|
||||
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, certExt)
|
||||
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, keyExt)
|
||||
|
||||
if certRes.IssuerCertificate != nil {
|
||||
meta[renewEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, issuerExt)
|
||||
}
|
||||
|
||||
if certsStorage.pem {
|
||||
meta[renewEnvCertPEMPath] = certsStorage.GetFileName(domain, pemExt)
|
||||
}
|
||||
|
||||
if certsStorage.pfx {
|
||||
meta[renewEnvCertPFXPath] = certsStorage.GetFileName(domain, pfxExt)
|
||||
}
|
||||
}
|
||||
|
||||
func merge(prevDomains, nextDomains []string) []string {
|
||||
for _, next := range nextDomains {
|
||||
if slices.Contains(prevDomains, next) {
|
||||
|
|
|
|||
|
|
@ -108,9 +108,62 @@ func Test_needRenewal(t *testing.T) {
|
|||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
actual := needRenewal(test.x509Cert, "foo.com", test.days)
|
||||
actual := needRenewal(test.x509Cert, "foo.com", test.days, false)
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_needRenewalDynamic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
now time.Time
|
||||
notBefore, notAfter time.Time
|
||||
expected assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "higher than 1/3 of the certificate lifetime left (lifetime > 10 days)",
|
||||
now: time.Date(2025, 1, 19, 1, 1, 1, 1, time.UTC),
|
||||
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
notAfter: time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC),
|
||||
expected: assert.False,
|
||||
},
|
||||
{
|
||||
desc: "lower than 1/3 of the certificate lifetime left(lifetime > 10 days)",
|
||||
now: time.Date(2025, 1, 21, 1, 1, 1, 1, time.UTC),
|
||||
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
notAfter: time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC),
|
||||
expected: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "higher than 1/2 of the certificate lifetime left (lifetime < 10 days)",
|
||||
now: time.Date(2025, 1, 4, 1, 1, 1, 1, time.UTC),
|
||||
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
notAfter: time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC),
|
||||
expected: assert.False,
|
||||
},
|
||||
{
|
||||
desc: "lower than 1/2 of the certificate lifetime left (lifetime < 10 days)",
|
||||
now: time.Date(2025, 1, 6, 1, 1, 1, 1, time.UTC),
|
||||
notBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
notAfter: time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC),
|
||||
expected: assert.True,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
x509Cert := &x509.Certificate{
|
||||
NotBefore: test.notBefore,
|
||||
NotAfter: test.notAfter,
|
||||
}
|
||||
|
||||
ok := needRenewalDynamic(x509Cert, "example.com", test.now)
|
||||
|
||||
test.expected(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,12 +38,14 @@ func createRevoke() *cli.Command {
|
|||
}
|
||||
|
||||
func revoke(ctx *cli.Context) error {
|
||||
acc, client := setup(ctx, NewAccountsStorage(ctx))
|
||||
account, keyType := setupAccount(ctx, NewAccountsStorage(ctx))
|
||||
|
||||
if acc.Registration == nil {
|
||||
log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
|
||||
if account.Registration == nil {
|
||||
log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email)
|
||||
}
|
||||
|
||||
client := newClient(ctx, account, keyType)
|
||||
|
||||
certsStorage := NewCertificatesStorage(ctx)
|
||||
certsStorage.CreateRootFolder()
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,12 @@ const (
|
|||
flgMustStaple = "must-staple"
|
||||
flgNotBefore = "not-before"
|
||||
flgNotAfter = "not-after"
|
||||
flgPrivateKey = "private-key"
|
||||
flgPreferredChain = "preferred-chain"
|
||||
flgProfile = "profile"
|
||||
flgAlwaysDeactivateAuthorizations = "always-deactivate-authorizations"
|
||||
flgRunHook = "run-hook"
|
||||
flgRunHookTimeout = "run-hook-timeout"
|
||||
)
|
||||
|
||||
func createRun() *cli.Command {
|
||||
|
|
@ -32,13 +35,16 @@ func createRun() *cli.Command {
|
|||
Before: func(ctx *cli.Context) error {
|
||||
// we require either domains or csr, but not both
|
||||
hasDomains := len(ctx.StringSlice(flgDomains)) > 0
|
||||
|
||||
hasCsr := ctx.String(flgCSR) != ""
|
||||
if hasDomains && hasCsr {
|
||||
log.Fatal("Please specify either --domains/-d or --csr/-c, but not both")
|
||||
}
|
||||
|
||||
if !hasDomains && !hasCsr {
|
||||
log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Action: run,
|
||||
|
|
@ -62,11 +68,19 @@ func createRun() *cli.Command {
|
|||
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
|
||||
Layout: time.RFC3339,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPrivateKey,
|
||||
Usage: "Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPreferredChain,
|
||||
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
|
||||
" If no match, the default offered chain will be used.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgProfile,
|
||||
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgAlwaysDeactivateAuthorizations,
|
||||
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
|
||||
|
|
@ -75,26 +89,32 @@ func createRun() *cli.Command {
|
|||
Name: flgRunHook,
|
||||
Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgRunHookTimeout,
|
||||
Usage: "Define the timeout for the hook execution.",
|
||||
Value: 2 * time.Minute,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const rootPathWarningMessage = `!!!! HEADS UP !!!!
|
||||
|
||||
Your account credentials have been saved in your Let's Encrypt
|
||||
Your account credentials have been saved in your
|
||||
configuration directory at "%s".
|
||||
|
||||
You should make a secure backup of this folder now. This
|
||||
configuration directory will also contain certificates and
|
||||
private keys obtained from Let's Encrypt so making regular
|
||||
backups of this folder is ideal.
|
||||
configuration directory will also contain private keys
|
||||
generated by lego and certificates obtained from the ACME
|
||||
server. Making regular backups of this folder is ideal.
|
||||
`
|
||||
|
||||
func run(ctx *cli.Context) error {
|
||||
accountsStorage := NewAccountsStorage(ctx)
|
||||
|
||||
account, client := setup(ctx, accountsStorage)
|
||||
setupChallenges(ctx, client)
|
||||
account, keyType := setupAccount(ctx, accountsStorage)
|
||||
|
||||
client := setupClient(ctx, account, keyType)
|
||||
|
||||
if account.Registration == nil {
|
||||
reg, err := register(ctx, client)
|
||||
|
|
@ -123,12 +143,12 @@ func run(ctx *cli.Context) error {
|
|||
certsStorage.SaveResource(cert)
|
||||
|
||||
meta := map[string]string{
|
||||
renewEnvAccountEmail: account.Email,
|
||||
hookEnvAccountEmail: account.Email,
|
||||
}
|
||||
|
||||
addPathToMetadata(meta, cert.Domain, cert, certsStorage)
|
||||
|
||||
return launchHook(ctx.String(flgRunHook), meta)
|
||||
return launchHook(ctx.String(flgRunHook), ctx.Duration(flgRunHookTimeout), meta)
|
||||
}
|
||||
|
||||
func handleTOS(ctx *cli.Context, client *lego.Client) bool {
|
||||
|
|
@ -138,10 +158,12 @@ func handleTOS(ctx *cli.Context, client *lego.Client) bool {
|
|||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
log.Printf("Please review the TOS at %s", client.GetToSURL())
|
||||
|
||||
for {
|
||||
fmt.Println("Do you accept the TOS? Y/n")
|
||||
|
||||
text, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Fatalf("Could not read from console: %v", err)
|
||||
|
|
@ -191,20 +213,22 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
|
|||
// obtain a certificate, generating a new private key
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
Bundle: bundle,
|
||||
MustStaple: ctx.Bool(flgMustStaple),
|
||||
NotBefore: getTime(ctx, flgNotBefore),
|
||||
NotAfter: getTime(ctx, flgNotAfter),
|
||||
Bundle: bundle,
|
||||
PreferredChain: ctx.String(flgPreferredChain),
|
||||
Profile: ctx.String(flgProfile),
|
||||
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
|
||||
}
|
||||
|
||||
notBefore := ctx.Timestamp(flgNotBefore)
|
||||
if notBefore != nil {
|
||||
request.NotBefore = *notBefore
|
||||
}
|
||||
if ctx.IsSet(flgPrivateKey) {
|
||||
var err error
|
||||
|
||||
notAfter := ctx.Timestamp(flgNotAfter)
|
||||
if notAfter != nil {
|
||||
request.NotAfter = *notAfter
|
||||
request.PrivateKey, err = loadPrivateKey(ctx.String(flgPrivateKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return client.Certificate.Obtain(request)
|
||||
|
|
@ -223,8 +247,18 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
|
|||
NotAfter: getTime(ctx, flgNotAfter),
|
||||
Bundle: bundle,
|
||||
PreferredChain: ctx.String(flgPreferredChain),
|
||||
Profile: ctx.String(flgProfile),
|
||||
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
|
||||
}
|
||||
|
||||
if ctx.IsSet(flgPrivateKey) {
|
||||
var err error
|
||||
|
||||
request.PrivateKey, err = loadPrivateKey(ctx.String(flgPrivateKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return client.Certificate.ObtainForCSR(request)
|
||||
}
|
||||
|
|
|
|||
47
cmd/flags.go
47
cmd/flags.go
|
|
@ -16,6 +16,7 @@ const (
|
|||
flgServer = "server"
|
||||
flgAcceptTOS = "accept-tos"
|
||||
flgEmail = "email"
|
||||
flgDisableCommonName = "disable-cn"
|
||||
flgCSR = "csr"
|
||||
flgEAB = "eab"
|
||||
flgKID = "kid"
|
||||
|
|
@ -25,12 +26,14 @@ const (
|
|||
flgPath = "path"
|
||||
flgHTTP = "http"
|
||||
flgHTTPPort = "http.port"
|
||||
flgHTTPDelay = "http.delay"
|
||||
flgHTTPProxyHeader = "http.proxy-header"
|
||||
flgHTTPWebroot = "http.webroot"
|
||||
flgHTTPMemcachedHost = "http.memcached-host"
|
||||
flgHTTPS3Bucket = "http.s3-bucket"
|
||||
flgTLS = "tls"
|
||||
flgTLSPort = "tls.port"
|
||||
flgTLSDelay = "tls.delay"
|
||||
flgDNS = "dns"
|
||||
flgDNSDisableCP = "dns.disable-cp"
|
||||
flgDNSPropagationWait = "dns.propagation-wait"
|
||||
|
|
@ -49,6 +52,18 @@ const (
|
|||
flgUserAgent = "user-agent"
|
||||
)
|
||||
|
||||
const (
|
||||
envEAB = "LEGO_EAB"
|
||||
envEABHMAC = "LEGO_EAB_HMAC"
|
||||
envEABKID = "LEGO_EAB_KID"
|
||||
envEmail = "LEGO_EMAIL"
|
||||
envPath = "LEGO_PATH"
|
||||
envPFX = "LEGO_PFX"
|
||||
envPFXFormat = "LEGO_PFX_FORMAT"
|
||||
envPFXPassword = "LEGO_PFX_PASSWORD"
|
||||
envServer = "LEGO_SERVER"
|
||||
)
|
||||
|
||||
func CreateFlags(defaultPath string) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
|
|
@ -59,7 +74,7 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
&cli.StringFlag{
|
||||
Name: flgServer,
|
||||
Aliases: []string{"s"},
|
||||
EnvVars: []string{"LEGO_SERVER"},
|
||||
EnvVars: []string{envServer},
|
||||
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
|
||||
Value: lego.LEDirectoryProduction,
|
||||
},
|
||||
|
|
@ -71,8 +86,13 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
&cli.StringFlag{
|
||||
Name: flgEmail,
|
||||
Aliases: []string{"m"},
|
||||
EnvVars: []string{envEmail},
|
||||
Usage: "Email used for registration and recovery contact.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgDisableCommonName,
|
||||
Usage: "Disable the use of the common name in the CSR.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgCSR,
|
||||
Aliases: []string{"c"},
|
||||
|
|
@ -80,17 +100,17 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flgEAB,
|
||||
EnvVars: []string{"LEGO_EAB"},
|
||||
EnvVars: []string{envEAB},
|
||||
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgKID,
|
||||
EnvVars: []string{"LEGO_EAB_KID"},
|
||||
EnvVars: []string{envEABKID},
|
||||
Usage: "Key identifier from External CA. Used for External Account Binding.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgHMAC,
|
||||
EnvVars: []string{"LEGO_EAB_HMAC"},
|
||||
EnvVars: []string{envEABHMAC},
|
||||
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
|
|
@ -105,7 +125,7 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPath,
|
||||
EnvVars: []string{"LEGO_PATH"},
|
||||
EnvVars: []string{envPath},
|
||||
Usage: "Directory to use for storing the data.",
|
||||
Value: defaultPath,
|
||||
},
|
||||
|
|
@ -118,6 +138,11 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
Usage: "Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port.",
|
||||
Value: ":80",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgHTTPDelay,
|
||||
Usage: "Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge.",
|
||||
Value: 0,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgHTTPProxyHeader,
|
||||
Usage: "Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy.",
|
||||
|
|
@ -145,6 +170,11 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
Usage: "Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port.",
|
||||
Value: ":443",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: flgTLSDelay,
|
||||
Usage: "Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge.",
|
||||
Value: 0,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgDNS,
|
||||
Usage: "Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.",
|
||||
|
|
@ -192,19 +222,19 @@ func CreateFlags(defaultPath string) []cli.Flag {
|
|||
&cli.BoolFlag{
|
||||
Name: flgPFX,
|
||||
Usage: "Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.",
|
||||
EnvVars: []string{"LEGO_PFX"},
|
||||
EnvVars: []string{envPFX},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPFXPass,
|
||||
Usage: "The password used to encrypt the .pfx (PCKS#12) file.",
|
||||
Value: pkcs12.DefaultPassword,
|
||||
EnvVars: []string{"LEGO_PFX_PASSWORD"},
|
||||
EnvVars: []string{envPFXPassword},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgPFXFormat,
|
||||
Usage: "The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256.",
|
||||
Value: "RC2",
|
||||
EnvVars: []string{"LEGO_PFX_FORMAT"},
|
||||
EnvVars: []string{envPFXFormat},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: flgCertTimeout,
|
||||
|
|
@ -228,5 +258,6 @@ func getTime(ctx *cli.Context, name string) time.Time {
|
|||
if value == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
return *value
|
||||
}
|
||||
|
|
|
|||
77
cmd/hook.go
77
cmd/hook.go
|
|
@ -1,6 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -8,32 +9,70 @@ import (
|
|||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
)
|
||||
|
||||
func launchHook(hook string, meta map[string]string) error {
|
||||
const (
|
||||
hookEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
|
||||
hookEnvCertDomain = "LEGO_CERT_DOMAIN"
|
||||
hookEnvCertPath = "LEGO_CERT_PATH"
|
||||
hookEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
|
||||
hookEnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH"
|
||||
hookEnvCertPEMPath = "LEGO_CERT_PEM_PATH"
|
||||
hookEnvCertPFXPath = "LEGO_CERT_PFX_PATH"
|
||||
)
|
||||
|
||||
func launchHook(hook string, timeout time.Duration, meta map[string]string) error {
|
||||
if hook == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctxCmd, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
ctxCmd, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
parts := strings.Fields(hook)
|
||||
|
||||
cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
|
||||
cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...)
|
||||
cmd := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
|
||||
|
||||
output, err := cmdCtx.CombinedOutput()
|
||||
cmd.Env = append(os.Environ(), metaToEnv(meta)...)
|
||||
|
||||
if len(output) > 0 {
|
||||
fmt.Println(string(output))
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create pipe: %w", err)
|
||||
}
|
||||
|
||||
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
|
||||
return errors.New("hook timed out")
|
||||
cmd.Stderr = cmd.Stdout
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("start command: %w", err)
|
||||
}
|
||||
|
||||
return err
|
||||
go func() {
|
||||
<-ctxCmd.Done()
|
||||
|
||||
if ctxCmd.Err() != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
_ = stdout.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
fmt.Println(scanner.Text())
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
|
||||
return errors.New("hook timed out")
|
||||
}
|
||||
|
||||
return fmt.Errorf("wait command: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func metaToEnv(meta map[string]string) []string {
|
||||
|
|
@ -45,3 +84,21 @@ func metaToEnv(meta map[string]string) []string {
|
|||
|
||||
return envs
|
||||
}
|
||||
|
||||
func addPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *CertificatesStorage) {
|
||||
meta[hookEnvCertDomain] = domain
|
||||
meta[hookEnvCertPath] = certsStorage.GetFileName(domain, certExt)
|
||||
meta[hookEnvCertKeyPath] = certsStorage.GetFileName(domain, keyExt)
|
||||
|
||||
if certRes.IssuerCertificate != nil {
|
||||
meta[hookEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, issuerExt)
|
||||
}
|
||||
|
||||
if certsStorage.pem {
|
||||
meta[hookEnvCertPEMPath] = certsStorage.GetFileName(domain, pemExt)
|
||||
}
|
||||
|
||||
if certsStorage.pfx {
|
||||
meta[hookEnvCertPFXPath] = certsStorage.GetFileName(domain, pfxExt)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
61
cmd/hook_test.go
Normal file
61
cmd/hook_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_launchHook(t *testing.T) {
|
||||
err := launchHook("echo foo", 1*time.Second, map[string]string{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_launchHook_errors(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
hook string
|
||||
timeout time.Duration
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "kill the hook",
|
||||
hook: "sleep 5",
|
||||
timeout: 1 * time.Second,
|
||||
expected: "hook timed out",
|
||||
},
|
||||
{
|
||||
desc: "context timeout on Start",
|
||||
hook: "echo foo",
|
||||
timeout: 1 * time.Nanosecond,
|
||||
expected: "start command: context deadline exceeded",
|
||||
},
|
||||
{
|
||||
desc: "multiple short sleeps",
|
||||
hook: "./testdata/sleepy.sh",
|
||||
timeout: 1 * time.Second,
|
||||
expected: "hook timed out",
|
||||
},
|
||||
{
|
||||
desc: "long sleep",
|
||||
hook: "./testdata/sleeping_beauty.sh",
|
||||
timeout: 1 * time.Second,
|
||||
expected: "hook timed out",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := launchHook(test.hook, test.timeout, map[string]string{})
|
||||
require.EqualError(t, err, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ func main() {
|
|||
}
|
||||
|
||||
var defaultPath string
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
defaultPath = filepath.Join(cwd, ".lego")
|
||||
|
|
|
|||
2
cmd/lego/zz_gen_version.go
generated
2
cmd/lego/zz_gen_version.go
generated
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
package main
|
||||
|
||||
const defaultVersion = "v4.20.2+dev-release"
|
||||
const defaultVersion = "v4.32.0+dev-detach"
|
||||
|
||||
var version = ""
|
||||
|
||||
|
|
|
|||
101
cmd/setup.go
101
cmd/setup.go
|
|
@ -1,25 +1,38 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/acme"
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/log"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const filePerm os.FileMode = 0o600
|
||||
|
||||
func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.Client) {
|
||||
// setupClient creates a new client with challenge settings.
|
||||
func setupClient(ctx *cli.Context, account *Account, keyType certcrypto.KeyType) *lego.Client {
|
||||
client := newClient(ctx, account, keyType)
|
||||
|
||||
setupChallenges(ctx, client)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func setupAccount(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, certcrypto.KeyType) {
|
||||
keyType := getKeyType(ctx)
|
||||
privateKey := accountsStorage.GetPrivateKey(keyType)
|
||||
|
||||
|
|
@ -27,12 +40,10 @@ func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.
|
|||
if accountsStorage.ExistsAccountFilePath() {
|
||||
account = accountsStorage.LoadAccount(privateKey)
|
||||
} else {
|
||||
account = &Account{Email: accountsStorage.GetUserID(), key: privateKey}
|
||||
account = &Account{Email: accountsStorage.GetEmail(), key: privateKey}
|
||||
}
|
||||
|
||||
client := newClient(ctx, account, keyType)
|
||||
|
||||
return account, client
|
||||
return account, keyType
|
||||
}
|
||||
|
||||
func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client {
|
||||
|
|
@ -43,6 +54,7 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy
|
|||
KeyType: keyType,
|
||||
Timeout: time.Duration(ctx.Int(flgCertTimeout)) * time.Second,
|
||||
OverallRequestLimit: ctx.Int(flgOverallRequestLimit),
|
||||
DisableCommonName: ctx.Bool(flgDisableCommonName),
|
||||
}
|
||||
config.UserAgent = getUserAgent(ctx)
|
||||
|
||||
|
|
@ -51,11 +63,26 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy
|
|||
}
|
||||
|
||||
if ctx.Bool(flgTLSSkipVerify) {
|
||||
config.HTTPClient.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
defaultTransport, ok := config.HTTPClient.Transport.(*http.Transport)
|
||||
if ok { // This is always true because the default client used by the CLI defined the transport.
|
||||
tr := defaultTransport.Clone()
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
config.HTTPClient.Transport = tr
|
||||
}
|
||||
}
|
||||
|
||||
retryClient := retryablehttp.NewClient()
|
||||
retryClient.RetryMax = 5
|
||||
retryClient.HTTPClient = config.HTTPClient
|
||||
retryClient.CheckRetry = checkRetry
|
||||
retryClient.Logger = nil
|
||||
|
||||
if _, v := os.LookupEnv("LEGO_DEBUG_ACME_HTTP_CLIENT"); v {
|
||||
retryClient.Logger = log.Logger
|
||||
}
|
||||
|
||||
config.HTTPClient = retryClient.StandardClient()
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not create client: %v", err)
|
||||
|
|
@ -87,15 +114,8 @@ func getKeyType(ctx *cli.Context) certcrypto.KeyType {
|
|||
}
|
||||
|
||||
log.Fatalf("Unsupported KeyType: %s", keyType)
|
||||
return ""
|
||||
}
|
||||
|
||||
func getEmail(ctx *cli.Context) string {
|
||||
email := ctx.String(flgEmail)
|
||||
if email == "" {
|
||||
log.Fatalf("You have to pass an account (email address) to the program using --%s or -m", flgEmail)
|
||||
}
|
||||
return email
|
||||
return ""
|
||||
}
|
||||
|
||||
func getUserAgent(ctx *cli.Context) string {
|
||||
|
|
@ -108,6 +128,7 @@ func createNonExistingFolder(path string) error {
|
|||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -116,10 +137,12 @@ func readCSRFile(filename string) (*x509.CertificateRequest, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw := bytes
|
||||
|
||||
// see if we can find a PEM-encoded CSR
|
||||
var p *pem.Block
|
||||
|
||||
rest := bytes
|
||||
for {
|
||||
// decode a PEM block
|
||||
|
|
@ -141,3 +164,49 @@ func readCSRFile(filename string) (*x509.CertificateRequest, error) {
|
|||
// (if this assumption is wrong, parsing these bytes will fail)
|
||||
return x509.ParseCertificateRequest(raw)
|
||||
}
|
||||
|
||||
func checkRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||
rt, err := retryablehttp.ErrorPropagatedRetryPolicy(ctx, resp, err)
|
||||
if err != nil {
|
||||
return rt, err
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode/100 == 2 {
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
all, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
var errorDetails *acme.ProblemDetails
|
||||
|
||||
err = json.Unmarshal(all, &errorDetails)
|
||||
if err != nil {
|
||||
return rt, fmt.Errorf("%s %s: %s", resp.Request.Method, resp.Request.URL.Redacted(), string(all))
|
||||
}
|
||||
|
||||
switch errorDetails.Type {
|
||||
case acme.BadNonceErr:
|
||||
return false, &acme.NonceError{
|
||||
ProblemDetails: errorDetails,
|
||||
}
|
||||
|
||||
case acme.AlreadyReplacedErr:
|
||||
if errorDetails.HTTPStatus == http.StatusConflict {
|
||||
return false, &acme.AlreadyReplacedError{
|
||||
ProblemDetails: errorDetails,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
log.Warnf("retry: %v", errorDetails)
|
||||
|
||||
return rt, errorDetails
|
||||
}
|
||||
}
|
||||
|
||||
return rt, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@ func setupChallenges(ctx *cli.Context, client *lego.Client) {
|
|||
}
|
||||
|
||||
if ctx.Bool(flgHTTP) {
|
||||
err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx))
|
||||
err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx), http01.SetDelay(ctx.Duration(flgHTTPDelay)))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Bool(flgTLS) {
|
||||
err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx))
|
||||
err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx), tlsalpn01.SetDelay(ctx.Duration(flgTLSDelay)))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
@ -54,18 +54,21 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
|
|||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return ps
|
||||
case ctx.IsSet(flgHTTPMemcachedHost):
|
||||
ps, err := memcached.NewMemcachedProvider(ctx.StringSlice(flgHTTPMemcachedHost))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return ps
|
||||
case ctx.IsSet(flgHTTPS3Bucket):
|
||||
ps, err := s3.NewHTTPProvider(ctx.String(flgHTTPS3Bucket))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return ps
|
||||
case ctx.IsSet(flgHTTPPort):
|
||||
iface := ctx.String(flgHTTPPort)
|
||||
|
|
@ -82,12 +85,14 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
|
|||
if header := ctx.String(flgHTTPProxyHeader); header != "" {
|
||||
srv.SetProxyHeader(header)
|
||||
}
|
||||
|
||||
return srv
|
||||
case ctx.Bool(flgHTTP):
|
||||
srv := http01.NewProviderServer("", "")
|
||||
if header := ctx.String(flgHTTPProxyHeader); header != "" {
|
||||
srv.SetProxyHeader(header)
|
||||
}
|
||||
|
||||
return srv
|
||||
default:
|
||||
log.Fatal("Invalid HTTP challenge options.")
|
||||
|
|
|
|||
3
cmd/testdata/sleeping_beauty.sh
vendored
Executable file
3
cmd/testdata/sleeping_beauty.sh
vendored
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash -e
|
||||
|
||||
sleep 50
|
||||
7
cmd/testdata/sleepy.sh
vendored
Executable file
7
cmd/testdata/sleepy.sh
vendored
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash -e
|
||||
|
||||
for i in `seq 1 10`
|
||||
do
|
||||
echo $i
|
||||
sleep 0.2
|
||||
done
|
||||
2236
cmd/zz_gen_cmd_dnshelp.go
generated
2236
cmd/zz_gen_cmd_dnshelp.go
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,14 @@
|
|||
.PHONY: default clean hugo hugo-build
|
||||
.PHONY: default clean serve build
|
||||
|
||||
default: clean hugo
|
||||
default: clean serve
|
||||
|
||||
clean:
|
||||
rm -rf public/
|
||||
|
||||
|
||||
hugo-build: clean
|
||||
build: clean
|
||||
hugo --enableGitInfo --source .
|
||||
|
||||
hugo:
|
||||
serve:
|
||||
hugo server --disableFastRender --enableGitInfo --watch --source .
|
||||
# hugo server -D
|
||||
|
|
|
|||
|
|
@ -7,23 +7,34 @@ chapter: false
|
|||
|
||||
Let's Encrypt client and ACME library written in Go.
|
||||
|
||||
{{% notice important %}}
|
||||
lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️
|
||||
|
||||
This project is not owned by a company. I'm not an employee of a company.
|
||||
|
||||
I don't have gifted domains/accounts from DNS companies.
|
||||
|
||||
I've been maintaining it for about 10 years.
|
||||
{{% /notice %}}
|
||||
|
||||
## Features
|
||||
|
||||
- ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html)
|
||||
- Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension
|
||||
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses
|
||||
- Support [draft-ietf-acme-ari-01](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
|
||||
- Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension
|
||||
- Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension
|
||||
- Comes with about [180 DNS providers]({{% ref "dns" %}})
|
||||
- Register with CA
|
||||
- Obtain certificates, both from scratch or with an existing CSR
|
||||
- Renew certificates
|
||||
- Revoke certificates
|
||||
- Robust implementation of all ACME challenges
|
||||
- Robust implementation of ACME challenges:
|
||||
- HTTP (http-01)
|
||||
- DNS (dns-01)
|
||||
- TLS (tls-alpn-01)
|
||||
- SAN certificate support
|
||||
- [CNAME support](https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme.html) by default
|
||||
- Comes with multiple optional [DNS providers]({{% ref "dns" %}})
|
||||
- [Custom challenge solvers]({{% ref "usage/library/Writing-a-Challenge-Solver" %}})
|
||||
- Certificate bundling
|
||||
- OCSP helper function
|
||||
|
|
|
|||
|
|
@ -5,6 +5,16 @@ draft: false
|
|||
weight: 3
|
||||
---
|
||||
|
||||
{{% notice important %}}
|
||||
lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️
|
||||
|
||||
This project is not owned by a company. I'm not an employee of a company.
|
||||
|
||||
I don't have gifted domains/accounts from DNS companies.
|
||||
|
||||
I've been maintaining it for about 10 years.
|
||||
{{% /notice %}}
|
||||
|
||||
## Configuration and Credentials
|
||||
|
||||
Credentials and DNS configuration for DNS providers must be passed through environment variables.
|
||||
|
|
|
|||
23
docs/content/dns/zz_gen_acme-dns.md
generated
23
docs/content/dns/zz_gen_acme-dns.md
generated
|
|
@ -28,7 +28,13 @@ Here is an example bash command using the Joohoi's ACME-DNS provider:
|
|||
```bash
|
||||
ACME_DNS_API_BASE=http://10.0.0.8:4443 \
|
||||
ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \
|
||||
lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run
|
||||
lego --dns "acme-dns" -d '*.example.com' -d example.com run
|
||||
|
||||
# or
|
||||
|
||||
ACME_DNS_API_BASE=http://10.0.0.8:4443 \
|
||||
ACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \
|
||||
lego --dns "acme-dns" -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -39,20 +45,29 @@ lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com
|
|||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `ACME_DNS_API_BASE` | The ACME-DNS API address |
|
||||
| `ACME_DNS_STORAGE_BASE_URL` | The ACME-DNS JSON account data server. |
|
||||
| `ACME_DNS_STORAGE_PATH` | The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates. |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `ACME_DNS_ALLOWLIST` | Source networks using CIDR notation (multiple values should be separated with a comma). |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://github.com/joohoi/acme-dns#api)
|
||||
- [Go client](https://github.com/cpu/goacmedns)
|
||||
- [Go client](https://github.com/nrdcg/goacmedns)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/acmedns/acmedns.toml -->
|
||||
|
|
|
|||
69
docs/content/dns/zz_gen_active24.md
generated
Normal file
69
docs/content/dns/zz_gen_active24.md
generated
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
title: "Active24"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: active24
|
||||
dnsprovider:
|
||||
since: "v4.23.0"
|
||||
code: "active24"
|
||||
url: "https://www.active24.cz"
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/active24/active24.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
|
||||
Configuration for [Active24](https://www.active24.cz).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `active24`
|
||||
- Since: v4.23.0
|
||||
|
||||
|
||||
Here is an example bash command using the Active24 provider:
|
||||
|
||||
```bash
|
||||
ACTIVE24_API_KEY="xxx" \
|
||||
ACTIVE24_SECRET="yyy" \
|
||||
lego --dns active24 -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `ACTIVE24_API_KEY` | API key |
|
||||
| `ACTIVE24_SECRET` | Secret |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `ACTIVE24_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `ACTIVE24_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `ACTIVE24_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
|
||||
| `ACTIVE24_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://rest.active24.cz/v2/docs)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/active24/active24.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
18
docs/content/dns/zz_gen_alidns.md
generated
18
docs/content/dns/zz_gen_alidns.md
generated
|
|
@ -28,13 +28,13 @@ Here is an example bash command using the Alibaba Cloud DNS provider:
|
|||
```bash
|
||||
# Setup using instance RAM role
|
||||
ALICLOUD_RAM_ROLE=lego \
|
||||
lego --email you@example.com --dns alidns -d '*.example.com' -d example.com run
|
||||
lego --dns alidns -d '*.example.com' -d example.com run
|
||||
|
||||
# Or, using credentials
|
||||
ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \
|
||||
ALICLOUD_SECRET_KEY=your-secret-key \
|
||||
ALICLOUD_SECURITY_TOKEN=your-sts-token \
|
||||
lego --email you@example.com --dns alidns - -d '*.example.com' -d example.com run
|
||||
lego --dns alidns - -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ lego --email you@example.com --dns alidns - -d '*.example.com' -d example.com ru
|
|||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `ALICLOUD_ACCESS_KEY` | Access key ID |
|
||||
| `ALICLOUD_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/doc-detail/54579.htm) |
|
||||
| `ALICLOUD_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance) |
|
||||
| `ALICLOUD_SECRET_KEY` | Access Key secret |
|
||||
| `ALICLOUD_SECURITY_TOKEN` | STS Security Token (optional) |
|
||||
|
||||
|
|
@ -57,10 +57,12 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
|||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `ALICLOUD_HTTP_TIMEOUT` | API request timeout |
|
||||
| `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check |
|
||||
| `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
||||
| `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge |
|
||||
| `ALICLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |
|
||||
| `ALICLOUD_LINE` | Line (Default: default) |
|
||||
| `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
|
||||
| `ALICLOUD_REGION_ID` | Region ID (Default: cn-hangzhou) |
|
||||
| `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
|
@ -71,7 +73,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
|||
## More information
|
||||
|
||||
- [API documentation](https://www.alibabacloud.com/help/en/alibaba-cloud-dns/latest/api-alidns-2015-01-09-dir-parsing-records)
|
||||
- [Go client](https://github.com/aliyun/alibaba-cloud-sdk-go)
|
||||
- [Go client](https://github.com/alibabacloud-go/alidns-20150109)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/alidns/alidns.toml -->
|
||||
|
|
|
|||
78
docs/content/dns/zz_gen_aliesa.md
generated
Normal file
78
docs/content/dns/zz_gen_aliesa.md
generated
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
title: "AlibabaCloud ESA"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: aliesa
|
||||
dnsprovider:
|
||||
since: "v4.29.0"
|
||||
code: "aliesa"
|
||||
url: "https://www.alibabacloud.com/en/product/esa"
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/aliesa/aliesa.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
|
||||
Configuration for [AlibabaCloud ESA](https://www.alibabacloud.com/en/product/esa).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `aliesa`
|
||||
- Since: v4.29.0
|
||||
|
||||
|
||||
Here is an example bash command using the AlibabaCloud ESA provider:
|
||||
|
||||
```bash
|
||||
# Setup using instance RAM role
|
||||
ALIESA_RAM_ROLE=lego \
|
||||
lego --dns aliesa -d '*.example.com' -d example.com run
|
||||
|
||||
# Or, using credentials
|
||||
ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \
|
||||
ALIESA_SECRET_KEY=your-secret-key \
|
||||
ALIESA_SECURITY_TOKEN=your-sts-token \
|
||||
lego --dns aliesa - -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `ALIESA_ACCESS_KEY` | Access key ID |
|
||||
| `ALIESA_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance) |
|
||||
| `ALIESA_SECRET_KEY` | Access Key secret |
|
||||
| `ALIESA_SECURITY_TOKEN` | STS Security Token (optional) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `ALIESA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `ALIESA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `ALIESA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
|
||||
| `ALIESA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-overview?spm=a2c63.p38356.help-menu-2673927.d_6_0_0.20b224c28PSZDc#:~:text=DNS-,DNS%20records,-DNS%20records)
|
||||
- [Go client](https://github.com/alibabacloud-go/esa-20240910)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/aliesa/aliesa.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
8
docs/content/dns/zz_gen_allinkl.md
generated
8
docs/content/dns/zz_gen_allinkl.md
generated
|
|
@ -28,7 +28,7 @@ Here is an example bash command using the all-inkl provider:
|
|||
```bash
|
||||
ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \
|
||||
ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \
|
||||
lego --email you@example.com --dns allinkl -d '*.example.com' -d example.com run
|
||||
lego --dns allinkl -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -49,9 +49,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
|||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `ALL_INKL_HTTP_TIMEOUT` | API request timeout |
|
||||
| `ALL_INKL_POLLING_INTERVAL` | Time between DNS propagation check |
|
||||
| `ALL_INKL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
||||
| `ALL_INKL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `ALL_INKL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `ALL_INKL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
|
|
|||
68
docs/content/dns/zz_gen_alwaysdata.md
generated
Normal file
68
docs/content/dns/zz_gen_alwaysdata.md
generated
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
title: "Alwaysdata"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: alwaysdata
|
||||
dnsprovider:
|
||||
since: "v4.31.0"
|
||||
code: "alwaysdata"
|
||||
url: "https://alwaysdata.com/"
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/alwaysdata/alwaysdata.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
|
||||
Configuration for [Alwaysdata](https://alwaysdata.com/).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `alwaysdata`
|
||||
- Since: v4.31.0
|
||||
|
||||
|
||||
Here is an example bash command using the Alwaysdata provider:
|
||||
|
||||
```bash
|
||||
ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
|
||||
lego --dns alwaysdata -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `ALWAYSDATA_API_KEY` | API Key |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `ALWAYSDATA_ACCOUNT` | Account name |
|
||||
| `ALWAYSDATA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `ALWAYSDATA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `ALWAYSDATA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
|
||||
| `ALWAYSDATA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://help.alwaysdata.com/en/api/resources/)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/alwaysdata/alwaysdata.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
73
docs/content/dns/zz_gen_anexia.md
generated
Normal file
73
docs/content/dns/zz_gen_anexia.md
generated
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
title: "Anexia CloudDNS"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: anexia
|
||||
dnsprovider:
|
||||
since: "v4.28.0"
|
||||
code: "anexia"
|
||||
url: "https://www.anexia-it.com/"
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/anexia/anexia.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
|
||||
Configuration for [Anexia CloudDNS](https://www.anexia-it.com/).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `anexia`
|
||||
- Since: v4.28.0
|
||||
|
||||
|
||||
Here is an example bash command using the Anexia CloudDNS provider:
|
||||
|
||||
```bash
|
||||
ANEXIA_TOKEN=xxx \
|
||||
lego --dns anexia -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `ANEXIA_TOKEN` | API token for Anexia Engine |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|--------------------------------|-------------|
|
||||
| `ANEXIA_API_URL` | API endpoint URL (default: https://engine.anexia-it.com) |
|
||||
| `ANEXIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
|
||||
| `ANEXIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
|
||||
| `ANEXIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |
|
||||
| `ANEXIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
|
||||
## Description
|
||||
|
||||
You need to create an API token in the [Anexia Engine](https://engine.anexia-it.com/).
|
||||
|
||||
The token must have permissions to manage DNS zones and records.
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://engine.anexia-it.com/docs/en/module/clouddns/api)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/anexia/anexia.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue