Compare commits

..

No commits in common. "master" and "v4.17.2" have entirely different histories.

1820 changed files with 20699 additions and 84202 deletions

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
**/zz_gen_*.* linguist-generated
docs/data/zz_cli_help.toml linguist-generated

View file

@ -1,4 +1,4 @@
name: 🐞 Bug Report
name: Bug Report
description: Create a report to help us improve.
labels: [bug]
body:
@ -7,9 +7,9 @@ body:
attributes:
label: Welcome
options:
- label: Yes, I'm using a binary release within the two latest releases.
- label: Yes, I'm using a binary release within 2 latest releases.
required: true
- label: Yes, I've searched for similar issues on GitHub and didn't find any.
- label: Yes, I've searched 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,7 +35,6 @@ body:
attributes:
label: How do you use lego?
options:
- I don't know
- Library
- Binary
- Docker image
@ -43,10 +42,6 @@ body:
- Through Caddy
- Through Terraform ACME provider
- Through Bitnami
- Through 1Panel
- Through Zoraxy
- Through Certimate
- go install
- Other
validations:
required: true
@ -67,9 +62,8 @@ body:
- type: textarea
id: version
attributes:
label: Effective version of lego
label: Version of lego
description: |-
`latest` or `dev` are not effective versions.
```console
$ lego --version
```

View file

@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Questions
- name: Questions
url: https://github.com/go-acme/lego/discussions
about: If you have a question, or are looking for advice, please post on our Discussions section!
- name: 📖 Documentation
- name: lego documentation
url: https://go-acme.github.io/lego/
about: Please take a look to our documentation.

View file

@ -1,4 +1,4 @@
name: 💡 Feature request
name: Feature request
description: Suggest an idea for this project.
body:
- type: checkboxes
@ -6,7 +6,7 @@ body:
attributes:
label: Welcome
options:
- label: Yes, I've searched for similar issues on GitHub and didn't find any.
- label: Yes, I've searched similar issues on GitHub and didn't find any.
required: true
- type: dropdown
@ -14,7 +14,6 @@ body:
attributes:
label: How do you use lego?
options:
- I don't know
- Library
- Binary
- Docker image
@ -22,22 +21,10 @@ body:
- Through Caddy
- Through Terraform ACME provider
- 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:

View file

@ -1,4 +1,4 @@
name: 🧩 New DNS provider support
name: New DNS provider support
description: Request for the support of a new DNS provider.
title: "Support for provider: <put the name of your provider>"
labels: [enhancement, new-provider]
@ -8,21 +8,15 @@ body:
attributes:
label: Welcome
options:
- label: Yes, I've searched for similar issues on GitHub and didn't find any.
- label: Yes, I've searched 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 can test an implementation with the help of the maintainers if someone creates a pull request.
- label: Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider.
required: false
- type: dropdown
@ -30,7 +24,6 @@ body:
attributes:
label: How do you use lego?
options:
- I don't know
- Library
- Binary
- Docker image
@ -38,25 +31,10 @@ body:
- Through Caddy
- Through Terraform ACME provider
- 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:

View file

@ -1,12 +0,0 @@
<!--
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.
-->

View file

@ -1,30 +0,0 @@
PULL REQUEST TEMPLATE FOR MAINTAINERS ONLY.
https://github.com/go-acme/lego/compare/master...ldez:branch?quick_pull=1&title=Add+DNS+provider+for+&labels=enhancement,area/dnsprovider,state/need-user-tests&template=mnp.md
?quick_pull=1&title=Add+DNS+provider+for+&labels=enhancement,area/dnsprovider,state/need-user-tests&template=mnp.md
---
- [x] adds a description to your PR
- [x] have a homogeneous design with the other providers
- [ ] add tests (units)
- [ ] add tests ("live")
- [ ] add a provider descriptor
- [ ] generate CLI help, documentation, and readme.
- [ ] be able to do: _(and put the output of this command to a comment)_
```bash
make build
rm -rf .lego
EXAMPLE_USERNAME=xxx \
./dist/lego -m your_email@example.com --dns EXAMPLE -d *.example.com -d example.com -s https://acme-staging-v02.api.letsencrypt.org/directory run
```
Note the wildcard domain is important.
- [ ] pass the linter
- [ ] do `go mod tidy`
Ping @xxx, can you run the command (with your domain, email, credentials, etc.)?
Closes #

View file

@ -12,16 +12,20 @@ jobs:
runs-on: ubuntu-latest
env:
GO_VERSION: stable
HUGO_VERSION: 0.148.2
HUGO_VERSION: 0.117.0
CGO_ENABLED: 0
steps:
- uses: actions/checkout@v6
# https://github.com/marketplace/actions/checkout
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v6
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

View file

@ -16,12 +16,17 @@ jobs:
strategy:
matrix:
go-version: [ oldstable, stable ]
go-version: [ stable ]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
# 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
with:
go-version: ${{ matrix.go-version }}

View file

@ -13,44 +13,54 @@ jobs:
runs-on: ubuntu-latest
env:
GO_VERSION: stable
GOLANGCI_LINT_VERSION: v2.10
HUGO_VERSION: 0.148.2
GOLANGCI_LINT_VERSION: v1.59.0
HUGO_VERSION: 0.117.0
CGO_ENABLED: 0
LEGO_E2E_TESTS: CI
MEMCACHED_HOSTS: localhost:11211
steps:
- uses: actions/checkout@v6
# https://github.com/marketplace/actions/checkout
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v6
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Check and get dependencies
run: |
go mod tidy --diff
go mod tidy
git diff --exit-code go.mod
git diff --exit-code go.sum
- name: Generate and Check generated elements
# https://golangci-lint.run/usage/install#other-ci
- name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }}
run: |
make generate-dns
git diff --exit-code
- uses: golangci/golangci-lint-action@v9
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}
install-only: true
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
- name: Install Pebble
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@3fe019bbc0a41ed16e2fee31592bb91751acaa47
- name: Install challtestsrv
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@3fe019bbc0a41ed16e2fee31592bb91751acaa47
- name: Set up a Memcached server
run: docker run -d --rm -p 11211:11211 memcached:1.6-alpine
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
- name: Make
run: |

View file

@ -5,11 +5,6 @@ on:
tags:
- v*
permissions:
# Allow the workflow to write attestations.
id-token: write
attestations: write
jobs:
release:
@ -42,11 +37,13 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@v6
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v6
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
@ -67,21 +64,10 @@ jobs:
# https://goreleaser.com/ci/actions/
- name: Run GoReleaser
id: goreleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v5
with:
version: v2.13.0
version: latest
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 }}

View file

@ -1,284 +1,238 @@
version: "2"
run:
timeout: 10m
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
settings:
gofumpt:
extra-rules: true
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
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:
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
linters:
default: all
enable-all: true
disable:
- wsl # Deprecated
- bodyclose
- canonicalheader
- contextcheck
- gomnd # deprecated
- cyclop # duplicate of gocyclo
- dupl # not relevant
- err113 # not relevant
- errchkjson
- errname
- exhaustive # not relevant
- exhaustruct # not relevant
- forbidigo
- forcetypeassert
- gosec
- gosmopolitan # not relevant
- ireturn # not relevant
- lll
- makezero # not relevant
- mnd
- musttag # false-positive https://github.com/junk1tm/musttag/issues/17
- 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
- rowserrcheck # not relevant (SQL)
- execinquery # 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
- varnamelen # not relevant
- paralleltest # not relevant
- nestif # too many false-positive
- wrapcheck
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:
- 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
- err113 # not relevant
- nlreturn # not relevant
- wsl # not relevant
- 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
- 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
issues:
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: providers/dns/gcloud/googlecloud_test.go
text: 'string `(lego\.wtf|manhattan)` has (\d+) occurrences, make it a constant'
- path: providers/dns/zoneee/zoneee_test.go
text: 'string `(bar|foo)` has (\d+) occurrences, make it a constant'
- path: certcrypto/crypto.go
text: '(tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable'
- path: challenge/dns01/nameserver.go
text: '(defaultNameservers|recursiveNameservers|fqdnSoaCache|muFqdnSoaCache) is a global variable'
- path: challenge/dns01/nameserver_.+.go
text: 'dnsTimeout is a global variable'
- path: challenge/dns01/nameserver_test.go
text: 'findXByFqdnTestCases is a global variable'
- path: challenge/http01/domain_matcher.go
text: 'string `Host` has \d occurrences, make it a constant'
- path: challenge/http01/domain_matcher.go
text: 'cyclomatic complexity \d+ of func `parseForwardedHeader` is high'
- path: challenge/http01/domain_matcher.go
text: "Function 'parseForwardedHeader' has too many statements"
- path: challenge/tlsalpn01/tls_alpn_challenge.go
text: 'idPeAcmeIdentifierV1 is a global variable'
- path: log/logger.go
text: 'Logger is a global variable'
- path: 'e2e/(dnschallenge/)?[\d\w]+_test.go'
text: load is a global variable
- path: 'providers/dns/([\d\w]+/)*[\d\w]+_test.go'
text: 'envTest is a global variable'
- path: 'providers/http/([\d\w]+/)*[\d\w]+_test.go'
text: 'envTest is a global variable'
- path: providers/dns/namecheap/namecheap_test.go
text: 'testCases is a global variable'
- path: providers/dns/acmedns/acmedns_test.go
text: 'egTestAccount is a global variable'
- path: providers/http/memcached/memcached_test.go
text: 'memcachedHosts is a global variable'
- path: providers/dns/sakuracloud/client_test.go
text: 'cyclomatic complexity 13 of func `(TestDNSProvider_cleanupTXTRecord_concurrent|TestDNSProvider_addTXTRecord_concurrent)` is high'
- path: providers/dns/dns_providers.go
text: "Function 'NewDNSChallengeProviderByName' has too many statements"
- path: cmd/flags.go
text: "Function 'CreateFlags' is too long"
- path: certificate/certificates.go
text: "Function 'GetOCSP' is too long"
- path: providers/dns/otc/client.go
text: "Function 'loginRequest' is too long"
- path: providers/dns/gandi/gandi.go
text: "Function 'Present' is too long"
- path: cmd/zz_gen_cmd_dnshelp.go
linters:
- gocyclo
- funlen
- path: providers/dns/checkdomain/internal/types.go
text: '`payed` is a misspelling of `paid`'
- path: providers/dns/namecheap/namecheap_test.go
text: 'cognitive complexity (\d+) of func `TestDNSProvider_getHosts` is high'
- 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'
- path: challenge/http01/domain_matcher.go
text: 'yodaStyleExpr'
- path: providers/dns/dns_providers.go
text: 'Function name: NewDNSChallengeProviderByName,'
- path: providers/dns/sakuracloud/wrapper.go
text: 'mu is a global variable'
- path: providers/dns/hosttech/internal/client_test.go
text: 'Duplicate words \(0\) found'
- path: cmd/cmd_renew.go
text: 'cyclomatic complexity \d+ of func `(renewForDomains|renewForCSR)` is high'
- path: providers/dns/cpanel/cpanel.go
text: 'cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high'
output:
sort-results: true
sort-order:
- linter
- file

View file

@ -1,11 +1,9 @@
version: 2
project_name: lego
builds:
- binary: lego
main: ./cmd/lego/
main: ./cmd/lego/main.go
env:
- CGO_ENABLED=0
flags:
@ -14,12 +12,12 @@ builds:
- -s -w -X main.version={{.Version}}
goos:
- linux
- darwin
- windows
- darwin
- linux
- freebsd
- openbsd
- solaris
# - solaris # https://github.com/gofrs/flock/issues/60
goarch:
- amd64
- 386
@ -42,10 +40,6 @@ 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
@ -55,98 +49,123 @@ 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 }}'
formats: ['tar.gz']
format: tar.gz
format_overrides:
- goos: windows
formats: ['zip']
format: zip
files:
- LICENSE
- CHANGELOG.md
dockers_v2:
- images:
- 'goacme/lego'
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
dockerfile: buildx.Dockerfile
platforms:
- linux/amd64
- linux/arm64
- linux/arm/v7
tags:
- 'latest'
- 'v{{ .Major }}'
- 'v{{ .Major }}.{{ .Minor }}'
- '{{ .Tag }}'
labels:
image_templates:
- 'goacme/lego:latest-amd64'
- 'goacme/lego:{{ .Tag }}-amd64'
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-amd64'
build_flag_templates:
- '--pull'
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
'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}}'
- '--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'
- 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'
snapcrafts:
- name_template: "{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
disable: false
publish: true
- name: lego
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: lego
command: bin/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"

File diff suppressed because it is too large Load diff

View file

@ -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 behavior and the actual behavior.
follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behaviour and the actual behaviour.
## Feature proposals and requests
@ -20,26 +20,31 @@ 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 that alter the behavior of the program,
add new behavior or somehow alter code in a non-trivial way should **always** include tests.
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.
**IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of the [MIT License](LICENSE).
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).
### How to create a pull request
Requirements:
- `go` v1.24+
- `go` v1.15+
- 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
@ -51,12 +56,14 @@ git fetch upstream
```bash
# Create your branch
git switch -c my-feature
git checkout -b my-feature
## Create your code ##
```
```bash
# Format
make fmt
# Linters
make checks
# Tests

View file

@ -1,6 +1,5 @@
The MIT License (MIT)
Copyright (c) 2017-2024 Ludovic Fernandez
Copyright (c) 2015-2017 Sebastian Erhart
Permission is hereby granted, free of charge, to any person obtaining a copy

View file

@ -39,25 +39,25 @@ checks:
.PHONY: patch minor major detach
patch:
go run ./internal/releaser/ release -m patch
go run internal/release.go release -m patch
minor:
go run ./internal/releaser/ release -m minor
go run internal/release.go release -m minor
major:
go run ./internal/releaser/ release -m major
go run internal/release.go release -m major
detach:
go run ./internal/releaser/ detach
go run internal/release.go detach
# Docs
.PHONY: docs-build docs-serve docs-themes
docs-build: generate-dns
@make -C ./docs build
@make -C ./docs hugo-build
docs-serve: generate-dns
@make -C ./docs serve
@make -C ./docs hugo
docs-themes:
@make -C ./docs hugo-themes

295
README.md
View file

@ -5,36 +5,29 @@
# Lego
[ACME](https://www.rfc-editor.org/rfc/rfc8555.html) client and library for Let's Encrypt and other ACME CAs written in Go.
Let's Encrypt client and ACME library written in Go.
[![Go Reference](https://pkg.go.dev/badge/github.com/go-acme/lego/v4.svg)](https://pkg.go.dev/github.com/go-acme/lego/v4)
[![Build Status](https://github.com//go-acme/lego/workflows/Main/badge.svg?branch=master)](https://github.com//go-acme/lego/actions)
[![Docker Pulls](https://img.shields.io/docker/pulls/goacme/lego.svg)](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 ApplicationLayer Protocol Negotiation (ALPN) Challenge Extension
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses
- 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)
- Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
- Register with CA
- Obtain certificates, both from scratch or with an existing CSR
- Renew certificates
- Revoke certificates
- Robust implementation of ACME challenges:
- Robust implementation of all 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
@ -56,252 +49,44 @@ 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>
<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>
<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 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>
<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>
</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>
</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>
<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>
</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/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>
</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>
<td><a href="https://go-acme.github.io/lego/dns/httpnet/">http.net</a></td>
<td><a href="https://go-acme.github.io/lego/dns/huaweicloud/">Huawei Cloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/hurricane/">Hurricane Electric DNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/hyperone/">HyperOne</a></td>
<td><a href="https://go-acme.github.io/lego/dns/ibmcloud/">IBM Cloud (SoftLayer)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/iijdpf/">IIJ DNS Platform Service</a></td>
<td><a href="https://go-acme.github.io/lego/dns/infoblox/">Infoblox</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/infomaniak/">Infomaniak</a></td>
<td><a href="https://go-acme.github.io/lego/dns/iij/">Internet Initiative Japan</a></td>
<td><a href="https://go-acme.github.io/lego/dns/internetbs/">Internet.bs</a></td>
<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/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&#39;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>
<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>
<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>
</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>
<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>
<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>
<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>
<td><a href="https://go-acme.github.io/lego/dns/regfish/">Regfish</a></td>
<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>
</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>
</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/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>
</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>
</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>
<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>
</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>
</tr></table>
| | | | |
|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [Akamai EdgeDNS](https://go-acme.github.io/lego/dns/edgedns/) | [Alibaba Cloud DNS](https://go-acme.github.io/lego/dns/alidns/) | [all-inkl](https://go-acme.github.io/lego/dns/allinkl/) | [Amazon Lightsail](https://go-acme.github.io/lego/dns/lightsail/) |
| [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) | [ArvanCloud](https://go-acme.github.io/lego/dns/arvancloud/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | [Autodns](https://go-acme.github.io/lego/dns/autodns/) |
| [Azure (deprecated)](https://go-acme.github.io/lego/dns/azure/) | [Azure DNS](https://go-acme.github.io/lego/dns/azuredns/) | [Bindman](https://go-acme.github.io/lego/dns/bindman/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) |
| [Brandit](https://go-acme.github.io/lego/dns/brandit/) | [Bunny](https://go-acme.github.io/lego/dns/bunny/) | [Checkdomain](https://go-acme.github.io/lego/dns/checkdomain/) | [Civo](https://go-acme.github.io/lego/dns/civo/) |
| [Cloud.ru](https://go-acme.github.io/lego/dns/cloudru/) | [CloudDNS](https://go-acme.github.io/lego/dns/clouddns/) | [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) |
| [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) | [ConoHa](https://go-acme.github.io/lego/dns/conoha/) | [Constellix](https://go-acme.github.io/lego/dns/constellix/) | [CPanel/WHM](https://go-acme.github.io/lego/dns/cpanel/) |
| [Derak Cloud](https://go-acme.github.io/lego/dns/derak/) | [deSEC.io](https://go-acme.github.io/lego/dns/desec/) | [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) | [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) |
| [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) | [dnsHome.de](https://go-acme.github.io/lego/dns/dnshomede/) | [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | [DNSPod (deprecated)](https://go-acme.github.io/lego/dns/dnspod/) |
| [Domain Offensive (do.de)](https://go-acme.github.io/lego/dns/dode/) | [Domeneshop](https://go-acme.github.io/lego/dns/domeneshop/) | [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) |
| [Dyn](https://go-acme.github.io/lego/dns/dyn/) | [Dynu](https://go-acme.github.io/lego/dns/dynu/) | [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | [Efficient IP](https://go-acme.github.io/lego/dns/efficientip/) |
| [Epik](https://go-acme.github.io/lego/dns/epik/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) | [External program](https://go-acme.github.io/lego/dns/exec/) | [freemyip.com](https://go-acme.github.io/lego/dns/freemyip/) |
| [G-Core](https://go-acme.github.io/lego/dns/gcore/) | [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) |
| [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [Google Domains](https://go-acme.github.io/lego/dns/googledomains/) | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) |
| [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | [Hosttech](https://go-acme.github.io/lego/dns/hosttech/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [http.net](https://go-acme.github.io/lego/dns/httpnet/) |
| [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [IBM Cloud (SoftLayer)](https://go-acme.github.io/lego/dns/ibmcloud/) | [IIJ DNS Platform Service](https://go-acme.github.io/lego/dns/iijdpf/) |
| [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) | [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) |
| [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Ionos](https://go-acme.github.io/lego/dns/ionos/) | [IPv64](https://go-acme.github.io/lego/dns/ipv64/) | [iwantmyname](https://go-acme.github.io/lego/dns/iwantmyname/) |
| [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Liara](https://go-acme.github.io/lego/dns/liara/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) |
| [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Mail-in-a-Box](https://go-acme.github.io/lego/dns/mailinabox/) |
| [Manual](https://go-acme.github.io/lego/dns/manual/) | [Metaname](https://go-acme.github.io/lego/dns/metaname/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) |
| [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [NearlyFreeSpeech.NET](https://go-acme.github.io/lego/dns/nearlyfreespeech/) |
| [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [Nicmanager](https://go-acme.github.io/lego/dns/nicmanager/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) |
| [Njalla](https://go-acme.github.io/lego/dns/njalla/) | [Nodion](https://go-acme.github.io/lego/dns/nodion/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) |
| [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) | [plesk.com](https://go-acme.github.io/lego/dns/plesk/) | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) |
| [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [RcodeZero](https://go-acme.github.io/lego/dns/rcodezero/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) |
| [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) |
| [Selectel v2](https://go-acme.github.io/lego/dns/selectelv2/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Shellrent](https://go-acme.github.io/lego/dns/shellrent/) |
| [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) |
| [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/) | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/) |
| [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) |
| [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Webnames](https://go-acme.github.io/lego/dns/webnames/) |
| [Websupport](https://go-acme.github.io/lego/dns/websupport/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex 360](https://go-acme.github.io/lego/dns/yandex360/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) |
| [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | |
<!-- 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.yml).
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).

View file

@ -13,7 +13,6 @@ 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)
@ -30,9 +29,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 := decodeEABHmac(hmacEncoded)
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
if err != nil {
return acme.ExtendedAccount{}, err
return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %w", err)
}
eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac)
@ -52,12 +51,10 @@ 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
}
@ -68,7 +65,6 @@ 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
@ -85,20 +81,5 @@ 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))
}

View file

@ -1,35 +0,0 @@
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)
})
}
}

View file

@ -2,7 +2,6 @@ package api
import (
"bytes"
"context"
"crypto"
"encoding/json"
"errors"
@ -10,7 +9,7 @@ import (
"net/http"
"time"
"github.com/cenkalti/backoff/v5"
"github.com/cenkalti/backoff/v4"
"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"
@ -61,7 +60,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 any) (*http.Response, error) {
func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) {
content, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.New("failed to marshal message")
@ -72,44 +71,47 @@ func (a *Core) post(uri string, reqBody, response any) (*http.Response, error) {
// 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 any) (*http.Response, error) {
func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) {
return a.retrievablePost(uri, []byte{}, response)
}
func (a *Core) retrievablePost(uri string, content []byte, response any) (*http.Response, error) {
ctx := context.Background()
func (a *Core) retrievablePost(uri string, content []byte, response interface{}) (*http.Response, error) {
// 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
operation := func() (*http.Response, error) {
resp, err := a.signedPost(uri, content, response)
var resp *http.Response
operation := func() error {
var err 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 resp, err
return err
}
return resp, backoff.Permanent(err)
return backoff.Permanent(err)
}
return resp, nil
return nil
}
notify := func(err error, duration time.Duration) {
log.Infof("retry due to: %v", err)
}
return backoff.Retry(ctx, operation,
backoff.WithBackOff(bo),
backoff.WithMaxElapsedTime(20*time.Second),
backoff.WithNotify(notify))
err := backoff.RetryNotify(operation, bo, notify)
if err != nil {
return resp, err
}
return resp, nil
}
func (a *Core) signedPost(uri string, content []byte, response any) (*http.Response, error) {
func (a *Core) signedPost(uri string, content []byte, response interface{}) (*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)
@ -155,7 +157,6 @@ 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")
}

View file

@ -15,12 +15,10 @@ 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
}
@ -31,8 +29,6 @@ func (c *AuthorizationService) Deactivate(authzURL string) error {
}
var disabledAuth acme.Authorization
_, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth)
return err
}

View file

@ -2,12 +2,15 @@ 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.
@ -74,22 +77,62 @@ func (c *CertificateService) get(certURL string, bundle bool) (*acme.RawCertific
return nil, resp.Header, err
}
cert := c.getCertificateChain(data, bundle)
cert := c.getCertificateChain(data, resp.Header, bundle, certURL)
return cert, resp.Header, err
}
// getCertificateChain Returns the certificate and the issuer certificate.
func (c *CertificateService) getCertificateChain(cert []byte, bundle bool) *acme.RawCertificate {
func (c *CertificateService) getCertificateChain(cert []byte, headers http.Header, bundle bool, certURL string) *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}
}
// 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)
// 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...)
}
}
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
}

View file

@ -3,10 +3,11 @@ 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"
)
@ -73,34 +74,56 @@ rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2
`
func TestCertificateService_Get_issuerRelUp(t *testing.T) {
server := tester.MockACMEServer().
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
BuildHTTPS(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
}
})
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
core, err := New(server.Client(), "lego-test", server.URL+"/dir", "", key)
core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
cert, issuer, err := core.Certificates.Get(server.URL+"/certificate", true)
cert, issuer, err := core.Certificates.Get(apiURL+"/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) {
server := tester.MockACMEServer().
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
BuildHTTPS(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
}
})
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
core, err := New(server.Client(), "lego-test", server.URL+"/dir", "", key)
core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
cert, issuer, err := core.Certificates.Get(server.URL+"/certificate", true)
cert, issuer, err := core.Certificates.Get(apiURL+"/certificate", true)
require.NoError(t, err)
assert.Equal(t, certResponseMock, string(cert), "Certificate")
assert.Equal(t, issuerMock, string(issuer), "IssuerCertificate")

View file

@ -17,7 +17,6 @@ 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
@ -25,7 +24,6 @@ func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil
}
@ -36,7 +34,6 @@ 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
@ -44,6 +41,5 @@ func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil
}

View file

@ -1,52 +0,0 @@
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),
)
}

View file

@ -1,111 +0,0 @@
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))
})
}
}

View file

@ -11,11 +11,10 @@ import (
// Manager Manages nonces.
type Manager struct {
sync.Mutex
do *sender.Doer
nonceURL string
nonces []string
sync.Mutex
}
// NewManager Creates a new Manager.
@ -37,7 +36,6 @@ func (n *Manager) Pop() (string, bool) {
nonce := n.nonces[len(n.nonces)-1]
n.nonces = n.nonces[:len(n.nonces)-1]
return nonce, true
}
@ -45,7 +43,6 @@ func (n *Manager) Pop() (string, bool) {
func (n *Manager) Push(nonce string) {
n.Lock()
defer n.Unlock()
n.nonces = append(n.nonces, nonce)
}
@ -54,7 +51,6 @@ func (n *Manager) Nonce() (string, error) {
if nonce, ok := n.Pop(); ok {
return nonce, nil
}
return n.getNonce()
}

View file

@ -8,52 +8,45 @@ 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/servermock"
"github.com/go-acme/lego/v4/platform/tester"
)
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
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)
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)
doer := sender.NewDoer(http.DefaultClient, "lego-test")
j := NewManager(doer, server.URL)
ch := make(chan bool)
resultCh := make(chan bool)
go func() {
_, errN := manager.Nonce()
_, errN := j.Nonce()
if errN != nil {
t.Log(errN)
}
ch <- true
}()
go func() {
_, errN := manager.Nonce()
_, errN := j.Nonce()
if errN != nil {
t.Log(errN)
}
ch <- true
}()
go func() {
<-ch
<-ch
resultCh <- true
}()
select {
case <-resultCh:
case <-time.After(500 * time.Millisecond):

View file

@ -36,7 +36,6 @@ 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
@ -55,7 +54,7 @@ func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, e
options := jose.SignerOptions{
NonceSource: j.nonces,
ExtraHeaders: map[jose.HeaderKey]any{
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
}
@ -73,14 +72,12 @@ 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)
@ -90,7 +87,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]any{
ExtraHeaders: map[jose.HeaderKey]interface{}{
"kid": kid,
"url": url,
},
@ -111,7 +108,6 @@ 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()

View file

@ -9,52 +9,45 @@ 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/servermock"
"github.com/go-acme/lego/v4/platform/tester"
)
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
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)
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)
doer := sender.NewDoer(http.DefaultClient, "lego-test")
j := nonces.NewManager(doer, server.URL)
ch := make(chan bool)
resultCh := make(chan bool)
go func() {
_, errN := manager.Nonce()
_, errN := j.Nonce()
if errN != nil {
t.Log(errN)
}
ch <- true
}()
go func() {
_, errN := manager.Nonce()
_, errN := j.Nonce()
if errN != nil {
t.Log(errN)
}
ch <- true
}()
go func() {
<-ch
<-ch
resultCh <- true
}()
select {
case <-resultCh:
case <-time.After(500 * time.Millisecond):

View file

@ -27,8 +27,6 @@ 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,
@ -37,7 +35,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 any) (*http.Response, error) {
func (d *Doer) Get(url string, response interface{}) (*http.Response, error) {
req, err := d.newRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
@ -59,7 +57,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 any) (*http.Response, error) {
func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) {
req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType))
if err != nil {
return nil, err
@ -86,7 +84,7 @@ func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOpt
return req, nil
}
func (d *Doer) do(req *http.Request, response any) (*http.Response, error) {
func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) {
resp, err := d.httpClient.Do(req)
if err != nil {
return nil, err
@ -120,69 +118,31 @@ func (d *Doer) formatUserAgent() string {
}
func checkError(req *http.Request, resp *http.Response) error {
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"),
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}
}
default:
return errorDetails
}
}
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)
return nil
}

View file

@ -1,28 +1,24 @@
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.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
ua = r.Header.Get("User-Agent")
method = r.Method
}))
t.Cleanup(server.Close)
doer := NewDoer(server.Client(), "")
doer := NewDoer(http.DefaultClient, "")
testCases := []struct {
method string
@ -64,87 +60,8 @@ 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)
}

View file

@ -1,13 +1,14 @@
// Code generated by 'internal/releaser'; DO NOT EDIT.
package sender
// CODE GENERATED AUTOMATICALLY
// THIS FILE MUST NOT BE EDITED BY HAND
const (
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme/4.32.0"
ourUserAgent = "xenolf-acme/4.17.2"
// 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 = "detach"
ourUserAgentComment = "release"
)

View file

@ -3,8 +3,7 @@ package api
import (
"encoding/base64"
"errors"
"fmt"
"slices"
"net"
"time"
"github.com/go-acme/lego/v4/acme"
@ -14,15 +13,9 @@ 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://www.rfc-editor.org/rfc/rfc9773.html#section-5
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
ReplacesCertID string
}
@ -35,7 +28,18 @@ 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) {
orderReq := acme.Order{Identifiers: createIdentifiers(domains)}
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}
if opts != nil {
if !opts.NotAfter.IsZero() {
@ -49,50 +53,12 @@ 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 {
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{}, err
}
return acme.ExtendedOrder{
@ -108,7 +74,6 @@ 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
@ -124,14 +89,13 @@ 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{}, fmt.Errorf("invalid order: %w", order.Err())
return acme.ExtendedOrder{}, order.Error
}
return acme.ExtendedOrder{Order: order}, nil

View file

@ -11,51 +11,55 @@ 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, 1024)
privateKey, errK := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, errK, "Could not generate test key")
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
}
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
}
order := acme.Order{}
body, err := readSignedBody(r, privateKey)
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
}
order := acme.Order{}
err = json.Unmarshal(body, &order)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
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)
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
}
})
core, err := New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
testCases := []struct {
@ -108,7 +112,6 @@ 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

View file

@ -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://www.rfc-editor.org/rfc/rfc9773.html
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
func (c *CertificateService) GetRenewalInfo(certID string) (*http.Response, error) {
if c.core.GetDirectory().RenewalInfo == "" {
return nil, ErrNoARI

View file

@ -1,11 +1,8 @@
package api
import (
"fmt"
"net/http"
"regexp"
"strconv"
"time"
)
type service struct {
@ -26,13 +23,11 @@ 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])
}
@ -59,29 +54,3 @@ 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)
}

View file

@ -3,10 +3,8 @@ package api
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_getLink(t *testing.T) {
@ -55,38 +53,3 @@ 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)
})
}
}

View file

@ -38,7 +38,7 @@ const (
// Directory the ACME directory object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1
// - https://www.rfc-editor.org/rfc/rfc9773.html
// - https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
type Directory struct {
NewNonceURL string `json:"newNonce"`
NewAccountURL string `json:"newAccount"`
@ -74,17 +74,11 @@ 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:"-"`
}
@ -154,12 +148,6 @@ 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].
@ -197,18 +185,10 @@ 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://www.rfc-editor.org/rfc/rfc9773.html#section-5
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#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 {
@ -221,11 +201,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,omitzero"`
Expires time.Time `json:"expires,omitempty"`
// identifier (required, object):
// The identifier that the account is authorized to represent
Identifier Identifier `json:"identifier"`
Identifier Identifier `json:"identifier,omitempty"`
// challenges (required, array of objects):
// For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier.
@ -245,7 +225,6 @@ 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"
@ -272,7 +251,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,omitzero"`
Validated time.Time `json:"validated,omitempty"`
// error (optional, object):
// Error that occurred while the server was validating the challenge, if any,
@ -295,14 +274,6 @@ 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 {
@ -351,7 +322,7 @@ type Window struct {
}
// RenewalInfoResponse is the response to GET requests made the renewalInfo endpoint.
// - (4.1. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html
// - (4.1. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
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.
@ -364,11 +335,11 @@ type RenewalInfoResponse struct {
}
// RenewalInfoUpdateRequest is the JWS payload for POST requests made to the renewalInfo endpoint.
// - (4.2. RenewalInfo Objects) https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2
// - (4.2. RenewalInfo Objects) https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#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://www.rfc-editor.org/rfc/rfc9773.html#section-4.1
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#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,

View file

@ -2,15 +2,12 @@ package acme
import (
"fmt"
"strings"
)
// Errors types.
const (
errNS = "urn:ietf:params:acme:error:"
BadNonceErr = errNS + "badNonce"
AlreadyReplacedErr = errNS + "alreadyReplaced"
RateLimitedErr = errNS + "rateLimited"
errNS = "urn:ietf:params:acme:error:"
BadNonceErr = errNS + "badNonce"
)
// ProblemDetails the problem details object.
@ -28,34 +25,30 @@ 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"`
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
}
// NonceError represents the error which is returned
@ -63,31 +56,3 @@ type SubProblem struct {
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
}

View file

@ -1,12 +1,10 @@
# 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 $TARGETPLATFORM/lego /
COPY lego /
ENTRYPOINT ["/lego"]
EXPOSE 80

View file

@ -57,10 +57,8 @@ 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
certDERBlock *pem.Block
)
var certificates []*x509.Certificate
var certDERBlock *pem.Block
for {
certDERBlock, bundle = pem.Decode(bundle)
@ -73,7 +71,6 @@ func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
if err != nil {
return nil, err
}
certificates = append(certificates, cert)
}
}
@ -138,29 +135,10 @@ 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) {
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 {
var dnsNames []string
var ipAddresses []net.IP
for _, altname := range san {
if ip := net.ParseIP(altname); ip != nil {
ipAddresses = append(ipAddresses, ip)
} else {
@ -169,13 +147,12 @@ func CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) {
}
template := x509.CertificateRequest{
Subject: pkix.Name{CommonName: opts.Domain},
DNSNames: dnsNames,
EmailAddresses: opts.EmailAddresses,
IPAddresses: ipAddresses,
Subject: pkix.Name{CommonName: domain},
DNSNames: dnsNames,
IPAddresses: ipAddresses,
}
if opts.MustStaple {
if mustStaple {
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
Id: tlsFeatureExtensionOID,
Value: ocspMustStapleFeature,
@ -185,13 +162,12 @@ func CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) {
return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
}
func PEMEncode(data any) []byte {
func PEMEncode(data interface{}) []byte {
return pem.EncodeToMemory(PEMBlock(data))
}
func PEMBlock(data any) *pem.Block {
func PEMBlock(data interface{}) *pem.Block {
var pemBlock *pem.Block
switch key := data.(type) {
case *ecdsa.PrivateKey:
keyBytes, _ := x509.MarshalECPrivateKey(key)
@ -242,15 +218,15 @@ func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) {
}
func GetCertificateMainDomain(cert *x509.Certificate) (string, error) {
return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses)
return getMainDomain(cert.Subject, cert.DNSNames)
}
func GetCSRMainDomain(cert *x509.CertificateRequest) (string, error) {
return getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses)
return getMainDomain(cert.Subject, cert.DNSNames)
}
func getMainDomain(subject pkix.Name, dnsNames []string, ips []net.IP) (string, error) {
if subject.CommonName == "" && len(dnsNames) == 0 && len(ips) == 0 {
func getMainDomain(subject pkix.Name, dnsNames []string) (string, error) {
if subject.CommonName == "" && len(dnsNames) == 0 {
return "", errors.New("missing domain")
}
@ -258,11 +234,7 @@ func getMainDomain(subject pkix.Name, dnsNames []string, ips []net.IP) (string,
return subject.CommonName, nil
}
if len(dnsNames) > 0 {
return dnsNames[0], nil
}
return ips[0].String(), nil
return dnsNames[0], nil
}
func ExtractDomains(cert *x509.Certificate) []string {
@ -276,7 +248,6 @@ func ExtractDomains(cert *x509.Certificate) []string {
if sanDomain == cert.Subject.CommonName {
continue
}
domains = append(domains, sanDomain)
}
@ -328,7 +299,6 @@ 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

View file

@ -6,6 +6,7 @@ import (
"crypto/rand"
"crypto/rsa"
"encoding/pem"
"regexp"
"testing"
"time"
@ -13,13 +14,6 @@ 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")
@ -28,7 +22,7 @@ func TestGeneratePrivateKey(t *testing.T) {
}
func TestGenerateCSR(t *testing.T) {
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Error generating private key")
type expected struct {
@ -39,75 +33,55 @@ func TestGenerateCSR(t *testing.T) {
testCases := []struct {
desc string
privateKey crypto.PrivateKey
opts CSROptions
domain string
san []string
mustStaple bool
expected expected
}{
{
desc: "without SAN (nil)",
privateKey: privateKey,
opts: CSROptions{
Domain: testDomain1,
MustStaple: true,
},
expected: expected{len: 382},
domain: "lego.acme",
mustStaple: true,
expected: expected{len: 245},
},
{
desc: "without SAN (empty)",
privateKey: privateKey,
opts: CSROptions{
Domain: testDomain1,
SAN: []string{},
MustStaple: true,
},
expected: expected{len: 382},
domain: "lego.acme",
san: []string{},
mustStaple: true,
expected: expected{len: 245},
},
{
desc: "with SAN",
privateKey: privateKey,
opts: CSROptions{
Domain: testDomain1,
SAN: []string{testDomain2, testDomain3, testDomain4},
MustStaple: true,
},
expected: expected{len: 442},
domain: "lego.acme",
san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
mustStaple: true,
expected: expected{len: 296},
},
{
desc: "no domain",
privateKey: privateKey,
opts: CSROptions{
Domain: "",
MustStaple: true,
},
expected: expected{len: 359},
domain: "",
mustStaple: true,
expected: expected{len: 225},
},
{
desc: "no domain with SAN",
privateKey: privateKey,
opts: CSROptions{
Domain: "",
SAN: []string{testDomain2, testDomain3, testDomain4},
MustStaple: true,
},
expected: expected{len: 419},
domain: "",
san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
mustStaple: true,
expected: expected{len: 276},
},
{
desc: "private key nil",
privateKey: nil,
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},
domain: "fizz.buzz",
mustStaple: true,
expected: expected{error: true},
},
}
@ -115,7 +89,7 @@ func TestGenerateCSR(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
csr, err := CreateCSR(test.privateKey, test.opts)
csr, err := GenerateCSR(test.privateKey, test.domain, test.san, test.mustStaple)
if test.expected.error {
require.Error(t, err)
@ -130,17 +104,17 @@ func TestGenerateCSR(t *testing.T) {
}
func TestPEMEncode(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 1024)
buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
reader := MockRandReader{b: buf}
key, err := rsa.GenerateKey(reader, 32)
require.NoError(t, err, "Error generating private key")
data := PEMEncode(key)
require.NotNil(t, data)
p, rest := pem.Decode(data)
assert.Equal(t, "RSA PRIVATE KEY", p.Type)
assert.Empty(t, rest)
assert.Empty(t, p.Headers)
exp := regexp.MustCompile(`^-----BEGIN RSA PRIVATE KEY-----\s+\S{60,}\s+-----END RSA PRIVATE KEY-----\s+`)
assert.Regexp(t, exp, string(data))
}
func TestParsePEMCertificate(t *testing.T) {
@ -175,13 +149,10 @@ func TestParsePEMPrivateKey(t *testing.T) {
pemPrivateKey := PEMEncode(privateKey)
// Decoding a key should work and create an identical RSA key to the original,
// ignoring precomputed values.
// Decoding a key should work and create an identical key to the original
decoded, err := ParsePEMPrivateKey(pemPrivateKey)
require.NoError(t, err)
decodedRsaPrivateKey := decoded.(*rsa.PrivateKey)
require.True(t, decodedRsaPrivateKey.Equal(privateKey))
assert.Equal(t, decoded, privateKey)
// Decoding a PEM block that doesn't contain a private key should error
_, err = ParsePEMPrivateKey(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE"}))
@ -195,3 +166,11 @@ 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)
}

View file

@ -7,10 +7,19 @@ import (
"github.com/go-acme/lego/v4/log"
)
const (
// overallRequestLimit is the overall number of request per second
// limited on the "new-reg", "new-authz" and "new-cert" endpoints.
// From the documentation the limitation is 20 requests per second,
// but using 20 as value doesn't work but 18 do.
// https://letsencrypt.org/docs/rate-limits/
overallRequestLimit = 18
)
func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) {
resc, errc := make(chan acme.Authorization), make(chan domainError)
delay := time.Second / time.Duration(c.overallRequestLimit)
delay := time.Second / overallRequestLimit
for _, authzURL := range order.Authorizations {
time.Sleep(delay)
@ -29,7 +38,6 @@ func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authoriz
var responses []acme.Authorization
failures := newObtainError()
for range len(order.Authorizations) {
select {
case res := <-resc:
@ -53,7 +61,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: %v", authzURL, err)
log.Infof("Unable to get the authorization for: %s", authzURL)
continue
}
@ -63,7 +71,6 @@ 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)
}

View file

@ -22,17 +22,6 @@ import (
"golang.org/x/net/idna"
)
const (
// DefaultOverallRequestLimit is the overall number of request per second
// limited on the "new-reg", "new-authz" and "new-cert" endpoints.
// From the documentation the limitation is 20 requests per second,
// but using 20 as value doesn't work but 18 do.
// https://letsencrypt.org/docs/rate-limits/
// ZeroSSL has a limit of 7.
// https://help.zerossl.com/hc/en-us/articles/17864245480093-Advantages-over-Using-Let-s-Encrypt#h_01HT4Z1JCJFJQFJ1M3P7S085Q9
DefaultOverallRequestLimit = 18
)
// maxBodySize is the maximum size of body that we will read.
const maxBodySize = 1024 * 1024
@ -65,26 +54,18 @@ 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
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
Domains []string
PrivateKey crypto.PrivateKey
MustStaple bool
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://www.rfc-editor.org/rfc/rfc9773.html#section-5
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
ReplacesCertID string
}
@ -97,23 +78,14 @@ type ObtainRequest struct {
type ObtainForCSRRequest struct {
CSR *x509.CertificateRequest
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
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://www.rfc-editor.org/rfc/rfc9773.html#section-5
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
ReplacesCertID string
}
@ -122,34 +94,24 @@ type resolver interface {
}
type CertifierOptions struct {
KeyType certcrypto.KeyType
Timeout time.Duration
OverallRequestLimit int
DisableCommonName bool
KeyType certcrypto.KeyType
Timeout time.Duration
}
// Certifier A service to obtain/renew/revoke certificates.
type Certifier struct {
core *api.Core
resolver resolver
options CertifierOptions
overallRequestLimit int
core *api.Core
resolver resolver
options CertifierOptions
}
// NewCertifier creates a Certifier.
func NewCertifier(core *api.Core, resolver resolver, options CertifierOptions) *Certifier {
c := &Certifier{
return &Certifier{
core: core,
resolver: resolver,
options: options,
}
c.overallRequestLimit = options.OverallRequestLimit
if c.overallRequestLimit <= 0 {
c.overallRequestLimit = DefaultOverallRequestLimit
}
return c
}
// Obtain tries to obtain a single certificate using all domains passed into it.
@ -172,7 +134,6 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
orderOpts := &api.OrderOptions{
NotBefore: request.NotBefore,
NotAfter: request.NotAfter,
Profile: request.Profile,
ReplacesCertID: request.ReplacesCertID,
}
@ -198,8 +159,7 @@ 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)
cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain)
if err != nil {
for _, auth := range authz {
failures.Add(challenge.GetTargetedDomain(auth), err)
@ -240,7 +200,6 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
orderOpts := &api.OrderOptions{
NotBefore: request.NotBefore,
NotAfter: request.NotAfter,
Profile: request.Profile,
ReplacesCertID: request.ReplacesCertID,
}
@ -266,13 +225,7 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := newObtainError()
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)
cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, nil, request.PreferredChain)
if err != nil {
for _, auth := range authz {
failures.Add(challenge.GetTargetedDomain(auth), err)
@ -291,12 +244,9 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
return cert, failures.Join()
}
func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, request ObtainRequest) (*Resource, error) {
privateKey := request.PrivateKey
func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) {
if privateKey == nil {
var err error
privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType)
if err != nil {
return nil, err
@ -304,7 +254,7 @@ func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, requ
}
commonName := ""
if len(domains[0]) <= 64 && !c.options.DisableCommonName {
if len(domains[0]) <= 64 {
commonName = domains[0]
}
@ -326,19 +276,13 @@ func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, requ
}
}
csrOptions := certcrypto.CSROptions{
Domain: commonName,
SAN: san,
MustStaple: request.MustStaple,
EmailAddresses: request.EmailAddresses,
}
csr, err := certcrypto.CreateCSR(privateKey, csrOptions)
// TODO: should the CSR be customizable?
csr, err := certcrypto.GenerateCSR(privateKey, commonName, san, mustStaple)
if err != nil {
return nil, err
}
return c.getForCSR(domains, order, request.Bundle, csr, certcrypto.PEMEncode(privateKey), request.PreferredChain)
return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey), preferredChain)
}
func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) {
@ -471,15 +415,11 @@ 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
Profile string
Bundle bool
PreferredChain string
AlwaysDeactivateAuthorizations bool
// Not supported for CSR request.
MustStaple bool
EmailAddresses []string
MustStaple bool
}
// Renew takes a Resource and tries to renew the certificate.
@ -492,7 +432,6 @@ 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{
@ -546,7 +485,6 @@ 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
}
@ -572,8 +510,6 @@ 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
}
@ -712,7 +648,7 @@ func checkOrderStatus(order acme.ExtendedOrder) (bool, error) {
case acme.StatusValid:
return true, nil
case acme.StatusInvalid:
return false, fmt.Errorf("invalid order: %w", order.Err())
return false, order.Error
default:
return false, nil
}
@ -725,7 +661,6 @@ 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 {
@ -734,6 +669,5 @@ func sanitizeDomain(domains []string) []string {
sanitizedDomains = append(sanitizedDomains, sanitizedDomain)
}
}
return sanitizedDomains
}

View file

@ -3,6 +3,7 @@ package certificate
import (
"crypto/rand"
"crypto/rsa"
"encoding/pem"
"fmt"
"net/http"
"testing"
@ -11,7 +12,6 @@ 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,14 +175,20 @@ Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
`
func Test_checkResponse(t *testing.T) {
server := tester.MockACMEServer().
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
BuildHTTPS(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
}
})
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)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
@ -190,7 +196,7 @@ func Test_checkResponse(t *testing.T) {
order := acme.ExtendedOrder{
Order: acme.Order{
Status: acme.StatusValid,
Certificate: server.URL + "/certificate",
Certificate: apiURL + "/certificate",
},
}
certRes := &Resource{}
@ -199,7 +205,7 @@ func Test_checkResponse(t *testing.T) {
require.NoError(t, err)
assert.True(t, valid)
assert.NotNil(t, certRes)
assert.Empty(t, certRes.Domain)
assert.Equal(t, "", certRes.Domain)
assert.Contains(t, certRes.CertStableURL, "/certificate")
assert.Contains(t, certRes.CertURL, "/certificate")
assert.Nil(t, certRes.CSR)
@ -209,14 +215,30 @@ func Test_checkResponse(t *testing.T) {
}
func Test_checkResponse_issuerRelUp(t *testing.T) {
server := tester.MockACMEServer().
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
BuildHTTPS(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
}
})
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)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
@ -224,7 +246,7 @@ func Test_checkResponse_issuerRelUp(t *testing.T) {
order := acme.ExtendedOrder{
Order: acme.Order{
Status: acme.StatusValid,
Certificate: server.URL + "/certificate",
Certificate: apiURL + "/certificate",
},
}
certRes := &Resource{}
@ -233,7 +255,7 @@ func Test_checkResponse_issuerRelUp(t *testing.T) {
require.NoError(t, err)
assert.True(t, valid)
assert.NotNil(t, certRes)
assert.Empty(t, certRes.Domain)
assert.Equal(t, "", certRes.Domain)
assert.Contains(t, certRes.CertStableURL, "/certificate")
assert.Contains(t, certRes.CertURL, "/certificate")
assert.Nil(t, certRes.CSR)
@ -243,14 +265,20 @@ func Test_checkResponse_issuerRelUp(t *testing.T) {
}
func Test_checkResponse_no_bundle(t *testing.T) {
server := tester.MockACMEServer().
Route("POST /certificate", servermock.RawStringResponse(certResponseMock)).
BuildHTTPS(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
}
})
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)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
@ -258,7 +286,7 @@ func Test_checkResponse_no_bundle(t *testing.T) {
order := acme.ExtendedOrder{
Order: acme.Order{
Status: acme.StatusValid,
Certificate: server.URL + "/certificate",
Certificate: apiURL + "/certificate",
},
}
certRes := &Resource{}
@ -267,7 +295,7 @@ func Test_checkResponse_no_bundle(t *testing.T) {
require.NoError(t, err)
assert.True(t, valid)
assert.NotNil(t, certRes)
assert.Empty(t, certRes.Domain)
assert.Equal(t, "", certRes.Domain)
assert.Contains(t, certRes.CertStableURL, "/certificate")
assert.Contains(t, certRes.CertURL, "/certificate")
assert.Nil(t, certRes.CSR)
@ -277,21 +305,30 @@ func Test_checkResponse_no_bundle(t *testing.T) {
}
func Test_checkResponse_alternate(t *testing.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, apiURL := tester.SetupFakeAPI(t)
servermock.RawStringResponse(certResponseMock).ServeHTTP(rw, req)
})).
Route("/certificate/1", servermock.RawStringResponse(certResponseMock2)).
BuildHTTPS(t)
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
}
})
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)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
@ -299,7 +336,7 @@ func Test_checkResponse_alternate(t *testing.T) {
order := acme.ExtendedOrder{
Order: acme.Order{
Status: acme.StatusValid,
Certificate: server.URL + "/certificate",
Certificate: apiURL + "/certificate",
},
}
certRes := &Resource{
@ -321,76 +358,37 @@ func Test_checkResponse_alternate(t *testing.T) {
}
func Test_Get(t *testing.T) {
server := tester.MockACMEServer().
Route("POST /acme/cert/test-cert", servermock.RawStringResponse(certResponseMock)).
BuildHTTPS(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
}
})
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)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
certRes, err := certifier.Get(server.URL+"/acme/cert/test-cert", true)
certRes, err := certifier.Get(apiURL+"/acme/cert/test-cert", true)
require.NoError(t, err)
assert.NotNil(t, certRes)
assert.Equal(t, "acme.wtf", certRes.Domain)
assert.Equal(t, server.URL+"/acme/cert/test-cert", certRes.CertStableURL)
assert.Equal(t, server.URL+"/acme/cert/test-cert", certRes.CertURL)
assert.Equal(t, apiURL+"/acme/cert/test-cert", certRes.CertStableURL)
assert.Equal(t, apiURL+"/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
}

View file

@ -11,7 +11,6 @@ import (
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
)
// RenewalInfoRequest contains the necessary renewal information.
@ -26,15 +25,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://www.rfc-editor.org/rfc/rfc9773.html#section-4.2
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#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 RFC 9773.
// This method implements the RECOMMENDED algorithm described in draft-ietf-acme-ari.
//
// - (4.1-11. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html
// - (4.1-11. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.Duration) *time.Time {
// Explicitly convert all times to UTC.
now = now.UTC()
@ -42,11 +41,9 @@ func (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.D
end := r.SuggestedWindow.End.UTC()
// Select a uniform random time within the suggested window.
rt := start
if window := end.Sub(start); window > 0 {
randomDuration := time.Duration(rand.Int63n(int64(window)))
rt = rt.Add(randomDuration)
}
window := end.Sub(start)
randomDuration := time.Duration(rand.Int63n(int64(window)))
rt := start.Add(randomDuration)
// If the selected time is in the past, attempt renewal immediately.
if rt.Before(now) {
@ -72,7 +69,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://www.rfc-editor.org/rfc/rfc9773.html
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse, error) {
certID, err := MakeARICertID(req.Cert)
if err != nil {
@ -86,23 +83,22 @@ 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 = api.ParseRetryAfter(retry)
info.RetryAfter, err = time.ParseDuration(retry + "s")
if err != nil {
return nil, fmt.Errorf("failed to parse Retry-After header: %w", err)
return nil, err
}
}
return &info, nil
}
// MakeARICertID constructs a certificate identifier as described in RFC 9773, section 4.1.
// MakeARICertID constructs a certificate identifier as described in draft-ietf-acme-ari-03, section 4.1.
func MakeARICertID(leaf *x509.Certificate) (string, error) {
if leaf == nil {
return "", errors.New("leaf certificate is nil")

View file

@ -11,7 +11,6 @@ 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"
)
@ -43,24 +42,31 @@ func TestCertifier_GetRenewalInfo(t *testing.T) {
require.NoError(t, err)
// Test with a fake API.
server := tester.MockACMEServer().
Route("GET /renewalInfo/"+ariLeafCertID,
servermock.RawStringResponse(`{
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(`{
"suggestedWindow": {
"start": "2020-03-17T17:51:09Z",
"end": "2020-03-17T18:21:09Z"
},
"explanationUrl": "https://aricapable.ca.example/docs/renewal-advice/"
"explanationUrl": "https://aricapable.ca/docs/renewal-advice/"
}
}`).
WithHeader("Content-Type", "application/json").
WithHeader("Retry-After", "21600")).
BuildHTTPS(t)
}`))
require.NoError(t, wErr)
})
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)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
@ -70,46 +76,10 @@ 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.example/docs/renewal-advice/", ri.ExplanationURL)
assert.Equal(t, "https://aricapable.ca/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)
@ -118,23 +88,24 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
require.NoError(t, err, "Could not generate test key")
testCases := []struct {
desc string
timeout time.Duration
request RenewalInfoRequest
handler http.HandlerFunc
desc string
httpClient *http.Client
request RenewalInfoRequest
handler http.HandlerFunc
}{
{
desc: "API timeout",
timeout: 500 * time.Millisecond, // HTTP client that times out after 500ms.
request: RenewalInfoRequest{leaf},
desc: "API timeout",
httpClient: &http.Client{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",
request: RenewalInfoRequest{leaf},
desc: "API error",
httpClient: http.DefaultClient,
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)
@ -146,17 +117,10 @@ func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
server := tester.MockACMEServer().
Route("GET /renewalInfo/"+ariLeafCertID, test.handler).
BuildHTTPS(t)
mux, apiURL := tester.SetupFakeAPI(t)
mux.HandleFunc("/renewalInfo/"+ariLeafCertID, test.handler)
client := server.Client()
if test.timeout != 0 {
client.Timeout = test.timeout
}
core, err := api.New(client, "lego-test", server.URL+"/dir", "", key)
core, err := api.New(test.httpClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})

View file

@ -40,6 +40,5 @@ func GetTargetedDomain(authz acme.Authorization) string {
if authz.Wildcard {
return "*." + authz.Identifier.Value
}
return authz.Identifier.Value
}

View file

@ -40,7 +40,6 @@ func CondOption(condition bool, opt ChallengeOption) ChallengeOption {
return nil
}
}
return opt
}
@ -119,7 +118,6 @@ 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()
@ -136,7 +134,6 @@ 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 {
@ -144,7 +141,6 @@ func (c *Challenge) Solve(authz acme.Authorization) error {
}
chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng)
}
@ -169,7 +165,6 @@ func (c *Challenge) Sequential() (bool, time.Duration) {
if p, ok := c.provider.(sequential); ok {
return ok, p.Sequential()
}
return false, 0
}
@ -178,7 +173,6 @@ 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)

View file

@ -12,14 +12,9 @@ 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
}

View file

@ -1,4 +1,4 @@
package manual
package dns01
import (
"io"
@ -10,7 +10,6 @@ import (
func TestDNSProviderManual(t *testing.T) {
backupStdin := os.Stdin
defer func() { os.Stdin = backupStdin }()
testCases := []struct {
@ -31,10 +30,9 @@ func TestDNSProviderManual(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
file, err := os.CreateTemp(t.TempDir(), "lego_test")
file, err := os.CreateTemp("", "lego_test")
require.NoError(t, err)
t.Cleanup(func() { _ = file.Close() })
defer func() { _ = os.Remove(file.Name()) }()
_, err = file.WriteString(test.input)
require.NoError(t, err)
@ -44,7 +42,7 @@ func TestDNSProviderManual(t *testing.T) {
os.Stdin = file
manualProvider, err := NewDNSProvider()
manualProvider, err := NewDNSProviderManual()
require.NoError(t, err)
err = manualProvider.Present("example.com", "", "")

View file

@ -4,6 +4,7 @@ import (
"crypto/rand"
"crypto/rsa"
"errors"
"net/http"
"testing"
"time"
@ -11,8 +12,6 @@ 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"
)
@ -33,12 +32,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) {
server := tester.MockACMEServer().BuildHTTPS(t)
_, apiURL := tester.SetupFakeAPI(t)
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err)
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
testCases := []struct {
@ -115,16 +114,12 @@ func TestChallenge_PreSolve(t *testing.T) {
}
func TestChallenge_Solve(t *testing.T) {
useAsNameserver(t, dnsmock.NewServer().
Query("_acme-challenge.example.com. CNAME", dnsmock.Noop).
Build(t))
_, apiURL := tester.SetupFakeAPI(t)
server := tester.MockACMEServer().BuildHTTPS(t)
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err)
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
testCases := []struct {
@ -184,7 +179,6 @@ 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{
@ -207,12 +201,12 @@ func TestChallenge_Solve(t *testing.T) {
}
func TestChallenge_CleanUp(t *testing.T) {
server := tester.MockACMEServer().BuildHTTPS(t)
_, apiURL := tester.SetupFakeAPI(t)
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err)
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
testCases := []struct {
@ -287,55 +281,3 @@ 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)
}

View file

@ -1,4 +1,4 @@
domain example.com
domain company.com
nameserver 10.200.3.249
nameserver 10.200.3.250:5353
nameserver 2001:4860:4860::8844

View file

@ -1,16 +1,12 @@
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 {
return dns.Fqdn(name)
n := len(name)
if n == 0 || name[n-1] == '.' {
return name
}
return name + "."
}
// UnFqdn converts the fqdn into a name removing the trailing dot.
@ -19,36 +15,5 @@ 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
}
}
}
}

View file

@ -1,12 +1,39 @@
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
@ -35,103 +62,3 @@ 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)
})
}
}

View file

@ -1,81 +0,0 @@
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()})
}

View file

@ -16,7 +16,10 @@ import (
const defaultResolvConf = "/etc/resolv.conf"
var fqdnSoaCache = &sync.Map{}
var (
fqdnSoaCache = map[string]*soaCacheEntry{}
muFqdnSoaCache sync.Mutex
)
var defaultNameservers = []string{
"google-public-dns-a.google.com:53",
@ -48,11 +51,9 @@ func (cache *soaCacheEntry) isExpired() bool {
// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
func ClearFqdnCache() {
// TODO(ldez): use `fqdnSoaCache.Clear()` when updating to go1.23
fqdnSoaCache.Range(func(k, v any) bool {
fqdnSoaCache.Delete(k)
return true
})
muFqdnSoaCache.Lock()
fqdnSoaCache = map[string]*soaCacheEntry{}
muFqdnSoaCache.Unlock()
}
func AddDNSTimeout(timeout time.Duration) ChallengeOption {
@ -81,7 +82,6 @@ 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 {
@ -90,7 +90,6 @@ func ParseNameservers(servers []string) []string {
resolvers = append(resolvers, resolver)
}
}
return resolvers
}
@ -134,7 +133,6 @@ func FindPrimaryNsByFqdnCustom(fqdn string, nameservers []string) (string, error
if err != nil {
return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err)
}
return soa.primaryNs, nil
}
@ -151,18 +149,16 @@ func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) {
if err != nil {
return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err)
}
return soa.zone, nil
}
func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
muFqdnSoaCache.Lock()
defer muFqdnSoaCache.Unlock()
// Do we have it cached and is it still fresh?
entAny, ok := fqdnSoaCache.Load(fqdn)
if ok && entAny != nil {
ent, ok1 := entAny.(*soaCacheEntry)
if ok1 && !ent.isExpired() {
return ent, nil
}
if ent := fqdnSoaCache[fqdn]; ent != nil && !ent.isExpired() {
return ent, nil
}
ent, err := fetchSoaByFqdn(fqdn, nameservers)
@ -170,18 +166,18 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error)
return nil, err
}
fqdnSoaCache.Store(fqdn, ent)
fqdnSoaCache[fqdn] = ent
return ent, nil
}
func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
var (
err error
r *dns.Msg
)
var err error
var r *dns.Msg
labelIndexes := dns.Split(fqdn)
for _, index := range labelIndexes {
domain := fqdn[index:]
for domain := range DomainsSeq(fqdn) {
r, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
if err != nil {
continue
@ -235,11 +231,9 @@ func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (
return nil, &DNSError{Message: "empty list of nameservers"}
}
var (
r *dns.Msg
err error
errAll error
)
var r *dns.Msg
var err error
var errAll error
for _, ns := range nameservers {
r, err = sendDNSQuery(m, ns)
@ -272,7 +266,6 @@ 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}

View file

@ -5,237 +5,138 @@ 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 Test_lookupNameserversOK(t *testing.T) {
func TestLookupNameserversOK(t *testing.T) {
testCases := []struct {
desc string
fakeDNSServer *dnsmock.Builder
fqdn string
expected []string
fqdn string
nss []string
}{
{
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: "en.wikipedia.org.",
nss: []string{"ns0.wikimedia.org.", "ns1.wikimedia.org.", "ns2.wikimedia.org."},
},
{
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: "www.google.com.",
nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
},
{
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."},
fqdn: "physics.georgetown.edu.",
nss: []string{"ns4.georgetown.edu.", "ns5.georgetown.edu.", "ns6.georgetown.edu."},
},
}
for _, test := range testCases {
t.Run(test.fqdn, func(t *testing.T) {
useAsNameserver(t, test.fakeDNSServer.Build(t))
t.Parallel()
nss, err := lookupNameservers(test.fqdn)
require.NoError(t, err)
sort.Strings(nss)
sort.Strings(test.expected)
sort.Strings(test.nss)
assert.Equal(t, test.expected, nss)
assert.EqualValues(t, test.nss, nss)
})
}
}
func Test_lookupNameserversErr(t *testing.T) {
func TestLookupNameserversErr(t *testing.T) {
testCases := []struct {
desc string
fqdn string
fakeDNSServer *dnsmock.Builder
error string
desc string
fqdn string
error string
}{
{
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",
desc: "invalid tld",
fqdn: "_null.n0n0.",
error: "could not find zone",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
useAsNameserver(t, test.fakeDNSServer.Build(t))
t.Parallel()
_, err := lookupNameservers(test.fqdn)
require.Error(t, err)
assert.EqualError(t, err, test.error)
assert.Contains(t, err.Error(), test.error)
})
}
}
type lookupSoaByFqdnTestCase struct {
var findXByFqdnTestCases = []struct {
desc string
fqdn string
zone string
primaryNs string
nameservers []string
expectedError string
}
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",
},
}
}{
{
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 TestFindZoneByFqdnCustom(t *testing.T) {
for _, test := range lookupSoaByFqdnTestCases(t) {
for _, test := range findXByFqdnTestCases {
t.Run(test.desc, func(t *testing.T) {
ClearFqdnCache()
@ -252,7 +153,7 @@ func TestFindZoneByFqdnCustom(t *testing.T) {
}
func TestFindPrimaryNsByFqdnCustom(t *testing.T) {
for _, test := range lookupSoaByFqdnTestCases(t) {
for _, test := range findXByFqdnTestCases {
t.Run(test.desc, func(t *testing.T) {
ClearFqdnCache()
@ -268,7 +169,7 @@ func TestFindPrimaryNsByFqdnCustom(t *testing.T) {
}
}
func Test_getNameservers_ResolveConfServers(t *testing.T) {
func TestResolveConfServers(t *testing.T) {
testCases := []struct {
fixture string
expected []string

View file

@ -4,15 +4,10 @@ import (
"fmt"
"net"
"strings"
"time"
"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)
@ -28,53 +23,23 @@ func WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption {
}
}
// DisableCompletePropagationRequirement obsolete.
//
// Deprecated: use DisableAuthoritativeNssPropagationRequirement instead.
func DisableCompletePropagationRequirement() ChallengeOption {
return DisableAuthoritativeNssPropagationRequirement()
}
func DisableAuthoritativeNssPropagationRequirement() ChallengeOption {
return func(chlg *Challenge) error {
chlg.preCheck.requireAuthoritativeNssPropagation = false
chlg.preCheck.requireCompletePropagation = false
return nil
}
}
func RecursiveNSsPropagationRequirement() ChallengeOption {
return func(chlg *Challenge) error {
chlg.preCheck.requireRecursiveNssPropagation = true
return nil
}
}
func PropagationWait(wait time.Duration, skipCheck bool) ChallengeOption {
return WrapPreCheck(func(domain, fqdn, value string, check PreCheckFunc) (bool, error) {
time.Sleep(wait)
if skipCheck {
return true, nil
}
return check(fqdn, value)
})
}
type preCheck struct {
// checks DNS propagation before notifying ACME that the DNS challenge is ready.
checkFunc WrapPreCheckFunc
// require the TXT record to be propagated to all authoritative name servers
requireAuthoritativeNssPropagation bool
// require the TXT record to be propagated to all recursive name servers
requireRecursiveNssPropagation bool
requireCompletePropagation bool
}
func newPreCheck() preCheck {
return preCheck{
requireAuthoritativeNssPropagation: true,
requireCompletePropagation: true,
}
}
@ -88,48 +53,32 @@ func (p preCheck) call(domain, fqdn, value string) (bool, error) {
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) {
// Initial attempt to resolve at the recursive NS (require to get CNAME)
// Initial attempt to resolve at the recursive NS
r, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameservers, true)
if err != nil {
return false, fmt.Errorf("initial recursive nameserver: %w", err)
return false, err
}
if !p.requireCompletePropagation {
return true, nil
}
if r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn)
}
if p.requireRecursiveNssPropagation {
_, err = checkNameserversPropagation(fqdn, value, recursiveNameservers, false)
if err != nil {
return false, fmt.Errorf("recursive nameservers: %w", err)
}
}
if !p.requireAuthoritativeNssPropagation {
return true, nil
}
authoritativeNss, err := lookupNameservers(fqdn)
if err != nil {
return false, err
}
found, err := checkNameserversPropagation(fqdn, value, authoritativeNss, true)
if err != nil {
return found, fmt.Errorf("authoritative nameservers: %w", err)
}
return found, nil
return checkAuthoritativeNss(fqdn, value, authoritativeNss)
}
// checkNameserversPropagation queries each of the given nameservers for the expected TXT record.
func checkNameserversPropagation(fqdn, value string, nameservers []string, addPort bool) (bool, error) {
// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
for _, ns := range nameservers {
if addPort {
ns = net.JoinHostPort(ns, defaultNameserverPort)
}
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, false)
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false)
if err != nil {
return false, err
}
@ -141,11 +90,9 @@ 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

View file

@ -3,73 +3,40 @@ 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 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),
)
func TestCheckDNSPropagation(t *testing.T) {
testCases := []struct {
desc string
fqdn string
value string
expectedError string
desc string
fqdn string
value string
expectError bool
}{
{
desc: "success",
fqdn: "example.com.",
value: "four",
fqdn: "postman-echo.com.",
value: "postman-domain-verification=c85de626cb79d941310696e06558e2e790223802f3697dfbdcaf65510152d52c",
},
{
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",
desc: "no TXT record",
fqdn: "acme-staging.api.letsencrypt.org.",
value: "fe01=",
expectError: true,
},
}
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.expectedError != "" {
assert.ErrorContainsf(t, err, test.expectedError, "PreCheckDNS must fail for %s", test.fqdn)
if test.expectError {
assert.Errorf(t, err, "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)
@ -79,67 +46,69 @@ func Test_preCheck_checkDNSPropagation(t *testing.T) {
}
}
func Test_checkNameserversPropagation_authoritativeNss(t *testing.T) {
func TestCheckAuthoritativeNss(t *testing.T) {
testCases := []struct {
desc string
fqdn, value string
fakeDNSServer *dnsmock.Builder
expectedError string
desc string
fqdn, value string
ns []string
expected bool
}{
{
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: "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: "No TXT RR",
// NS: ns2.google.com.
fqdn: "ns1.google.com.",
value: "fe01=",
fakeDNSServer: dnsmock.NewServer().
Query("ns1.google.com.", dnsmock.Noop),
expectedError: "did not return the expected TXT record [fqdn: ns1.google.com., value: fe01=]: ",
fqdn: "ns1.google.com.",
ns: []string{"ns2.google.com."},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
ClearFqdnCache()
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)
}
ok, _ := checkAuthoritativeNss(test.fqdn, test.value, test.ns)
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",
fqdn: "ns1.google.com.",
value: "fe01=",
ns: []string{"ns2.google.com."},
error: "did not return the expected TXT record",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
ClearFqdnCache()
_, err := checkAuthoritativeNss(test.fqdn, test.value, test.ns)
require.Error(t, err)
assert.Contains(t, err.Error(), test.error)
})
}
}

View file

@ -3,7 +3,6 @@ package http01
import (
"fmt"
"net/http"
"net/netip"
"strings"
)
@ -55,10 +54,10 @@ func (m *hostMatcher) name() string {
}
func (m *hostMatcher) matches(r *http.Request, domain string) bool {
return matchDomain(r.Host, domain)
return strings.HasPrefix(r.Host, domain)
}
// arbitraryMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name.
// hostMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name.
type arbitraryMatcher string
func (m arbitraryMatcher) name() string {
@ -66,7 +65,7 @@ func (m arbitraryMatcher) name() string {
}
func (m arbitraryMatcher) matches(r *http.Request, domain string) bool {
return matchDomain(r.Header.Get(m.name()), domain)
return strings.HasPrefix(r.Header.Get(m.name()), domain)
}
// forwardedMatcher checks whether the Forwarded header contains a "host" element starting with a domain name.
@ -88,8 +87,7 @@ func (m *forwardedMatcher) matches(r *http.Request, domain string) bool {
}
host := fwds[0]["host"]
return matchDomain(host, domain)
return strings.HasPrefix(host, domain)
}
// parsing requires some form of state machine.
@ -100,7 +98,6 @@ 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])
@ -112,7 +109,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
pos = i
inquote = false
}
continue
}
@ -121,7 +117,6 @@ 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
@ -138,10 +133,11 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
case r == ',': // end of forwarded-element
if key != "" {
val = s[pos:i]
if val == "" {
val = s[pos:i]
}
cur[key] = val
}
elements = append(elements, cur)
cur = make(map[string]string)
key = ""
@ -164,14 +160,11 @@ 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
}
@ -186,19 +179,9 @@ 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)
}

View file

@ -1,15 +1,13 @@
package http01
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_parseForwardedHeader(t *testing.T) {
func TestParseForwardedHeader(t *testing.T) {
testCases := []struct {
name string
input string
@ -77,7 +75,7 @@ func Test_parseForwardedHeader(t *testing.T) {
actual, err := parseForwardedHeader(test.input)
if test.err == "" {
require.NoError(t, err)
assert.Equal(t, test.want, actual)
assert.EqualValues(t, test.want, actual)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), test.err)
@ -85,54 +83,3 @@ func Test_parseForwardedHeader(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))
})
}
}

View file

@ -2,7 +2,6 @@ package http01
import (
"fmt"
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
@ -12,16 +11,6 @@ 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
@ -31,24 +20,14 @@ type Challenge struct {
core *api.Core
validate ValidateFunc
provider challenge.Provider
delay time.Duration
}
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {
chlg := &Challenge{
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
return &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) {
@ -74,7 +53,6 @@ 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 {
@ -82,11 +60,6 @@ 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)
}

View file

@ -44,7 +44,6 @@ 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)
@ -57,9 +56,7 @@ func (s *ProviderServer) Present(domain, token, keyAuth string) error {
}
s.done = make(chan bool)
go s.serve(domain, token, keyAuth)
return nil
}
@ -72,11 +69,8 @@ func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {
if s.listener == nil {
return nil
}
s.listener.Close()
<-s.done
return nil
}
@ -113,24 +107,19 @@ 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)
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
} 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
}
}
})
@ -144,6 +133,5 @@ 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
}

View file

@ -67,7 +67,7 @@ func TestProviderServer_GetAddress(t *testing.T) {
}
func TestChallenge(t *testing.T) {
server := tester.MockACMEServer().BuildHTTPS(t)
_, apiURL := tester.SetupFakeAPI(t)
providerServer := NewProviderServer("", "23457")
@ -88,7 +88,6 @@ func TestChallenge(t *testing.T) {
if err != nil {
return err
}
bodyStr := string(body)
if bodyStr != chlng.KeyAuthorization {
@ -98,10 +97,10 @@ func TestChallenge(t *testing.T) {
return nil
}
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(core, validate, providerServer)
@ -124,7 +123,7 @@ func TestChallengeUnix(t *testing.T) {
t.Skip("only for UNIX systems")
}
server := tester.MockACMEServer().BuildHTTPS(t)
_, apiURL := tester.SetupFakeAPI(t)
dir := t.TempDir()
t.Cleanup(func() { _ = os.RemoveAll(dir) })
@ -158,7 +157,6 @@ func TestChallengeUnix(t *testing.T) {
if err != nil {
return err
}
bodyStr := string(body)
if bodyStr != chlng.KeyAuthorization {
@ -168,10 +166,10 @@ func TestChallengeUnix(t *testing.T) {
return nil
}
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(core, validate, providerServer)
@ -190,12 +188,12 @@ func TestChallengeUnix(t *testing.T) {
}
func TestChallengeInvalidPort(t *testing.T) {
server := tester.MockACMEServer().BuildHTTPS(t)
_, apiURL := tester.SetupFakeAPI(t)
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 128)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
validate := func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }
@ -226,7 +224,6 @@ 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 != "" {
@ -374,7 +371,7 @@ func TestChallengeWithProxy(t *testing.T) {
func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectError bool) {
t.Helper()
server := tester.MockACMEServer().BuildHTTPS(t)
_, apiURL := tester.SetupFakeAPI(t)
providerServer := NewProviderServer("localhost", "23457")
if header != nil {
@ -388,7 +385,6 @@ func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectErro
if err != nil {
return err
}
header.update(req)
extra.update(req)
@ -406,7 +402,6 @@ func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectErro
if err != nil {
return err
}
bodyStr := string(body)
if bodyStr != chlng.KeyAuthorization {
@ -416,10 +411,10 @@ func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectErro
return nil
}
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(core, validate, providerServer)

View file

@ -3,8 +3,6 @@ package resolver
import (
"bytes"
"fmt"
"maps"
"slices"
"sort"
)
@ -18,16 +16,10 @@ 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))
}

View file

@ -1,70 +0,0 @@
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))
})
}
}

View file

@ -50,14 +50,11 @@ func NewProber(solverManager *SolverManager) *Prober {
func (p *Prober) Solve(authorizations []acme.Authorization) error {
failures := make(obtainError)
var (
authSolvers []*selectedAuthSolver
authSolversSequential []*selectedAuthSolver
)
var authSolvers []*selectedAuthSolver
var 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 {
@ -93,88 +90,47 @@ 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
}
if _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok || chlg.Token == "" {
// Clean challenge
cleanUp(authSolver.solver, authSolver.authz)
// 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)
}
delete(uniq, authSolver.authz.Identifier.Value+chlg.Token)
} else {
log.Infof("acme: duplicate token for %q (DNS-01); skipping cleanup.", authSolver.authz.Identifier.Value)
if len(authSolvers)-1 > i {
solvr := authSolver.solver.(sequential)
_, interval := solvr.Sequential()
log.Infof("sequence: wait for %s", interval)
time.Sleep(interval)
}
}
}
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 {
@ -186,16 +142,6 @@ 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)
}
}()
@ -203,7 +149,6 @@ 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
@ -220,7 +165,6 @@ 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)

View file

@ -1,7 +1,6 @@
package resolver
import (
"fmt"
"time"
"github.com/go-acme/lego/v4/acme"
@ -12,68 +11,34 @@ 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{
Wildcard: wildcard,
Status: status,
Expires: time.Now(),
Status: status,
Expires: time.Now(),
Identifier: acme.Identifier{
Type: "dns",
Type: challenge.HTTP01.String(),
Value: domain,
},
Challenges: chlgs,
Challenges: []acme.Challenge{
{
Type: challenge.HTTP01.String(),
Validated: time.Now(),
Error: nil,
},
},
}
}

View file

@ -2,22 +2,19 @@ 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
expectedCounters map[challenge.Type]string
desc string
solvers map[challenge.Type]solver
authz []acme.Authorization
expectedError string
}{
{
desc: "success",
@ -29,33 +26,9 @@ func TestProber_Solve(t *testing.T) {
},
},
authz: []acme.Authorization{
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",
createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing),
},
},
{
@ -68,12 +41,9 @@ func TestProber_Solve(t *testing.T) {
},
},
authz: []acme.Authorization{
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",
createStubAuthorizationHTTP01("acme.wtf", acme.StatusValid),
createStubAuthorizationHTTP01("lego.wtf", acme.StatusValid),
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusValid),
},
},
{
@ -81,56 +51,50 @@ func TestProber_Solve(t *testing.T) {
solvers: map[challenge.Type]solver{
challenge.HTTP01: &preSolverMock{
preSolve: map[string]error{
"example.com": errors.New("preSolve error example.com"),
"acme.wtf": errors.New("preSolve error acme.wtf"),
},
solve: map[string]error{
"example.com": errors.New("solve error example.com"),
"acme.wtf": errors.New("solve error acme.wtf"),
},
cleanUp: map[string]error{
"example.com": errors.New("clean error example.com"),
"acme.wtf": errors.New("clean error acme.wtf"),
},
},
},
authz: []acme.Authorization{
createStubAuthorizationHTTP01("example.com", acme.StatusProcessing),
createStubAuthorizationHTTP01("example.org", acme.StatusProcessing),
createStubAuthorizationHTTP01("example.net", acme.StatusProcessing),
createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing),
},
expectedError: `error: one or more domains had a problem:
[example.com] preSolve error example.com
[acme.wtf] preSolve error acme.wtf
`,
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{
"example.com": errors.New("preSolve error example.com"),
"acme.wtf": errors.New("preSolve error acme.wtf"),
},
solve: map[string]error{
"example.com": errors.New("solve error example.com"),
"example.org": errors.New("solve error example.org"),
"acme.wtf": errors.New("solve error acme.wtf"),
"lego.wtf": errors.New("solve error lego.wtf"),
},
cleanUp: map[string]error{
"example.net": errors.New("clean error example.net"),
"mydomain.wtf": errors.New("clean error mydomain.wtf"),
},
},
},
authz: []acme.Authorization{
createStubAuthorizationHTTP01("example.com", acme.StatusProcessing),
createStubAuthorizationHTTP01("example.org", acme.StatusProcessing),
createStubAuthorizationHTTP01("example.net", acme.StatusProcessing),
createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing),
},
expectedError: `error: one or more domains had a problem:
[example.com] preSolve error example.com
[example.org] solve error example.org
[acme.wtf] preSolve error acme.wtf
[lego.wtf] solve error lego.wtf
`,
expectedCounters: map[challenge.Type]string{
challenge.HTTP01: "PreSolve: 3, Solve: 2, CleanUp: 3",
},
},
}
@ -148,10 +112,6 @@ 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))
}
})
}
}

View file

@ -1,13 +1,13 @@
package resolver
import (
"context"
"errors"
"fmt"
"sort"
"strconv"
"time"
"github.com/cenkalti/backoff/v5"
"github.com/cenkalti/backoff/v4"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/challenge"
@ -15,7 +15,6 @@ 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
@ -37,14 +36,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, opts ...http01.ChallengeOption) error {
c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p, opts...)
func (c *SolverManager) SetHTTP01Provider(p challenge.Provider) error {
c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p)
return nil
}
// SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge.
func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider, opts ...tlsalpn01.ChallengeOption) error {
c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p, opts...)
func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider) error {
c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p)
return nil
}
@ -70,7 +69,6 @@ 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)
}
@ -93,20 +91,20 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error {
return nil
}
retryAfter, err := api.ParseRetryAfter(chlng.RetryAfter)
if err != nil || retryAfter == 0 {
ra, err := strconv.Atoi(chlng.RetryAfter)
if err != nil {
// The ACME server MUST return a Retry-After.
// If it doesn't, or if it's invalid, we'll just poll hard.
// If it doesn't, 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
retryAfter = 5 * time.Second
ra = 5
}
ctx := context.Background()
initialInterval := time.Duration(ra) * time.Second
bo := backoff.NewExponentialBackOff()
bo.InitialInterval = retryAfter
bo.MaxInterval = 10 * retryAfter
bo.InitialInterval = initialInterval
bo.MaxInterval = 10 * initialInterval
bo.MaxElapsedTime = 100 * initialInterval
// After the path is sent, the ACME server will access our server.
// Repeatedly check the server for an updated status on our request.
@ -126,12 +124,10 @@ func validate(core *api.Core, domain string, chlg acme.Challenge) error {
return nil
}
return fmt.Errorf("the server didn't respond to our request (status=%s)", authz.Status)
return errors.New("the server didn't respond to our request")
}
return wait.Retry(ctx, operation,
backoff.WithBackOff(bo),
backoff.WithMaxElapsedTime(100*retryAfter))
return backoff.Retry(operation, bo)
}
func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
@ -141,9 +137,9 @@ func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
case acme.StatusPending, acme.StatusProcessing:
return false, nil
case acme.StatusInvalid:
return false, fmt.Errorf("invalid challenge: %w", chlng.Err())
return false, chlng.Error
default:
return false, fmt.Errorf("the server returned an unexpected challenge status: %s", chlng.Status)
return false, errors.New("the server returned an unexpected state")
}
}
@ -158,12 +154,11 @@ 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, fmt.Errorf("invalid authorization: %w", chlg.Err())
return false, chlg.Error
}
}
return false, errors.New("invalid authorization")
return false, fmt.Errorf("the authorization state %s", authz.Status)
default:
return false, fmt.Errorf("the server returned an unexpected authorization status: %s", authz.Status)
return false, errors.New("the server returned an unexpected state")
}
}

View file

@ -12,7 +12,6 @@ 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"
@ -33,50 +32,70 @@ func TestByType(t *testing.T) {
}
func TestValidate(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(t)
var statuses []string
privateKey, _ := rsa.GenerateKey(rand.Reader, 1024)
privateKey, _ := rsa.GenerateKey(rand.Reader, 512)
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
}
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
}
rw.Header().Set("Link",
fmt.Sprintf(`<https://%s/my-authz>; rel="up"`, req.Context().Value(http.LocalAddrContextKey)))
if err := validateNoBody(privateKey, r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
st := statuses[0]
statuses = statuses[1:]
w.Header().Set("Link", "<"+apiURL+`/my-authz>; rel="up"`)
chlg := &acme.Challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}
st := statuses[0]
statuses = statuses[1:]
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:]
chlg := &acme.Challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}
if st == acme.StatusInvalid {
chlg.Error = &acme.ProblemDetails{}
}
authorization := acme.Authorization{
Status: st,
Challenges: []acme.Challenge{},
}
err := tester.WriteJSONResponse(w, chlg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
if st == acme.StatusInvalid {
chlg := acme.Challenge{
Status: acme.StatusInvalid,
}
authorization.Challenges = append(authorization.Challenges, chlg)
}
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
}
servermock.JSONEncode(authorization).ServeHTTP(rw, req)
})).
BuildHTTPS(t)
st := statuses[0]
statuses = statuses[1:]
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
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)
require.NoError(t, err)
testCases := []struct {
@ -87,7 +106,7 @@ func TestValidate(t *testing.T) {
{
name: "POST-unexpected",
statuses: []string{"weird"},
want: "the server returned an unexpected challenge status: weird",
want: "unexpected",
},
{
name: "POST-valid",
@ -96,12 +115,12 @@ func TestValidate(t *testing.T) {
{
name: "POST-invalid",
statuses: []string{acme.StatusInvalid},
want: "invalid challenge:",
want: "error",
},
{
name: "POST-pending-unexpected",
statuses: []string{acme.StatusPending, "weird"},
want: "the server returned an unexpected authorization status: weird",
want: "unexpected",
},
{
name: "POST-pending-valid",
@ -110,7 +129,7 @@ func TestValidate(t *testing.T) {
{
name: "POST-pending-invalid",
statuses: []string{acme.StatusPending, acme.StatusInvalid},
want: "invalid authorization",
want: "error",
},
}
@ -118,7 +137,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: server.URL + "/chlg"})
err := validate(core, "example.com", acme.Challenge{Type: "http-01", Token: "token", URL: apiURL + "/chlg"})
if test.want == "" {
require.NoError(t, err)
} else {
@ -129,126 +148,6 @@ 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.
@ -260,7 +159,6 @@ 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
@ -277,6 +175,5 @@ 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
}

View file

@ -7,7 +7,6 @@ import (
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
@ -22,38 +21,18 @@ 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, opts ...ChallengeOption) *Challenge {
chlg := &Challenge{
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
return &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) {
@ -80,7 +59,6 @@ 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 {
@ -88,12 +66,7 @@ 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)
}

View file

@ -8,6 +8,7 @@ import (
"crypto/tls"
"encoding/asn1"
"net"
"net/http"
"testing"
"github.com/go-acme/lego/v4/acme"
@ -20,7 +21,7 @@ import (
)
func TestChallenge(t *testing.T) {
server := tester.MockACMEServer().BuildHTTPS(t)
_, apiURL := tester.SetupFakeAPI(t)
domain := "localhost"
port := "24457"
@ -42,7 +43,6 @@ 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, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(
@ -93,12 +93,12 @@ func TestChallenge(t *testing.T) {
}
func TestChallengeInvalidPort(t *testing.T) {
server := tester.MockACMEServer().BuildHTTPS(t)
_, apiURL := tester.SetupFakeAPI(t)
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 128)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(
@ -123,7 +123,7 @@ func TestChallengeInvalidPort(t *testing.T) {
}
func TestChallengeIPaddress(t *testing.T) {
server := tester.MockACMEServer().BuildHTTPS(t)
_, apiURL := tester.SetupFakeAPI(t)
domain := "127.0.0.1"
port := "24457"
@ -146,37 +146,31 @@ 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
extValue []byte
)
var foundAcmeIdentifier bool
var 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.Equal(t, value, extValue, "Expected the challenge certificate id-pe-acmeIdentifier extension to contain the SHA-256 digest of the keyAuth")
require.EqualValues(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, 1024)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(server.Client(), "lego-test", server.URL+"/dir", "", privateKey)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(

View file

@ -2,8 +2,10 @@ package cmd
import (
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"net/url"
"os"
"path/filepath"
@ -16,8 +18,6 @@ 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/foo@example.com/
// ./.lego/accounts/localhost_14000/hubert@hubert.com/
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
// │ └── root accounts directory
@ -42,7 +42,7 @@ const (
//
// keysPath:
//
// ./.lego/accounts/localhost_14000/foo@example.com/keys/
// ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/
// │ │ │ │ └── root keys directory
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
@ -51,7 +51,7 @@ const (
//
// accountFilePath:
//
// ./.lego/accounts/localhost_14000/foo@example.com/account.json
// ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json
// │ │ │ │ └── account file
// │ │ │ └── userID ("email" option)
// │ │ └── CA server ("server" option)
@ -59,7 +59,6 @@ const (
// └── "path" option
type AccountsStorage struct {
userID string
email string
rootPath string
rootUserPath string
keysPath string
@ -69,27 +68,21 @@ type AccountsStorage struct {
// NewAccountsStorage Creates a new AccountsStorage.
func NewAccountsStorage(ctx *cli.Context) *AccountsStorage {
// TODO: move to account struct?
email := ctx.String(flgEmail)
// TODO: move to account struct? Currently MUST pass email.
email := getEmail(ctx)
userID := email
if userID == "" {
userID = userIDPlaceholder
}
serverURL, err := url.Parse(ctx.String(flgServer))
serverURL, err := url.Parse(ctx.String("server"))
if err != nil {
log.Fatal(err)
}
rootPath := filepath.Join(ctx.String(flgPath), baseAccountsRootFolderName)
rootPath := filepath.Join(ctx.String("path"), baseAccountsRootFolderName)
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
accountsPath := filepath.Join(rootPath, serverPath)
rootUserPath := filepath.Join(accountsPath, userID)
rootUserPath := filepath.Join(accountsPath, email)
return &AccountsStorage{
userID: userID,
email: email,
userID: email,
rootPath: rootPath,
rootUserPath: rootUserPath,
keysPath: filepath.Join(rootUserPath, baseKeysFolderName),
@ -105,7 +98,6 @@ func (s *AccountsStorage) ExistsAccountFilePath() bool {
} else if err != nil {
log.Fatal(err)
}
return true
}
@ -121,10 +113,6 @@ 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 {
@ -137,14 +125,13 @@ 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.GetUserID(), err)
log.Fatalf("Could not load file for account %s: %v", s.userID, err)
}
var account Account
err = json.Unmarshal(fileBytes, &account)
if err != nil {
log.Fatalf("Could not parse file for account %s: %v", s.GetUserID(), err)
log.Fatalf("Could not parse file for account %s: %v", s.userID, err)
}
account.key = privateKey
@ -152,14 +139,13 @@ 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.GetUserID(), err)
log.Fatalf("Could not load account for %s. Registration is nil: %#v", s.userID, err)
}
account.Registration = reg
err = s.Save(&account)
if err != nil {
log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.GetUserID(), err)
log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.userID, err)
}
}
@ -167,19 +153,18 @@ func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account {
}
func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey {
accKeyPath := filepath.Join(s.keysPath, s.GetUserID()+".key")
accKeyPath := filepath.Join(s.keysPath, s.userID+".key")
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
log.Printf("No key found for account %s. Generating a %s key.", s.GetUserID(), keyType)
log.Printf("No key found for account %s. Generating a %s key.", s.userID, 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.GetUserID(), err)
log.Fatalf("Could not generate RSA private account key for account %s: %v", s.userID, err)
}
log.Printf("Saved key to %s", accKeyPath)
return privateKey
}
@ -193,7 +178,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.GetUserID(), err)
log.Fatalf("Could not check/create directory for account %s: %v", s.userID, err)
}
}
@ -210,7 +195,6 @@ 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
@ -225,18 +209,22 @@ func loadPrivateKey(file string) (crypto.PrivateKey, error) {
return nil, err
}
privateKey, err := certcrypto.ParsePEMPrivateKey(keyBytes)
if err != nil {
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)
}
return privateKey, nil
return nil, errors.New("unknown private key type")
}
func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) {
// couldn't load account but got a key. Try to look the account up.
config := lego.NewConfig(&Account{key: privateKey})
config.CADirURL = ctx.String(flgServer)
config.CADirURL = ctx.String("server")
config.UserAgent = getUserAgent(ctx)
client, err := lego.NewClient(config)
@ -248,6 +236,5 @@ func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*re
if err != nil {
return nil, err
}
return reg, nil
}

View file

@ -2,6 +2,7 @@ package cmd
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
@ -60,7 +61,7 @@ type CertificatesStorage struct {
// NewCertificatesStorage create a new certificates storage.
func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage {
pfxFormat := ctx.String(flgPFXFormat)
pfxFormat := ctx.String("pfx.format")
switch pfxFormat {
case "DES", "RC2", "SHA256":
@ -69,13 +70,13 @@ func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage {
}
return &CertificatesStorage{
rootPath: filepath.Join(ctx.String(flgPath), baseCertificatesFolderName),
archivePath: filepath.Join(ctx.String(flgPath), baseArchivesFolderName),
pem: ctx.Bool(flgPEM),
pfx: ctx.Bool(flgPFX),
pfxPassword: ctx.String(flgPFXPass),
rootPath: filepath.Join(ctx.String("path"), baseCertificatesFolderName),
archivePath: filepath.Join(ctx.String("path"), baseArchivesFolderName),
pem: ctx.Bool("pem"),
pfx: ctx.Bool("pfx"),
pfxPassword: ctx.String("pfx.pass"),
pfxFormat: pfxFormat,
filename: ctx.String(flgFilename),
filename: ctx.String("filename"),
}
}
@ -158,7 +159,6 @@ func (s *CertificatesStorage) ExistsFile(domain, extension string) bool {
} else if err != nil {
log.Fatal(err)
}
return true
}
@ -233,9 +233,27 @@ func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.R
return fmt.Errorf("unable to get certificate chain for domain %s: %w", domain, err)
}
privateKey, err := certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)
if err != nil {
return fmt.Errorf("unable to parse PrivateKey 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)
}
encoder, err := getPFXEncoder(s.pfxFormat)
@ -284,7 +302,6 @@ func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, er
}
var certChain []*x509.Certificate
for chainCertPemBlock != nil {
chainCert, err := x509.ParseCertificate(chainCertPemBlock.Bytes)
if err != nil {
@ -300,7 +317,6 @@ 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
@ -321,6 +337,5 @@ func sanitizedDomain(domain string) string {
if err != nil {
log.Fatal(err)
}
return safe
}

View file

@ -6,17 +6,17 @@ import (
)
func Before(ctx *cli.Context) error {
if ctx.String(flgPath) == "" {
log.Fatalf("Could not determine current working directory. Please pass --%s.", flgPath)
if ctx.String("path") == "" {
log.Fatal("Could not determine current working directory. Please pass --path.")
}
err := createNonExistingFolder(ctx.String(flgPath))
err := createNonExistingFolder(ctx.String("path"))
if err != nil {
log.Fatalf("Could not check/create path: %v", err)
}
if ctx.String(flgServer) == "" {
log.Fatalf("Could not determine current working server. Please pass --%s.", flgServer)
if ctx.String("server") == "" {
log.Fatal("Could not determine current working server. Please pass --server.")
}
return nil

View file

@ -9,8 +9,6 @@ import (
"github.com/urfave/cli/v2"
)
const flgCode = "code"
func createDNSHelp() *cli.Command {
return &cli.Command{
Name: "dnshelp",
@ -18,7 +16,7 @@ func createDNSHelp() *cli.Command {
Action: dnsHelp,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flgCode,
Name: "code",
Aliases: []string{"c"},
Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()),
},
@ -27,7 +25,7 @@ func createDNSHelp() *cli.Command {
}
func dnsHelp(ctx *cli.Context) error {
code := ctx.String(flgCode)
code := ctx.String("code")
if code == "" {
w := tabwriter.NewWriter(ctx.App.Writer, 0, 0, 2, ' ', 0)
ew := &errWriter{w: w}
@ -58,7 +56,7 @@ type errWriter struct {
err error
}
func (ew *errWriter) writeln(a ...any) {
func (ew *errWriter) writeln(a ...interface{}) {
if ew.err != nil {
return
}
@ -66,7 +64,7 @@ func (ew *errWriter) writeln(a ...any) {
_, ew.err = fmt.Fprintln(ew.w, a...)
}
func (ew *errWriter) writef(format string, a ...any) {
func (ew *errWriter) writef(format string, a ...interface{}) {
if ew.err != nil {
return
}

View file

@ -3,7 +3,6 @@ package cmd
import (
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"path/filepath"
@ -13,11 +12,6 @@ import (
"github.com/urfave/cli/v2"
)
const (
flgAccounts = "accounts"
flgNames = "names"
)
func createList() *cli.Command {
return &cli.Command{
Name: "list",
@ -25,19 +19,19 @@ func createList() *cli.Command {
Action: list,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: flgAccounts,
Name: "accounts",
Aliases: []string{"a"},
Usage: "Display accounts.",
},
&cli.BoolFlag{
Name: flgNames,
Name: "names",
Aliases: []string{"n"},
Usage: "Display certificate common names only.",
},
// fake email, needed by NewAccountsStorage
&cli.StringFlag{
Name: flgEmail,
Value: "",
Name: "email",
Value: "unknown",
Hidden: true,
},
},
@ -45,7 +39,7 @@ func createList() *cli.Command {
}
func list(ctx *cli.Context) error {
if ctx.Bool(flgAccounts) && !ctx.Bool(flgNames) {
if ctx.Bool("accounts") && !ctx.Bool("names") {
if err := listAccount(ctx); err != nil {
return err
}
@ -62,13 +56,12 @@ func listCertificates(ctx *cli.Context) error {
return err
}
names := ctx.Bool(flgNames)
names := ctx.Bool("names")
if len(matches) == 0 {
if !names {
fmt.Println("No certificates found.")
}
return nil
}
@ -77,7 +70,7 @@ func listCertificates(ctx *cli.Context) error {
}
for _, filename := range matches {
if strings.HasSuffix(filename, issuerExt) {
if strings.HasSuffix(filename, ".issuer.crt") {
continue
}
@ -101,11 +94,6 @@ 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()
@ -129,7 +117,6 @@ func listAccount(ctx *cli.Context) error {
}
fmt.Println("Found the following accounts:")
for _, filename := range matches {
data, err := os.ReadFile(filename)
if err != nil {
@ -137,7 +124,6 @@ func listAccount(ctx *cli.Context) error {
}
var account Account
err = json.Unmarshal(data, &account)
if err != nil {
return err
@ -156,12 +142,3 @@ 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, ", ")
}

View file

@ -6,7 +6,6 @@ import (
"errors"
"math/rand"
"os"
"slices"
"time"
"github.com/go-acme/lego/v4/acme/api"
@ -18,17 +17,14 @@ import (
"github.com/urfave/cli/v2"
)
// Flag names.
const (
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"
flgForceCertDomains = "force-cert-domains"
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"
)
func createRenew() *cli.Command {
@ -38,103 +34,78 @@ func createRenew() *cli.Command {
Action: renew,
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) != ""
hasDomains := len(ctx.StringSlice("domains")) > 0
hasCsr := ctx.String("csr") != ""
if hasDomains && hasCsr {
log.Fatalf("Please specify either --%s/-d or --%s/-c, but not both", flgDomains, flgCSR)
log.Fatal("Please specify either --domains/-d or --csr/-c, but not both")
}
if !hasDomains && !hasCsr {
log.Fatalf("Please specify --%s/-d (or --%s/-c if you already have a CSR)", flgDomains, flgCSR)
log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
}
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: flgRenewDays,
Name: "days",
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 (RFC9773) to check if a certificate should be renewed.",
Name: "ari-enable",
Usage: "Use the renewalInfo endpoint (draft-ietf-acme-ari) to check if a certificate should be renewed.",
},
&cli.DurationFlag{
Name: flgARIWaitToRenewDuration,
Name: "ari-wait-to-renew-duration",
Usage: "The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint.",
},
&cli.BoolFlag{
Name: flgReuseKey,
Name: "reuse-key",
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
},
&cli.BoolFlag{
Name: flgNoBundle,
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
&cli.BoolFlag{
Name: flgMustStaple,
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
" Only works if the CSR is generated by lego.",
},
&cli.TimestampFlag{
Name: flgNotBefore,
Name: "not-before",
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
Layout: time.RFC3339,
},
&cli.TimestampFlag{
Name: flgNotAfter,
Name: "not-after",
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
Layout: time.RFC3339,
},
&cli.StringFlag{
Name: flgPreferredChain,
Name: "preferred-chain",
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,
Name: "always-deactivate-authorizations",
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
},
&cli.StringFlag{
Name: flgRenewHook,
Name: "renew-hook",
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,
Name: "no-random-sleep",
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, keyType := setupAccount(ctx, NewAccountsStorage(ctx))
account, client := setup(ctx, NewAccountsStorage(ctx))
setupChallenges(ctx, client)
if account.Registration == nil {
log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email)
@ -142,83 +113,59 @@ func renew(ctx *cli.Context) error {
certsStorage := NewCertificatesStorage(ctx)
bundle := !ctx.Bool(flgNoBundle)
bundle := !ctx.Bool("no-bundle")
meta := map[string]string{
hookEnvAccountEmail: account.Email,
}
meta := map[string]string{renewEnvAccountEmail: account.Email}
// CSR
if ctx.IsSet(flgCSR) {
return renewForCSR(ctx, account, keyType, certsStorage, bundle, meta)
if ctx.IsSet("csr") {
return renewForCSR(ctx, client, certsStorage, bundle, meta)
}
// Domains
return renewForDomains(ctx, account, keyType, certsStorage, bundle, meta)
return renewForDomains(ctx, client, certsStorage, bundle, meta)
}
func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
domains := ctx.StringSlice(flgDomains)
func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
domains := ctx.StringSlice("domains")
domain := domains[0]
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, certExt)
certificates, err := certsStorage.ReadCertificate(domain, ".crt")
if err != nil {
log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err)
}
cert := certificates[0]
var (
ariRenewalTime *time.Time
replacesCertID string
)
var client *lego.Client
if !ctx.Bool(flgARIDisable) {
client = setupClient(ctx, account, keyType)
var ariRenewalTime *time.Time
if ctx.Bool("ari-enable") {
ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)
if ariRenewalTime != nil {
now := time.Now().UTC()
// Figure out if we need to sleep before renewing.
if ariRenewalTime.After(now) {
log.Infof("[%s] Sleeping %s until renewal time %s", domain, ariRenewalTime.Sub(now), ariRenewalTime)
time.Sleep(ariRenewalTime.Sub(now))
}
}
replacesCertID, err = certificate.MakeARICertID(cert)
if err != nil {
log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err)
}
}
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)) {
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int("days")) {
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()))
var privateKey crypto.PrivateKey
certDomains := certcrypto.ExtractDomains(cert)
if ctx.Bool(flgReuseKey) {
keyBytes, errR := certsStorage.ReadFile(domain, keyExt)
var privateKey crypto.PrivateKey
if ctx.Bool("reuse-key") {
keyBytes, errR := certsStorage.ReadFile(domain, ".key")
if errR != nil {
log.Fatalf("Error while loading the private key for domain %s\n\t%v", domain, errR)
}
@ -231,10 +178,9 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
// https://github.com/go-acme/lego/issues/1656
// https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L435-L440
if !isatty.IsTerminal(os.Stdout.Fd()) && !ctx.Bool(flgNoRandomSleep) {
if !isatty.IsTerminal(os.Stdout.Fd()) && !ctx.Bool("no-random-sleep") {
// 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)))
@ -242,25 +188,22 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
time.Sleep(sleepTime)
}
renewalDomains := slices.Clone(domains)
if !forceDomains {
renewalDomains = merge(certDomains, domains)
}
request := certificate.ObtainRequest{
Domains: renewalDomains,
Domains: merge(certDomains, domains),
PrivateKey: privateKey,
MustStaple: ctx.Bool(flgMustStaple),
NotBefore: getTime(ctx, flgNotBefore),
NotAfter: getTime(ctx, flgNotAfter),
MustStaple: ctx.Bool("must-staple"),
NotBefore: getTime(ctx, "not-before"),
NotAfter: getTime(ctx, "not-after"),
Bundle: bundle,
PreferredChain: ctx.String(flgPreferredChain),
Profile: ctx.String(flgProfile),
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
PreferredChain: ctx.String("preferred-chain"),
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
}
if replacesCertID != "" {
request.ReplacesCertID = replacesCertID
if ctx.Bool("ari-enable") {
request.ReplacesCertID, err = certificate.MakeARICertID(cert)
if err != nil {
log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err)
}
}
certRes, err := client.Certificate.Obtain(request)
@ -268,17 +211,15 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
log.Fatal(err)
}
certRes.Domain = domain
certsStorage.SaveResource(certRes)
addPathToMetadata(meta, domain, certRes, certsStorage)
return launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta)
return launchHook(ctx.String("renew-hook"), meta)
}
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))
func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
csr, err := readCSRFile(ctx.String("csr"))
if err != nil {
log.Fatal(err)
}
@ -291,64 +232,48 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, certExt)
certificates, err := certsStorage.ReadCertificate(domain, ".crt")
if err != nil {
log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err)
}
cert := certificates[0]
var (
ariRenewalTime *time.Time
replacesCertID string
)
var client *lego.Client
if !ctx.Bool(flgARIDisable) {
client = setupClient(ctx, account, keyType)
var ariRenewalTime *time.Time
if ctx.Bool("ari-enable") {
ariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)
if ariRenewalTime != nil {
now := time.Now().UTC()
// Figure out if we need to sleep before renewing.
if ariRenewalTime.After(now) {
log.Infof("[%s] Sleeping %s until renewal time %s", domain, ariRenewalTime.Sub(now), ariRenewalTime)
time.Sleep(ariRenewalTime.Sub(now))
}
}
replacesCertID, err = certificate.MakeARICertID(cert)
if err != nil {
log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err)
}
}
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) {
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int("days")) {
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()))
request := certificate.ObtainForCSRRequest{
CSR: csr,
NotBefore: getTime(ctx, flgNotBefore),
NotAfter: getTime(ctx, flgNotAfter),
NotBefore: getTime(ctx, "not-before"),
NotAfter: getTime(ctx, "not-after"),
Bundle: bundle,
PreferredChain: ctx.String(flgPreferredChain),
Profile: ctx.String(flgProfile),
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
PreferredChain: ctx.String("preferred-chain"),
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
}
if replacesCertID != "" {
request.ReplacesCertID = replacesCertID
if ctx.Bool("ari-enable") {
request.ReplacesCertID, err = certificate.MakeARICertID(cert)
if err != nil {
log.Fatalf("Error while construction the ARI CertID for domain %s\n\t%v", domain, err)
}
}
certRes, err := client.Certificate.ObtainForCSR(request)
@ -360,51 +285,24 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
addPathToMetadata(meta, domain, certRes, certsStorage)
return launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta)
return launchHook(ctx.String("renew-hook"), meta)
}
func needRenewal(x509Cert *x509.Certificate, domain string, days int, dynamic bool) bool {
func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool {
if x509Cert.IsCA {
log.Fatalf("[%s] Certificate bundle starts with a CA certificate", domain)
}
if dynamic {
return needRenewalDynamic(x509Cert, domain, time.Now())
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 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
return true
}
// getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint.
@ -420,20 +318,16 @@ 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))
renewalTime := renewalInfo.ShouldRenewAt(now, ctx.Duration("ari-wait-to-renew-duration"))
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 != "" {
@ -443,14 +337,36 @@ func getARIRenewalTime(ctx *cli.Context, cert *x509.Certificate, domain string,
return renewalTime
}
func merge(prevDomains, nextDomains []string) []string {
for _, next := range nextDomains {
if slices.Contains(prevDomains, next) {
continue
}
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)
prevDomains = append(prevDomains, next)
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 {
var found bool
for _, prev := range prevDomains {
if prev == next {
found = true
break
}
}
if !found {
prevDomains = append(prevDomains, next)
}
}
return prevDomains
}

View file

@ -108,62 +108,9 @@ 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, false)
actual := needRenewal(test.x509Cert, "foo.com", test.days)
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)
})
}
}

View file

@ -6,12 +6,6 @@ import (
"github.com/urfave/cli/v2"
)
// Flag names.
const (
flgKeep = "keep"
flgReason = "reason"
)
func createRevoke() *cli.Command {
return &cli.Command{
Name: "revoke",
@ -19,12 +13,12 @@ func createRevoke() *cli.Command {
Action: revoke,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: flgKeep,
Name: "keep",
Aliases: []string{"k"},
Usage: "Keep the certificates after the revocation instead of archiving them.",
},
&cli.UintFlag{
Name: flgReason,
Name: "reason",
Usage: "Identifies the reason for the certificate revocation." +
" See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1." +
" Valid values are:" +
@ -38,26 +32,24 @@ func createRevoke() *cli.Command {
}
func revoke(ctx *cli.Context) error {
account, keyType := setupAccount(ctx, NewAccountsStorage(ctx))
acc, client := setup(ctx, NewAccountsStorage(ctx))
if account.Registration == nil {
log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email)
if acc.Registration == nil {
log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
}
client := newClient(ctx, account, keyType)
certsStorage := NewCertificatesStorage(ctx)
certsStorage.CreateRootFolder()
for _, domain := range ctx.StringSlice(flgDomains) {
for _, domain := range ctx.StringSlice("domains") {
log.Printf("Trying to revoke certificate for domain %s", domain)
certBytes, err := certsStorage.ReadFile(domain, certExt)
certBytes, err := certsStorage.ReadFile(domain, ".crt")
if err != nil {
log.Fatalf("Error while revoking the certificate for domain %s\n\t%v", domain, err)
}
reason := ctx.Uint(flgReason)
reason := ctx.Uint("reason")
err = client.Certificate.RevokeWithReason(certBytes, &reason)
if err != nil {
@ -66,7 +58,7 @@ func revoke(ctx *cli.Context) error {
log.Println("Certificate was revoked.")
if ctx.Bool(flgKeep) {
if ctx.Bool("keep") {
return nil
}

View file

@ -14,107 +14,76 @@ import (
"github.com/urfave/cli/v2"
)
// Flag names.
const (
flgNoBundle = "no-bundle"
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 {
return &cli.Command{
Name: "run",
Usage: "Register an account, then create and install a certificate",
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) != ""
hasDomains := len(ctx.StringSlice("domains")) > 0
hasCsr := ctx.String("csr") != ""
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,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: flgNoBundle,
Name: "no-bundle",
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
&cli.BoolFlag{
Name: flgMustStaple,
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
" Only works if the CSR is generated by lego.",
},
&cli.TimestampFlag{
Name: flgNotBefore,
Name: "not-before",
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
Layout: time.RFC3339,
},
&cli.TimestampFlag{
Name: flgNotAfter,
Name: "not-after",
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,
Name: "preferred-chain",
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,
Name: "always-deactivate-authorizations",
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
},
&cli.StringFlag{
Name: flgRunHook,
Name: "run-hook",
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
Your account credentials have been saved in your Let's Encrypt
configuration directory at "%s".
You should make a secure backup of this folder now. This
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.
configuration directory will also contain certificates and
private keys obtained from Let's Encrypt so making regular
backups of this folder is ideal.
`
func run(ctx *cli.Context) error {
accountsStorage := NewAccountsStorage(ctx)
account, keyType := setupAccount(ctx, accountsStorage)
client := setupClient(ctx, account, keyType)
account, client := setup(ctx, accountsStorage)
setupChallenges(ctx, client)
if account.Registration == nil {
reg, err := register(ctx, client)
@ -143,27 +112,28 @@ func run(ctx *cli.Context) error {
certsStorage.SaveResource(cert)
meta := map[string]string{
hookEnvAccountEmail: account.Email,
renewEnvAccountEmail: account.Email,
renewEnvCertDomain: cert.Domain,
renewEnvCertPath: certsStorage.GetFileName(cert.Domain, ".crt"),
renewEnvCertKeyPath: certsStorage.GetFileName(cert.Domain, ".key"),
renewEnvCertPEMPath: certsStorage.GetFileName(cert.Domain, ".pem"),
renewEnvCertPFXPath: certsStorage.GetFileName(cert.Domain, ".pfx"),
}
addPathToMetadata(meta, cert.Domain, cert, certsStorage)
return launchHook(ctx.String(flgRunHook), ctx.Duration(flgRunHookTimeout), meta)
return launchHook(ctx.String("run-hook"), meta)
}
func handleTOS(ctx *cli.Context, client *lego.Client) bool {
// Check for a global accept override
if ctx.Bool(flgAcceptTOS) {
if ctx.Bool("accept-tos") {
return true
}
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)
@ -187,12 +157,12 @@ func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, er
log.Fatal("You did not accept the TOS. Unable to proceed.")
}
if ctx.Bool(flgEAB) {
kid := ctx.String(flgKID)
hmacEncoded := ctx.String(flgHMAC)
if ctx.Bool("eab") {
kid := ctx.String("kid")
hmacEncoded := ctx.String("hmac")
if kid == "" || hmacEncoded == "" {
log.Fatalf("Requires arguments --%s and --%s.", flgKID, flgHMAC)
log.Fatalf("Requires arguments --kid and --hmac.")
}
return client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
@ -206,36 +176,34 @@ func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, er
}
func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Resource, error) {
bundle := !ctx.Bool(flgNoBundle)
bundle := !ctx.Bool("no-bundle")
domains := ctx.StringSlice(flgDomains)
domains := ctx.StringSlice("domains")
if len(domains) > 0 {
// obtain a certificate, generating a new private key
request := certificate.ObtainRequest{
Domains: domains,
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),
MustStaple: ctx.Bool("must-staple"),
PreferredChain: ctx.String("preferred-chain"),
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
}
if ctx.IsSet(flgPrivateKey) {
var err error
notBefore := ctx.Timestamp("not-before")
if notBefore != nil {
request.NotBefore = *notBefore
}
request.PrivateKey, err = loadPrivateKey(ctx.String(flgPrivateKey))
if err != nil {
return nil, fmt.Errorf("load private key: %w", err)
}
notAfter := ctx.Timestamp("not-after")
if notAfter != nil {
request.NotAfter = *notAfter
}
return client.Certificate.Obtain(request)
}
// read the CSR
csr, err := readCSRFile(ctx.String(flgCSR))
csr, err := readCSRFile(ctx.String("csr"))
if err != nil {
return nil, err
}
@ -243,21 +211,11 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
// obtain a certificate for this CSR
request := certificate.ObtainForCSRRequest{
CSR: csr,
NotBefore: getTime(ctx, flgNotBefore),
NotAfter: getTime(ctx, flgNotAfter),
NotBefore: getTime(ctx, "not-before"),
NotAfter: getTime(ctx, "not-after"),
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)
}
PreferredChain: ctx.String("preferred-chain"),
AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
}
return client.Certificate.ObtainForCSR(request)

View file

@ -1,253 +1,161 @@
package cmd
import (
"fmt"
"time"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/urfave/cli/v2"
"software.sslmate.com/src/go-pkcs12"
)
// Flag names.
const (
flgDomains = "domains"
flgServer = "server"
flgAcceptTOS = "accept-tos"
flgEmail = "email"
flgDisableCommonName = "disable-cn"
flgCSR = "csr"
flgEAB = "eab"
flgKID = "kid"
flgHMAC = "hmac"
flgKeyType = "key-type"
flgFilename = "filename"
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"
flgDNSPropagationDisableANS = "dns.propagation-disable-ans"
flgDNSPropagationRNS = "dns.propagation-rns"
flgDNSResolvers = "dns.resolvers"
flgHTTPTimeout = "http-timeout"
flgTLSSkipVerify = "tls-skip-verify"
flgDNSTimeout = "dns-timeout"
flgPEM = "pem"
flgPFX = "pfx"
flgPFXPass = "pfx.pass"
flgPFXFormat = "pfx.format"
flgCertTimeout = "cert.timeout"
flgOverallRequestLimit = "overall-request-limit"
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{
Name: flgDomains,
Name: "domains",
Aliases: []string{"d"},
Usage: "Add a domain to the process. Can be specified multiple times.",
},
&cli.StringFlag{
Name: flgServer,
Name: "server",
Aliases: []string{"s"},
EnvVars: []string{envServer},
EnvVars: []string{"LEGO_SERVER"},
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
Value: lego.LEDirectoryProduction,
},
&cli.BoolFlag{
Name: flgAcceptTOS,
Name: "accept-tos",
Aliases: []string{"a"},
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
},
&cli.StringFlag{
Name: flgEmail,
Name: "email",
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,
Name: "csr",
Aliases: []string{"c"},
Usage: "Certificate signing request filename, if an external CSR is to be used.",
},
&cli.BoolFlag{
Name: flgEAB,
EnvVars: []string{envEAB},
Name: "eab",
EnvVars: []string{"LEGO_EAB"},
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
},
&cli.StringFlag{
Name: flgKID,
EnvVars: []string{envEABKID},
Name: "kid",
EnvVars: []string{"LEGO_EAB_KID"},
Usage: "Key identifier from External CA. Used for External Account Binding.",
},
&cli.StringFlag{
Name: flgHMAC,
EnvVars: []string{envEABHMAC},
Name: "hmac",
EnvVars: []string{"LEGO_EAB_HMAC"},
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
},
&cli.StringFlag{
Name: flgKeyType,
Name: "key-type",
Aliases: []string{"k"},
Value: "ec256",
Usage: "Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384.",
},
&cli.StringFlag{
Name: flgFilename,
Name: "filename",
Usage: "(deprecated) Filename of the generated certificate.",
},
&cli.StringFlag{
Name: flgPath,
EnvVars: []string{envPath},
Name: "path",
EnvVars: []string{"LEGO_PATH"},
Usage: "Directory to use for storing the data.",
Value: defaultPath,
},
&cli.BoolFlag{
Name: flgHTTP,
Name: "http",
Usage: "Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.",
},
&cli.StringFlag{
Name: flgHTTPPort,
Name: "http.port",
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,
Name: "http.proxy-header",
Usage: "Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy.",
Value: "Host",
},
&cli.StringFlag{
Name: flgHTTPWebroot,
Name: "http.webroot",
Usage: "Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file." +
" This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge",
},
&cli.StringSliceFlag{
Name: flgHTTPMemcachedHost,
Name: "http.memcached-host",
Usage: "Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts.",
},
&cli.StringFlag{
Name: flgHTTPS3Bucket,
Name: "http.s3-bucket",
Usage: "Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.",
},
&cli.BoolFlag{
Name: flgTLS,
Name: "tls",
Usage: "Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.",
},
&cli.StringFlag{
Name: flgTLSPort,
Name: "tls.port",
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,
Name: "dns",
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.",
},
&cli.BoolFlag{
Name: flgDNSDisableCP,
Usage: fmt.Sprintf("(deprecated) use %s instead.", flgDNSPropagationDisableANS),
},
&cli.BoolFlag{
Name: flgDNSPropagationDisableANS,
Name: "dns.disable-cp",
Usage: "By setting this flag to true, disables the need to await propagation of the TXT record to all authoritative name servers.",
},
&cli.BoolFlag{
Name: flgDNSPropagationRNS,
Usage: "By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record.",
},
&cli.DurationFlag{
Name: flgDNSPropagationWait,
Usage: "By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead.",
},
&cli.StringSliceFlag{
Name: flgDNSResolvers,
Name: "dns.resolvers",
Usage: "Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination." +
" For DNS-01 challenge verification, the authoritative DNS server is queried directly." +
" Supported: host:port." +
" The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.",
},
&cli.IntFlag{
Name: flgHTTPTimeout,
Name: "http-timeout",
Usage: "Set the HTTP timeout value to a specific value in seconds.",
},
&cli.BoolFlag{
Name: flgTLSSkipVerify,
Usage: "Skip the TLS verification of the ACME server.",
},
&cli.IntFlag{
Name: flgDNSTimeout,
Name: "dns-timeout",
Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries.",
Value: 10,
},
&cli.BoolFlag{
Name: flgPEM,
Name: "pem",
Usage: "Generate an additional .pem (base64) file by concatenating the .key and .crt files together.",
},
&cli.BoolFlag{
Name: flgPFX,
Name: "pfx",
Usage: "Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.",
EnvVars: []string{envPFX},
EnvVars: []string{"LEGO_PFX"},
},
&cli.StringFlag{
Name: flgPFXPass,
Name: "pfx.pass",
Usage: "The password used to encrypt the .pfx (PCKS#12) file.",
Value: pkcs12.DefaultPassword,
EnvVars: []string{envPFXPassword},
EnvVars: []string{"LEGO_PFX_PASSWORD"},
},
&cli.StringFlag{
Name: flgPFXFormat,
Name: "pfx.format",
Usage: "The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256.",
Value: "RC2",
EnvVars: []string{envPFXFormat},
EnvVars: []string{"LEGO_PFX_FORMAT"},
},
&cli.IntFlag{
Name: flgCertTimeout,
Name: "cert.timeout",
Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.",
Value: 30,
},
&cli.IntFlag{
Name: flgOverallRequestLimit,
Usage: "ACME overall requests limit.",
Value: certificate.DefaultOverallRequestLimit,
},
&cli.StringFlag{
Name: flgUserAgent,
Name: "user-agent",
Usage: "Add to the user-agent sent to the CA to identify an application embedding lego-cli",
},
}
@ -258,6 +166,5 @@ func getTime(ctx *cli.Context, name string) time.Time {
if value == nil {
return time.Time{}
}
return *value
}

View file

@ -1,7 +1,6 @@
package cmd
import (
"bufio"
"context"
"errors"
"fmt"
@ -9,70 +8,32 @@ import (
"os/exec"
"strings"
"time"
"github.com/go-acme/lego/v4/certificate"
)
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 {
func launchHook(hook string, meta map[string]string) error {
if hook == "" {
return nil
}
ctxCmd, cancel := context.WithTimeout(context.Background(), timeout)
ctxCmd, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
parts := strings.Fields(hook)
cmd := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...)
cmd.Env = append(os.Environ(), metaToEnv(meta)...)
output, err := cmdCtx.CombinedOutput()
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("create pipe: %w", err)
if len(output) > 0 {
fmt.Println(string(output))
}
cmd.Stderr = cmd.Stdout
err = cmd.Start()
if err != nil {
return fmt.Errorf("start command: %w", err)
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
return errors.New("hook timed out")
}
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
return err
}
func metaToEnv(meta map[string]string) []string {
@ -84,21 +45,3 @@ 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)
}
}

View file

@ -1,61 +0,0 @@
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)
})
}
}

View file

@ -13,6 +13,8 @@ import (
"github.com/urfave/cli/v2"
)
var version = "dev"
func main() {
app := cli.NewApp()
app.Name = "lego"
@ -20,13 +22,12 @@ func main() {
app.Usage = "Let's Encrypt client written in Go"
app.EnableBashCompletion = true
app.Version = getVersion()
app.Version = version
cli.VersionPrinter = func(c *cli.Context) {
fmt.Printf("lego version %s %s/%s\n", c.App.Version, runtime.GOOS, runtime.GOARCH)
}
var defaultPath string
cwd, err := os.Getwd()
if err == nil {
defaultPath = filepath.Join(cwd, ".lego")

View file

@ -1,15 +0,0 @@
// Code generated by 'internal/releaser'; DO NOT EDIT.
package main
const defaultVersion = "v4.32.0+dev-detach"
var version = ""
func getVersion() string {
if version == "" {
return defaultVersion
}
return version
}

View file

@ -1,38 +1,23 @@
package cmd
import (
"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
// 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) {
func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.Client) {
keyType := getKeyType(ctx)
privateKey := accountsStorage.GetPrivateKey(keyType)
@ -40,56 +25,35 @@ func setupAccount(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account,
if accountsStorage.ExistsAccountFilePath() {
account = accountsStorage.LoadAccount(privateKey)
} else {
account = &Account{Email: accountsStorage.GetEmail(), key: privateKey}
account = &Account{Email: accountsStorage.GetUserID(), key: privateKey}
}
return account, keyType
client := newClient(ctx, account, keyType)
return account, client
}
func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client {
config := lego.NewConfig(acc)
config.CADirURL = ctx.String(flgServer)
config.CADirURL = ctx.String("server")
config.Certificate = lego.CertificateConfig{
KeyType: keyType,
Timeout: time.Duration(ctx.Int(flgCertTimeout)) * time.Second,
OverallRequestLimit: ctx.Int(flgOverallRequestLimit),
DisableCommonName: ctx.Bool(flgDisableCommonName),
KeyType: keyType,
Timeout: time.Duration(ctx.Int("cert.timeout")) * time.Second,
}
config.UserAgent = getUserAgent(ctx)
if ctx.IsSet(flgHTTPTimeout) {
config.HTTPClient.Timeout = time.Duration(ctx.Int(flgHTTPTimeout)) * time.Second
if ctx.IsSet("http-timeout") {
config.HTTPClient.Timeout = time.Duration(ctx.Int("http-timeout")) * time.Second
}
if ctx.Bool(flgTLSSkipVerify) {
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)
}
if client.GetExternalAccountRequired() && !ctx.IsSet(flgEAB) {
log.Fatalf("Server requires External Account Binding. Use --%s with --%s and --%s.", flgEAB, flgKID, flgHMAC)
if client.GetExternalAccountRequired() && !ctx.IsSet("eab") {
log.Fatal("Server requires External Account Binding. Use --eab with --kid and --hmac.")
}
return client
@ -97,7 +61,7 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy
// getKeyType the type from which private keys should be generated.
func getKeyType(ctx *cli.Context) certcrypto.KeyType {
keyType := ctx.String(flgKeyType)
keyType := ctx.String("key-type")
switch strings.ToUpper(keyType) {
case "RSA2048":
return certcrypto.RSA2048
@ -114,12 +78,19 @@ func getKeyType(ctx *cli.Context) certcrypto.KeyType {
}
log.Fatalf("Unsupported KeyType: %s", keyType)
return ""
}
func getEmail(ctx *cli.Context) string {
email := ctx.String("email")
if email == "" {
log.Fatal("You have to pass an account (email address) to the program using --email or -m")
}
return email
}
func getUserAgent(ctx *cli.Context) string {
return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", ctx.String(flgUserAgent), ctx.App.Version))
return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", ctx.String("user-agent"), ctx.App.Version))
}
func createNonExistingFolder(path string) error {
@ -128,7 +99,6 @@ func createNonExistingFolder(path string) error {
} else if err != nil {
return err
}
return nil
}
@ -137,12 +107,10 @@ 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
@ -164,49 +132,3 @@ 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
}

View file

@ -1,7 +1,6 @@
package cmd
import (
"fmt"
"net"
"strings"
"time"
@ -20,60 +19,54 @@ import (
)
func setupChallenges(ctx *cli.Context, client *lego.Client) {
if !ctx.Bool(flgHTTP) && !ctx.Bool(flgTLS) && !ctx.IsSet(flgDNS) {
log.Fatalf("No challenge selected. You must specify at least one challenge: `--%s`, `--%s`, `--%s`.", flgHTTP, flgTLS, flgDNS)
if !ctx.Bool("http") && !ctx.Bool("tls") && !ctx.IsSet("dns") {
log.Fatal("No challenge selected. You must specify at least one challenge: `--http`, `--tls`, `--dns`.")
}
if ctx.Bool(flgHTTP) {
err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx), http01.SetDelay(ctx.Duration(flgHTTPDelay)))
if ctx.Bool("http") {
err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx))
if err != nil {
log.Fatal(err)
}
}
if ctx.Bool(flgTLS) {
err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx), tlsalpn01.SetDelay(ctx.Duration(flgTLSDelay)))
if ctx.Bool("tls") {
err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx))
if err != nil {
log.Fatal(err)
}
}
if ctx.IsSet(flgDNS) {
err := setupDNS(ctx, client)
if err != nil {
log.Fatal(err)
}
if ctx.IsSet("dns") {
setupDNS(ctx, client)
}
}
//nolint:gocyclo // the complexity is expected.
func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
switch {
case ctx.IsSet(flgHTTPWebroot):
ps, err := webroot.NewHTTPProvider(ctx.String(flgHTTPWebroot))
case ctx.IsSet("http.webroot"):
ps, err := webroot.NewHTTPProvider(ctx.String("http.webroot"))
if err != nil {
log.Fatal(err)
}
return ps
case ctx.IsSet(flgHTTPMemcachedHost):
ps, err := memcached.NewMemcachedProvider(ctx.StringSlice(flgHTTPMemcachedHost))
case ctx.IsSet("http.memcached-host"):
ps, err := memcached.NewMemcachedProvider(ctx.StringSlice("http.memcached-host"))
if err != nil {
log.Fatal(err)
}
return ps
case ctx.IsSet(flgHTTPS3Bucket):
ps, err := s3.NewHTTPProvider(ctx.String(flgHTTPS3Bucket))
case ctx.IsSet("http.s3-bucket"):
ps, err := s3.NewHTTPProvider(ctx.String("http.s3-bucket"))
if err != nil {
log.Fatal(err)
}
return ps
case ctx.IsSet(flgHTTPPort):
iface := ctx.String(flgHTTPPort)
case ctx.IsSet("http.port"):
iface := ctx.String("http.port")
if !strings.Contains(iface, ":") {
log.Fatalf("The --%s switch only accepts interface:port or :port for its argument.", flgHTTPPort)
log.Fatalf("The --http switch only accepts interface:port or :port for its argument.")
}
host, port, err := net.SplitHostPort(iface)
@ -82,17 +75,15 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
}
srv := http01.NewProviderServer(host, port)
if header := ctx.String(flgHTTPProxyHeader); header != "" {
if header := ctx.String("http.proxy-header"); header != "" {
srv.SetProxyHeader(header)
}
return srv
case ctx.Bool(flgHTTP):
case ctx.Bool("http"):
srv := http01.NewProviderServer("", "")
if header := ctx.String(flgHTTPProxyHeader); header != "" {
if header := ctx.String("http.proxy-header"); header != "" {
srv.SetProxyHeader(header)
}
return srv
default:
log.Fatal("Invalid HTTP challenge options.")
@ -102,10 +93,10 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
func setupTLSProvider(ctx *cli.Context) challenge.Provider {
switch {
case ctx.IsSet(flgTLSPort):
iface := ctx.String(flgTLSPort)
case ctx.IsSet("tls.port"):
iface := ctx.String("tls.port")
if !strings.Contains(iface, ":") {
log.Fatalf("The --%s switch only accepts interface:port or :port for its argument.", flgTLSPort)
log.Fatalf("The --tls switch only accepts interface:port or :port for its argument.")
}
host, port, err := net.SplitHostPort(iface)
@ -114,7 +105,7 @@ func setupTLSProvider(ctx *cli.Context) challenge.Provider {
}
return tlsalpn01.NewProviderServer(host, port)
case ctx.Bool(flgTLS):
case ctx.Bool("tls"):
return tlsalpn01.NewProviderServer("", "")
default:
log.Fatal("Invalid HTTP challenge options.")
@ -122,62 +113,22 @@ func setupTLSProvider(ctx *cli.Context) challenge.Provider {
}
}
func setupDNS(ctx *cli.Context, client *lego.Client) error {
err := checkPropagationExclusiveOptions(ctx)
func setupDNS(ctx *cli.Context, client *lego.Client) {
provider, err := dns.NewDNSChallengeProviderByName(ctx.String("dns"))
if err != nil {
return err
log.Fatal(err)
}
wait := ctx.Duration(flgDNSPropagationWait)
if wait < 0 {
return fmt.Errorf("'%s' cannot be negative", flgDNSPropagationWait)
}
provider, err := dns.NewDNSChallengeProviderByName(ctx.String(flgDNS))
if err != nil {
return err
}
servers := ctx.StringSlice(flgDNSResolvers)
servers := ctx.StringSlice("dns.resolvers")
err = client.Challenge.SetDNS01Provider(provider,
dns01.CondOption(len(servers) > 0,
dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.StringSlice(flgDNSResolvers)))),
dns01.CondOption(ctx.Bool(flgDNSDisableCP) || ctx.Bool(flgDNSPropagationDisableANS),
dns01.DisableAuthoritativeNssPropagationRequirement()),
dns01.CondOption(ctx.Duration(flgDNSPropagationWait) > 0,
// TODO(ldez): inside the next major version we will use flgDNSDisableCP here.
// This will change the meaning of this flag to really disable all propagation checks.
dns01.PropagationWait(wait, true)),
dns01.CondOption(ctx.Bool(flgDNSPropagationRNS),
dns01.RecursiveNSsPropagationRequirement()),
dns01.CondOption(ctx.IsSet(flgDNSTimeout),
dns01.AddDNSTimeout(time.Duration(ctx.Int(flgDNSTimeout))*time.Second)),
dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.StringSlice("dns.resolvers")))),
dns01.CondOption(ctx.Bool("dns.disable-cp"),
dns01.DisableCompletePropagationRequirement()),
dns01.CondOption(ctx.IsSet("dns-timeout"),
dns01.AddDNSTimeout(time.Duration(ctx.Int("dns-timeout"))*time.Second)),
)
return err
}
func checkPropagationExclusiveOptions(ctx *cli.Context) error {
if ctx.IsSet(flgDNSDisableCP) {
log.Printf("The flag '%s' is deprecated use '%s' instead.", flgDNSDisableCP, flgDNSPropagationDisableANS)
if err != nil {
log.Fatal(err)
}
if (isSetBool(ctx, flgDNSDisableCP) || isSetBool(ctx, flgDNSPropagationDisableANS)) && ctx.IsSet(flgDNSPropagationWait) {
return fmt.Errorf("'%s' and '%s' are mutually exclusive", flgDNSPropagationDisableANS, flgDNSPropagationWait)
}
if isSetBool(ctx, flgDNSPropagationRNS) && ctx.IsSet(flgDNSPropagationWait) {
return fmt.Errorf("'%s' and '%s' are mutually exclusive", flgDNSPropagationRNS, flgDNSPropagationWait)
}
return nil
}
func isSetBool(ctx *cli.Context, name string) bool {
return ctx.IsSet(name) && ctx.Bool(name)
}

View file

@ -1,3 +0,0 @@
#!/bin/bash -e
sleep 50

View file

@ -1,7 +0,0 @@
#!/bin/bash -e
for i in `seq 1 10`
do
echo $i
sleep 0.2
done

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,14 @@
.PHONY: default clean serve build
.PHONY: default clean hugo hugo-build
default: clean serve
default: clean hugo
clean:
rm -rf public/
build: clean
hugo-build: clean
hugo --enableGitInfo --source .
serve:
hugo:
hugo server --disableFastRender --enableGitInfo --watch --source .
# hugo server -D

View file

@ -1,40 +1,31 @@
---
title: "Lego"
title: "Welcome"
date: 2019-03-03T16:39:46+01:00
draft: false
chapter: false
chapter: true
---
# Lego
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 ApplicationLayer Protocol Negotiation (ALPN) Challenge Extension
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses
- 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" %}})
- Support [draft-ietf-acme-ari-01](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
- Register with CA
- Obtain certificates, both from scratch or with an existing CSR
- Renew certificates
- Revoke certificates
- Robust implementation of ACME challenges:
- Robust implementation of all 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
- [Custom challenge solvers]({{% ref "usage/library/Writing-a-Challenge-Solver" %}})
- Comes with multiple optional [DNS providers]({{< ref "dns" >}})
- [Custom challenge solvers]({{< ref "usage/library/Writing-a-Challenge-Solver" >}})
- Certificate bundling
- OCSP helper function

View file

@ -5,16 +5,6 @@ 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.
@ -25,7 +15,7 @@ The environment variables can reference a value.
Here is an example bash command using the Cloudflare DNS provider:
```bash
```console
$ CLOUDFLARE_EMAIL=you@example.com \
CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \
lego --dns cloudflare --domains www.example.com --email you@example.com run
@ -43,7 +33,7 @@ The file must contain only the value.
Here is an example bash command using the CloudFlare DNS provider:
```bash
```console
$ cat /the/path/to/my/key
b9841238feb177a84330febba8a83208921177bffe733

View file

@ -1,19 +1,24 @@
Name = "Manual"
Description = '''Solving the DNS-01 challenge using CLI prompt.'''
Code = "manual"
Since = "v0.3.0"
---
title: "Manual"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: manual
dnsprovider:
since: v0.3.0
code: manual
url:
---
Example = '''
lego --dns manual -d '*.example.com' -d example.com run
'''
Solving the DNS-01 challenge using CLI prompt.
<!--more-->
Additional = '''
## Example
To start using the CLI prompt "provider", start lego with `--dns manual`:
```console
$ lego --dns manual -d example.com run
$ lego --email "you@example.com" --domains="example.com" --dns "manual" run
```
What follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions:
@ -31,13 +36,13 @@ If you accept the linked Terms of Service, hit `Enter`.
[INFO] acme: Registering account for you@example.com
!!!! HEADS UP !!!!
Your account credentials have been saved in your
configuration directory at "./.lego/accounts".
Your account credentials have been saved in your Let's Encrypt
configuration directory at "./.lego/accounts".
You should make a secure backup of this folder now. This
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.
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.
[INFO] [example.com] acme: Obtaining bundled SAN certificate
[INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901
[INFO] [example.com] acme: Could not find solver for: tls-alpn-01
@ -65,5 +70,3 @@ _acme-challenge.example.com. 120 IN TXT "hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5
```
As mentioned, you can now remove the TXT record again.
'''

View file

@ -28,13 +28,7 @@ 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 --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
lego --email you@example.com --dns acme-dns --domains my.example.org run
```
@ -45,21 +39,12 @@ lego --dns "acme-dns" -d '*.example.com' -d example.com run
| 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" %}}).
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" %}}).
@ -67,7 +52,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://github.com/joohoi/acme-dns#api)
- [Go client](https://github.com/nrdcg/goacmedns)
- [Go client](https://github.com/cpu/goacmedns)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/acmedns/acmedns.toml -->

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