Compare commits

..

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

1736 changed files with 19213 additions and 69318 deletions

2
.gitattributes vendored
View file

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

View file

@ -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
@ -45,8 +44,6 @@ body:
- Through Bitnami
- Through 1Panel
- Through Zoraxy
- Through Certimate
- go install
- Other
validations:
required: true
@ -67,9 +64,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

@ -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
@ -24,20 +23,10 @@ body:
- Through Bitnami
- Through 1Panel
- Through Zoraxy
- Through Certimate
- go install
- Other
validations:
required: true
- type: input
id: version
attributes:
label: Effective version of lego
description: "`latest` or `dev` are not effective versions."
validations:
required: true
- type: textarea
id: description
attributes:

View file

@ -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
@ -40,23 +33,10 @@ body:
- Through Bitnami
- Through 1Panel
- Through Zoraxy
- Through Certimate
- go install
- Other
validations:
required: true
- type: dropdown
id: profile
attributes:
label: Who are you?
options:
- A customer of this DNS provider
- An employee of this DNS provider
- Other (please explain)
validations:
required: true
- type: input
id: provider-link
attributes:

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

@ -12,16 +12,20 @@ jobs:
runs-on: ubuntu-latest
env:
GO_VERSION: stable
HUGO_VERSION: 0.148.2
HUGO_VERSION: 0.131.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

@ -20,8 +20,13 @@ jobs:
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.62.0
HUGO_VERSION: 0.131.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,11 @@ jobs:
# https://goreleaser.com/ci/actions/
- name: Run GoReleaser
id: goreleaser
uses: goreleaser/goreleaser-action@v6
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,269 @@
version: "2"
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
settings:
gofumpt:
extra-rules: true
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
linters:
default: all
enable-all: true
disable:
- wsl # Deprecated
- bodyclose
- canonicalheader
- contextcheck
- 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)
- 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
- 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
- usestdlibvars # false-positive https://github.com/sashamelentyev/usestdlibvars/issues/96
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
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:
- 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
main:
deny:
- pkg: "github.com/instana/testify"
desc: not allowed
- pkg: "github.com/pkg/errors"
desc: Should be replaced by standard lib errors package
tagalign:
align: false
order:
- xml
- json
- yaml
- yml
- toml
- mapstructure
- url
godox:
keywords:
- FIXME
gocritic:
enabled-tags:
- diagnostic
- style
- performance
disabled-checks:
- paramTypeCombine # already handle by gofumpt.extra-rules
- whyNoLint # already handle by nonolint
- unnamedResult
- hugeParam
- sloppyReassign
- rangeValCopy
- octalLiteral
- ptrToRefParam
- appendAssign
- ruleguard
- httpNoBody
- exposedSyncMutex
revive:
rules:
- name: struct-tag
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
disabled: true
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
testifylint:
disable:
- require-error
- go-require
perfsprint:
err-error: true
errorf: true
sprintf1: true
strconcat: false
run:
timeout: 10m
output:
show-stats: true
sort-results: true
sort-order:
- linter
- file
issues:
exclude-generated: strict
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
exclude:
- 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked'
- 'exported (type|method|function) (.+) should have comment or be unexported'
- 'ST1000: at least one file in a package should have a package comment'
exclude-rules:
- path: (.+)_test.go
linters:
- funlen
- goconst
- maintidx
- path: (.+)_test.go
text: 'Error return value of `fmt.Fprintln` is not checked'
linters:
- errcheck
- path: providers/dns/dns_providers.go
linters:
- gocyclo
- path: certcrypto/crypto.go
text: '(tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable'
linters:
- gochecknoglobals
- path: challenge/dns01/nameserver.go
text: '(defaultNameservers|recursiveNameservers|fqdnSoaCache|muFqdnSoaCache) is a global variable'
linters:
- gochecknoglobals
- path: challenge/dns01/nameserver_.+.go
text: 'dnsTimeout is a global variable'
linters:
- gochecknoglobals
- path: challenge/dns01/nameserver_test.go
text: 'findXByFqdnTestCases is a global variable'
linters:
- gochecknoglobals
- path: challenge/http01/domain_matcher.go
text: 'string `Host` has \d occurrences, make it a constant'
linters:
- goconst
- path: challenge/http01/domain_matcher.go
text: 'cyclomatic complexity \d+ of func `parseForwardedHeader` is high'
linters:
- gocyclo
- path: challenge/http01/domain_matcher.go
text: "Function 'parseForwardedHeader' has too many statements"
linters:
- funlen
- path: challenge/tlsalpn01/tls_alpn_challenge.go
text: 'idPeAcmeIdentifierV1 is a global variable'
linters:
- gochecknoglobals
- path: log/logger.go
text: 'Logger is a global variable'
linters:
- gochecknoglobals
- path: 'e2e/(dnschallenge/)?[\d\w]+_test.go'
text: load is a global variable
linters:
- gochecknoglobals
- path: 'providers/dns/([\d\w]+/)*[\d\w]+_test.go'
text: 'envTest is a global variable'
linters:
- gochecknoglobals
- path: 'providers/http/([\d\w]+/)*[\d\w]+_test.go'
text: 'envTest is a global variable'
linters:
- gochecknoglobals
- path: providers/dns/namecheap/namecheap_test.go
text: 'testCases is a global variable'
linters:
- gochecknoglobals
- path: providers/dns/acmedns/acmedns_test.go
text: 'egTestAccount is a global variable'
linters:
- gochecknoglobals
- path: providers/http/memcached/memcached_test.go
text: 'memcachedHosts is a global variable'
linters:
- gochecknoglobals
- path: cmd/zz_gen_cmd_dnshelp.go
linters:
- gocyclo
- funlen
- path: providers/dns/checkdomain/internal/types.go
text: '`payed` is a misspelling of `paid`'
linters:
- misspell
- path: platform/tester/env_test.go
linters:
- thelper
- path: providers/dns/oraclecloud/oraclecloud_test.go
text: 'SA1019: x509.EncryptPEMBlock has been deprecated since Go 1.16'
linters:
- staticcheck
- path: providers/dns/sakuracloud/wrapper.go
text: 'mu is a global variable'
linters:
- gochecknoglobals
- path: cmd/cmd_renew.go
text: 'cyclomatic complexity \d+ of func `(renewForDomains|renewForCSR)` is high'
linters:
- gocyclo
- path: 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/servercow/internal/types.go
text: 'the methods of "Value" use pointer receiver and non-pointer receiver.'
linters:
- recvcheck
# Those elements have been replaced by non-exposed structures.
- path: providers/dns/linode/linode_test.go
linters:
- staticcheck
text: "SA1019: linodego\\.(DomainsPagedResponse|DomainRecordsPagedResponse) is deprecated"

View file

@ -42,10 +42,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,54 +51,98 @@ changelog:
- '(?i)^Detach v[\d|.]+'
- '(?i)^Prepare release v[\d|.]+'
release:
skip_upload: false
github:
owner: 'go-acme'
name: 'lego'
header: |
lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️
Everybody thinks that the others will donate, but in the end, nobody does.
So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev).
For key updates, see the [changelog](https://github.com/go-acme/lego/blob/HEAD/CHANGELOG.md#v{{ .Major }}{{ .Minor }}{{ .Patch }}).
archives:
- id: lego
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}'
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 }}"

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

@ -54,10 +54,10 @@ detach:
.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

134
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,114 +49,77 @@ Documentation is hosted live at https://go-acme.github.io/lego/.
Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.yml).
<!-- START DNS PROVIDERS LIST -->
<table><tr>
<td><a href="https://go-acme.github.io/lego/dns/com35/">35.com/三五互联</a></td>
<td><a href="https://go-acme.github.io/lego/dns/active24/">Active24</a></td>
<td><a href="https://go-acme.github.io/lego/dns/edgedns/">Akamai EdgeDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/alidns/">Alibaba Cloud DNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/aliesa/">AlibabaCloud ESA</a></td>
<td><a href="https://go-acme.github.io/lego/dns/allinkl/">all-inkl</a></td>
<td><a href="https://go-acme.github.io/lego/dns/alwaysdata/">Alwaysdata</a></td>
<td><a href="https://go-acme.github.io/lego/dns/lightsail/">Amazon Lightsail</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/route53/">Amazon Route 53</a></td>
<td><a href="https://go-acme.github.io/lego/dns/anexia/">Anexia CloudDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/safedns/">ANS SafeDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/artfiles/">ArtFiles</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/arvancloud/">ArvanCloud</a></td>
<td><a href="https://go-acme.github.io/lego/dns/auroradns/">Aurora DNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/autodns/">Autodns</a></td>
<td><a href="https://go-acme.github.io/lego/dns/axelname/">Axelname</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/azion/">Azion</a></td>
<td><a href="https://go-acme.github.io/lego/dns/azure/">Azure (deprecated)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/azuredns/">Azure DNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/baiducloud/">Baidu Cloud</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/beget/">Beget.com</a></td>
<td><a href="https://go-acme.github.io/lego/dns/binarylane/">Binary Lane</a></td>
<td><a href="https://go-acme.github.io/lego/dns/bindman/">Bindman</a></td>
<td><a href="https://go-acme.github.io/lego/dns/bluecat/">Bluecat</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/bluecatv2/">Bluecat v2</a></td>
<td><a href="https://go-acme.github.io/lego/dns/bookmyname/">BookMyName</a></td>
<td><a href="https://go-acme.github.io/lego/dns/brandit/">Brandit (deprecated)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/bunny/">Bunny</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/checkdomain/">Checkdomain</a></td>
<td><a href="https://go-acme.github.io/lego/dns/civo/">Civo</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/cloudru/">Cloud.ru</a></td>
<td><a href="https://go-acme.github.io/lego/dns/clouddns/">CloudDNS</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/cloudflare/">Cloudflare</a></td>
<td><a href="https://go-acme.github.io/lego/dns/cloudns/">ClouDNS</a></td>
<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/cloudxns/">CloudXNS (Deprecated)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/conoha/">ConoHa</a></td>
<td><a href="https://go-acme.github.io/lego/dns/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/cpanel/">CPanel/WHM</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>
</tr><tr>
<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>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dnsimple/">DNSimple</a></td>
<td><a href="https://go-acme.github.io/lego/dns/dnspod/">DNSPod (deprecated)</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dode/">Domain Offensive (do.de)</a></td>
<td><a href="https://go-acme.github.io/lego/dns/domeneshop/">Domeneshop</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/dreamhost/">DreamHost</a></td>
<td><a href="https://go-acme.github.io/lego/dns/duckdns/">Duck DNS</a></td>
</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/easydns/">EasyDNS</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/exec/">External program</a></td>
<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/googledomains/">Google Domains</a></td>
<td><a href="https://go-acme.github.io/lego/dns/hetzner/">Hetzner</a></td>
<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>
@ -182,36 +138,26 @@ If your DNS provider is not supported, please open an [issue](https://github.com
<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/iwantmyname/">iwantmyname</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>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/liquidweb/">Liquid Web</a></td>
<td><a href="https://go-acme.github.io/lego/dns/loopia/">Loopia</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/luadns/">LuaDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/mailinabox/">Mail-in-a-Box</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/manageengine/">ManageEngine CloudDNS</a></td>
<td><a href="https://go-acme.github.io/lego/dns/manual/">Manual</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/metaname/">Metaname</a></td>
<td><a href="https://go-acme.github.io/lego/dns/metaregistrar/">Metaregistrar</a></td>
<td><a href="https://go-acme.github.io/lego/dns/mijnhost/">mijn.host</a></td>
<td><a href="https://go-acme.github.io/lego/dns/mittwald/">Mittwald</a></td>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/myaddr/">myaddr.{tools,dev,io}</a></td>
<td><a href="https://go-acme.github.io/lego/dns/mittwald/">Mittwald</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>
@ -219,89 +165,79 @@ If your DNS provider is not supported, please open an [issue](https://github.com
<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>
</tr><tr>
<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>
</tr><tr>
<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>
</tr><tr>
<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>
</tr><tr>
<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>
</tr><tr>
<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/scaleway/">Scaleway</a></td>
<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/servercow/">Servercow</a></td>
<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>
</tr><tr>
<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/safedns/">UKFast SafeDNS</a></td>
<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/vegadns/">VegaDNS</a></td>
<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/webnames/">Webnames</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>
</tr><tr>
<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>
</tr><tr>
<td><a href="https://go-acme.github.io/lego/dns/zonomi/">Zonomi</a></td>
<td></td>
<td></td>
<td></td>
</tr></table>
<!-- END DNS PROVIDERS LIST -->
If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.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

@ -4,10 +4,10 @@ package sender
const (
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme/4.32.0"
ourUserAgent = "xenolf-acme/4.21.0"
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release
// NOTE: Update this with each tagged release.
ourUserAgentComment = "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

@ -29,7 +29,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 +52,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 +62,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

@ -65,26 +65,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 +89,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
}
@ -125,7 +108,6 @@ type CertifierOptions struct {
KeyType certcrypto.KeyType
Timeout time.Duration
OverallRequestLimit int
DisableCommonName bool
}
// Certifier A service to obtain/renew/revoke certificates.
@ -172,7 +154,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 +179,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 +220,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 +245,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 +264,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 +274,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 +296,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 +435,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 +452,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 +505,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 +530,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 +668,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 +681,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 +689,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()
@ -72,7 +71,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 +85,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

@ -81,7 +81,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 +89,6 @@ func ParseNameservers(servers []string) []string {
resolvers = append(resolvers, resolver)
}
}
return resolvers
}
@ -134,7 +132,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,7 +148,6 @@ func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) {
if err != nil {
return "", fmt.Errorf("[fqdn=%s] %w", fqdn, err)
}
return soa.zone, nil
}
@ -176,12 +172,13 @@ func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error)
}
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 +232,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 +267,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

@ -9,10 +9,6 @@ import (
"github.com/miekg/dns"
)
// defaultNameserverPort used by authoritative NS.
// This is for tests only.
var defaultNameserverPort = "53"
// PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready.
type PreCheckFunc func(fqdn, value string) (bool, error)
@ -29,7 +25,6 @@ func WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption {
}
// DisableCompletePropagationRequirement obsolete.
//
// Deprecated: use DisableAuthoritativeNssPropagationRequirement instead.
func DisableCompletePropagationRequirement() ChallengeOption {
return DisableAuthoritativeNssPropagationRequirement()
@ -126,7 +121,7 @@ func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) {
func checkNameserversPropagation(fqdn, value string, nameservers []string, addPort bool) (bool, error) {
for _, ns := range nameservers {
if addPort {
ns = net.JoinHostPort(ns, defaultNameserverPort)
ns = net.JoinHostPort(ns, "53")
}
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, false)
@ -141,11 +136,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, _ := checkNameserversPropagation(test.fqdn, test.value, test.ns, true)
assert.Equal(t, test.expected, ok, test.fqdn)
})
}
}
func TestCheckAuthoritativeNssErr(t *testing.T) {
testCases := []struct {
desc string
fqdn, value string
ns []string
error string
}{
{
desc: "TXT RR /w unexpected value",
fqdn: "8.8.8.8.asn.routeviews.org.",
value: "fe01=",
ns: []string{"asnums.routeviews.org."},
error: "did not return the expected TXT record",
},
{
desc: "No TXT RR",
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 := checkNameserversPropagation(test.fqdn, test.value, test.ns, true)
require.Error(t, err)
assert.Contains(t, err.Error(), test.error)
})
}
}

View file

@ -88,7 +88,6 @@ func (m *forwardedMatcher) matches(r *http.Request, domain string) bool {
}
host := fwds[0]["host"]
return matchDomain(host, domain)
}
@ -100,7 +99,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 +110,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
pos = i
inquote = false
}
continue
}
@ -121,7 +118,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
@ -141,7 +137,6 @@ func parseForwardedHeader(s string) (elements []map[string]string, err error) {
val = s[pos:i]
cur[key] = val
}
elements = append(elements, cur)
cur = make(map[string]string)
key = ""
@ -164,14 +159,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,7 +178,6 @@ func skipWS(s string, i int) int {
for isWS(rune(s[i+1])) {
i++
}
return i
}

View file

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

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)
@ -121,7 +120,6 @@ func (s *ProviderServer) serve(domain, token, keyAuth string) {
}
log.Infof("[%s] Served key authentication", domain)
return
}

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,13 +68,8 @@ type AccountsStorage struct {
// NewAccountsStorage Creates a new AccountsStorage.
func NewAccountsStorage(ctx *cli.Context) *AccountsStorage {
// TODO: move to account struct?
email := ctx.String(flgEmail)
userID := email
if userID == "" {
userID = userIDPlaceholder
}
// TODO: move to account struct? Currently MUST pass email.
email := getEmail(ctx)
serverURL, err := url.Parse(ctx.String(flgServer))
if err != nil {
@ -85,11 +79,10 @@ func NewAccountsStorage(ctx *cli.Context) *AccountsStorage {
rootPath := filepath.Join(ctx.String(flgPath), baseAccountsRootFolderName)
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
accountsPath := filepath.Join(rootPath, serverPath)
rootUserPath := filepath.Join(accountsPath, 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,12 +209,16 @@ 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) {
@ -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"
@ -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

@ -58,7 +58,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 +66,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"
@ -37,7 +36,7 @@ func createList() *cli.Command {
// fake email, needed by NewAccountsStorage
&cli.StringFlag{
Name: flgEmail,
Value: "",
Value: "unknown",
Hidden: true,
},
},
@ -68,7 +67,6 @@ func listCertificates(ctx *cli.Context) error {
if !names {
fmt.Println("No certificates found.")
}
return nil
}
@ -101,11 +99,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 +122,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 +129,6 @@ func listAccount(ctx *cli.Context) error {
}
var account Account
err = json.Unmarshal(data, &account)
if err != nil {
return err
@ -156,12 +147,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

@ -20,17 +20,25 @@ import (
// Flag names.
const (
flgRenewDays = "days"
flgRenewDynamic = "dynamic"
flgDays = "days"
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"
)
const (
renewEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
renewEnvCertDomain = "LEGO_CERT_DOMAIN"
renewEnvCertPath = "LEGO_CERT_PATH"
renewEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
renewEnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH"
renewEnvCertPEMPath = "LEGO_CERT_PEM_PATH"
renewEnvCertPFXPath = "LEGO_CERT_PFX_PATH"
)
func createRenew() *cli.Command {
return &cli.Command{
Name: "renew",
@ -39,37 +47,27 @@ func createRenew() *cli.Command {
Before: func(ctx *cli.Context) error {
// we require either domains or csr, but not both
hasDomains := len(ctx.StringSlice(flgDomains)) > 0
hasCsr := ctx.String(flgCSR) != ""
if hasDomains && hasCsr {
log.Fatalf("Please specify either --%s/-d or --%s/-c, but not both", flgDomains, flgCSR)
log.Fatal("Please specify either --%s/-d or --%s/-c, but not both", flgDomains, flgCSR)
}
if !hasDomains && !hasCsr {
log.Fatalf("Please specify --%s/-d (or --%s/-c if you already have a CSR)", flgDomains, flgCSR)
log.Fatal("Please specify --%s/-d (or --%s/-c if you already have a CSR)", flgDomains, flgCSR)
}
if ctx.Bool(flgForceCertDomains) && hasCsr {
log.Fatalf("--%s only works with --%s/-d, --%s/-c doesn't support this option.", flgForceCertDomains, flgDomains, flgCSR)
log.Fatal("--%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: flgDays,
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.",
Usage: "Do not use the renewalInfo endpoint (draft-ietf-acme-ari) to check if a certificate should be renewed.",
},
&cli.DurationFlag{
Name: flgARIWaitToRenewDuration,
@ -103,10 +101,6 @@ func createRenew() *cli.Command {
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
" If no match, the default offered chain will be used.",
},
&cli.StringFlag{
Name: flgProfile,
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
},
&cli.StringFlag{
Name: flgAlwaysDeactivateAuthorizations,
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
@ -115,11 +109,6 @@ func createRenew() *cli.Command {
Name: flgRenewHook,
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
},
&cli.DurationFlag{
Name: flgRenewHookTimeout,
Usage: "Define the timeout for the hook execution.",
Value: 2 * time.Minute,
},
&cli.BoolFlag{
Name: flgNoRandomSleep,
Usage: "Do not add a random sleep before the renewal." +
@ -144,9 +133,7 @@ func renew(ctx *cli.Context) error {
bundle := !ctx.Bool(flgNoBundle)
meta := map[string]string{
hookEnvAccountEmail: account.Email,
}
meta := map[string]string{renewEnvAccountEmail: account.Email}
// CSR
if ctx.IsSet(flgCSR) {
@ -171,10 +158,8 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
cert := certificates[0]
var (
ariRenewalTime *time.Time
replacesCertID string
)
var ariRenewalTime *time.Time
var replacesCertID string
var client *lego.Client
@ -202,7 +187,7 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
certDomains := certcrypto.ExtractDomains(cert)
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) &&
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) &&
(!forceDomains || slices.Equal(certDomains, domains)) {
return nil
}
@ -216,7 +201,6 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours()))
var privateKey crypto.PrivateKey
if ctx.Bool(flgReuseKey) {
keyBytes, errR := certsStorage.ReadFile(domain, keyExt)
if errR != nil {
@ -234,7 +218,6 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
if !isatty.IsTerminal(os.Stdout.Fd()) && !ctx.Bool(flgNoRandomSleep) {
// https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L472
const jitter = 8 * time.Minute
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
sleepTime := time.Duration(rnd.Int63n(int64(jitter)))
@ -242,7 +225,7 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
time.Sleep(sleepTime)
}
renewalDomains := slices.Clone(domains)
renewalDomains := domains
if !forceDomains {
renewalDomains = merge(certDomains, domains)
}
@ -255,7 +238,6 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
NotAfter: getTime(ctx, flgNotAfter),
Bundle: bundle,
PreferredChain: ctx.String(flgPreferredChain),
Profile: ctx.String(flgProfile),
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
}
@ -268,13 +250,11 @@ 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(flgRenewHook), meta)
}
func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
@ -298,10 +278,8 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
cert := certificates[0]
var (
ariRenewalTime *time.Time
replacesCertID string
)
var ariRenewalTime *time.Time
var replacesCertID string
var client *lego.Client
@ -325,7 +303,7 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
}
}
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) {
if ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgDays)) {
return nil
}
@ -343,7 +321,6 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
NotAfter: getTime(ctx, flgNotAfter),
Bundle: bundle,
PreferredChain: ctx.String(flgPreferredChain),
Profile: ctx.String(flgProfile),
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
}
@ -360,51 +337,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(flgRenewHook), 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 +370,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))
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,6 +389,24 @@ func getARIRenewalTime(ctx *cli.Context, cert *x509.Certificate, domain string,
return renewalTime
}
func addPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *CertificatesStorage) {
meta[renewEnvCertDomain] = domain
meta[renewEnvCertPath] = certsStorage.GetFileName(domain, certExt)
meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, keyExt)
if certRes.IssuerCertificate != nil {
meta[renewEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, issuerExt)
}
if certsStorage.pem {
meta[renewEnvCertPEMPath] = certsStorage.GetFileName(domain, pemExt)
}
if certsStorage.pfx {
meta[renewEnvCertPFXPath] = certsStorage.GetFileName(domain, pfxExt)
}
}
func merge(prevDomains, nextDomains []string) []string {
for _, next := range nextDomains {
if slices.Contains(prevDomains, next) {

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

@ -20,12 +20,9 @@ const (
flgMustStaple = "must-staple"
flgNotBefore = "not-before"
flgNotAfter = "not-after"
flgPrivateKey = "private-key"
flgPreferredChain = "preferred-chain"
flgProfile = "profile"
flgAlwaysDeactivateAuthorizations = "always-deactivate-authorizations"
flgRunHook = "run-hook"
flgRunHookTimeout = "run-hook-timeout"
)
func createRun() *cli.Command {
@ -35,16 +32,13 @@ func createRun() *cli.Command {
Before: func(ctx *cli.Context) error {
// we require either domains or csr, but not both
hasDomains := len(ctx.StringSlice(flgDomains)) > 0
hasCsr := ctx.String(flgCSR) != ""
if hasDomains && hasCsr {
log.Fatal("Please specify either --domains/-d or --csr/-c, but not both")
}
if !hasDomains && !hasCsr {
log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
}
return nil
},
Action: run,
@ -68,19 +62,11 @@ func createRun() *cli.Command {
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
Layout: time.RFC3339,
},
&cli.StringFlag{
Name: flgPrivateKey,
Usage: "Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.",
},
&cli.StringFlag{
Name: flgPreferredChain,
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
" If no match, the default offered chain will be used.",
},
&cli.StringFlag{
Name: flgProfile,
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
},
&cli.StringFlag{
Name: flgAlwaysDeactivateAuthorizations,
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
@ -89,24 +75,19 @@ func createRun() *cli.Command {
Name: flgRunHook,
Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
},
&cli.DurationFlag{
Name: flgRunHookTimeout,
Usage: "Define the timeout for the hook execution.",
Value: 2 * time.Minute,
},
},
}
}
const rootPathWarningMessage = `!!!! HEADS UP !!!!
Your account credentials have been saved in your
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 {
@ -143,12 +124,12 @@ func run(ctx *cli.Context) error {
certsStorage.SaveResource(cert)
meta := map[string]string{
hookEnvAccountEmail: account.Email,
renewEnvAccountEmail: account.Email,
}
addPathToMetadata(meta, cert.Domain, cert, certsStorage)
return launchHook(ctx.String(flgRunHook), ctx.Duration(flgRunHookTimeout), meta)
return launchHook(ctx.String(flgRunHook), meta)
}
func handleTOS(ctx *cli.Context, client *lego.Client) bool {
@ -158,12 +139,10 @@ func handleTOS(ctx *cli.Context, client *lego.Client) bool {
}
reader := bufio.NewReader(os.Stdin)
log.Printf("Please review the TOS at %s", client.GetToSURL())
for {
fmt.Println("Do you accept the TOS? Y/n")
text, err := reader.ReadString('\n')
if err != nil {
log.Fatalf("Could not read from console: %v", err)
@ -213,22 +192,20 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
// 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,
MustStaple: ctx.Bool(flgMustStaple),
PreferredChain: ctx.String(flgPreferredChain),
Profile: ctx.String(flgProfile),
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
}
if ctx.IsSet(flgPrivateKey) {
var err error
notBefore := ctx.Timestamp(flgNotBefore)
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(flgNotAfter)
if notAfter != nil {
request.NotAfter = *notAfter
}
return client.Certificate.Obtain(request)
@ -247,18 +224,8 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
NotAfter: getTime(ctx, flgNotAfter),
Bundle: bundle,
PreferredChain: ctx.String(flgPreferredChain),
Profile: ctx.String(flgProfile),
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
}
if ctx.IsSet(flgPrivateKey) {
var err error
request.PrivateKey, err = loadPrivateKey(ctx.String(flgPrivateKey))
if err != nil {
return nil, fmt.Errorf("load private key: %w", err)
}
}
return client.Certificate.ObtainForCSR(request)
}

View file

@ -16,7 +16,6 @@ const (
flgServer = "server"
flgAcceptTOS = "accept-tos"
flgEmail = "email"
flgDisableCommonName = "disable-cn"
flgCSR = "csr"
flgEAB = "eab"
flgKID = "kid"
@ -26,14 +25,12 @@ const (
flgPath = "path"
flgHTTP = "http"
flgHTTPPort = "http.port"
flgHTTPDelay = "http.delay"
flgHTTPProxyHeader = "http.proxy-header"
flgHTTPWebroot = "http.webroot"
flgHTTPMemcachedHost = "http.memcached-host"
flgHTTPS3Bucket = "http.s3-bucket"
flgTLS = "tls"
flgTLSPort = "tls.port"
flgTLSDelay = "tls.delay"
flgDNS = "dns"
flgDNSDisableCP = "dns.disable-cp"
flgDNSPropagationWait = "dns.propagation-wait"
@ -52,18 +49,6 @@ const (
flgUserAgent = "user-agent"
)
const (
envEAB = "LEGO_EAB"
envEABHMAC = "LEGO_EAB_HMAC"
envEABKID = "LEGO_EAB_KID"
envEmail = "LEGO_EMAIL"
envPath = "LEGO_PATH"
envPFX = "LEGO_PFX"
envPFXFormat = "LEGO_PFX_FORMAT"
envPFXPassword = "LEGO_PFX_PASSWORD"
envServer = "LEGO_SERVER"
)
func CreateFlags(defaultPath string) []cli.Flag {
return []cli.Flag{
&cli.StringSliceFlag{
@ -74,7 +59,7 @@ func CreateFlags(defaultPath string) []cli.Flag {
&cli.StringFlag{
Name: flgServer,
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,
},
@ -86,13 +71,8 @@ func CreateFlags(defaultPath string) []cli.Flag {
&cli.StringFlag{
Name: flgEmail,
Aliases: []string{"m"},
EnvVars: []string{envEmail},
Usage: "Email used for registration and recovery contact.",
},
&cli.BoolFlag{
Name: flgDisableCommonName,
Usage: "Disable the use of the common name in the CSR.",
},
&cli.StringFlag{
Name: flgCSR,
Aliases: []string{"c"},
@ -100,17 +80,17 @@ func CreateFlags(defaultPath string) []cli.Flag {
},
&cli.BoolFlag{
Name: flgEAB,
EnvVars: []string{envEAB},
EnvVars: []string{"LEGO_EAB"},
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
},
&cli.StringFlag{
Name: flgKID,
EnvVars: []string{envEABKID},
EnvVars: []string{"LEGO_EAB_KID"},
Usage: "Key identifier from External CA. Used for External Account Binding.",
},
&cli.StringFlag{
Name: flgHMAC,
EnvVars: []string{envEABHMAC},
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{
@ -125,7 +105,7 @@ func CreateFlags(defaultPath string) []cli.Flag {
},
&cli.StringFlag{
Name: flgPath,
EnvVars: []string{envPath},
EnvVars: []string{"LEGO_PATH"},
Usage: "Directory to use for storing the data.",
Value: defaultPath,
},
@ -138,11 +118,6 @@ func CreateFlags(defaultPath string) []cli.Flag {
Usage: "Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port.",
Value: ":80",
},
&cli.DurationFlag{
Name: flgHTTPDelay,
Usage: "Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge.",
Value: 0,
},
&cli.StringFlag{
Name: flgHTTPProxyHeader,
Usage: "Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy.",
@ -170,11 +145,6 @@ func CreateFlags(defaultPath string) []cli.Flag {
Usage: "Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port.",
Value: ":443",
},
&cli.DurationFlag{
Name: flgTLSDelay,
Usage: "Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge.",
Value: 0,
},
&cli.StringFlag{
Name: flgDNS,
Usage: "Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.",
@ -222,19 +192,19 @@ func CreateFlags(defaultPath string) []cli.Flag {
&cli.BoolFlag{
Name: flgPFX,
Usage: "Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.",
EnvVars: []string{envPFX},
EnvVars: []string{"LEGO_PFX"},
},
&cli.StringFlag{
Name: flgPFXPass,
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,
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,
@ -258,6 +228,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

@ -26,7 +26,6 @@ func main() {
}
var defaultPath string
cwd, err := os.Getwd()
if err == nil {
defaultPath = filepath.Join(cwd, ".lego")

View file

@ -2,7 +2,7 @@
package main
const defaultVersion = "v4.32.0+dev-detach"
const defaultVersion = "v4.21.0+dev-release"
var version = ""

View file

@ -1,18 +1,14 @@
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"
@ -40,7 +36,7 @@ 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
@ -54,7 +50,6 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy
KeyType: keyType,
Timeout: time.Duration(ctx.Int(flgCertTimeout)) * time.Second,
OverallRequestLimit: ctx.Int(flgOverallRequestLimit),
DisableCommonName: ctx.Bool(flgDisableCommonName),
}
config.UserAgent = getUserAgent(ctx)
@ -74,12 +69,6 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy
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()
@ -114,10 +103,17 @@ func getKeyType(ctx *cli.Context) certcrypto.KeyType {
}
log.Fatalf("Unsupported KeyType: %s", keyType)
return ""
}
func getEmail(ctx *cli.Context) string {
email := ctx.String(flgEmail)
if email == "" {
log.Fatalf("You have to pass an account (email address) to the program using --%s or -m", flgEmail)
}
return email
}
func getUserAgent(ctx *cli.Context) string {
return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", ctx.String(flgUserAgent), ctx.App.Version))
}
@ -128,7 +124,6 @@ func createNonExistingFolder(path string) error {
} else if err != nil {
return err
}
return nil
}
@ -137,12 +132,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 +157,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

@ -25,14 +25,14 @@ func setupChallenges(ctx *cli.Context, client *lego.Client) {
}
if ctx.Bool(flgHTTP) {
err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx), http01.SetDelay(ctx.Duration(flgHTTPDelay)))
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)))
err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx))
if err != nil {
log.Fatal(err)
}
@ -54,21 +54,18 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
if err != nil {
log.Fatal(err)
}
return ps
case ctx.IsSet(flgHTTPMemcachedHost):
ps, err := memcached.NewMemcachedProvider(ctx.StringSlice(flgHTTPMemcachedHost))
if err != nil {
log.Fatal(err)
}
return ps
case ctx.IsSet(flgHTTPS3Bucket):
ps, err := s3.NewHTTPProvider(ctx.String(flgHTTPS3Bucket))
if err != nil {
log.Fatal(err)
}
return ps
case ctx.IsSet(flgHTTPPort):
iface := ctx.String(flgHTTPPort)
@ -85,14 +82,12 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
if header := ctx.String(flgHTTPProxyHeader); header != "" {
srv.SetProxyHeader(header)
}
return srv
case ctx.Bool(flgHTTP):
srv := http01.NewProviderServer("", "")
if header := ctx.String(flgHTTPProxyHeader); header != "" {
srv.SetProxyHeader(header)
}
return srv
default:
log.Fatal("Invalid HTTP challenge options.")

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

@ -7,34 +7,23 @@ chapter: false
Let's Encrypt client and ACME library written in Go.
{{% notice important %}}
lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️
This project is not owned by a company. I'm not an employee of a company.
I don't have gifted domains/accounts from DNS companies.
I've been maintaining it for about 10 years.
{{% /notice %}}
## Features
- ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html)
- Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS 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
- 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.

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" -d '*.example.com' -d example.com 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" %}}).
## 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 -->

View file

@ -1,69 +0,0 @@
---
title: "Active24"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: active24
dnsprovider:
since: "v4.23.0"
code: "active24"
url: "https://www.active24.cz"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/active24/active24.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [Active24](https://www.active24.cz).
<!--more-->
- Code: `active24`
- Since: v4.23.0
Here is an example bash command using the Active24 provider:
```bash
ACTIVE24_API_KEY="xxx" \
ACTIVE24_SECRET="yyy" \
lego --dns active24 -d '*.example.com' -d example.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `ACTIVE24_API_KEY` | API key |
| `ACTIVE24_SECRET` | Secret |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `ACTIVE24_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `ACTIVE24_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `ACTIVE24_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `ACTIVE24_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://rest.active24.cz/v2/docs)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/active24/active24.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

@ -28,13 +28,13 @@ Here is an example bash command using the Alibaba Cloud DNS provider:
```bash
# Setup using instance RAM role
ALICLOUD_RAM_ROLE=lego \
lego --dns alidns -d '*.example.com' -d example.com run
lego --email you@example.com --dns alidns -d '*.example.com' -d example.com run
# Or, using credentials
ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \
ALICLOUD_SECRET_KEY=your-secret-key \
ALICLOUD_SECURITY_TOKEN=your-sts-token \
lego --dns alidns - -d '*.example.com' -d example.com run
lego --email you@example.com --dns alidns - -d '*.example.com' -d example.com run
```
@ -45,7 +45,7 @@ lego --dns alidns - -d '*.example.com' -d example.com run
| Environment Variable Name | Description |
|-----------------------|-------------|
| `ALICLOUD_ACCESS_KEY` | Access key ID |
| `ALICLOUD_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance) |
| `ALICLOUD_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/doc-detail/54579.htm) |
| `ALICLOUD_SECRET_KEY` | Access Key secret |
| `ALICLOUD_SECURITY_TOKEN` | STS Security Token (optional) |
@ -57,12 +57,10 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `ALICLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |
| `ALICLOUD_LINE` | Line (Default: default) |
| `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `ALICLOUD_REGION_ID` | Region ID (Default: cn-hangzhou) |
| `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |
| `ALICLOUD_HTTP_TIMEOUT` | API request timeout |
| `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check |
| `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
| `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge |
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" %}}).
@ -73,7 +71,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://www.alibabacloud.com/help/en/alibaba-cloud-dns/latest/api-alidns-2015-01-09-dir-parsing-records)
- [Go client](https://github.com/alibabacloud-go/alidns-20150109)
- [Go client](https://github.com/aliyun/alibaba-cloud-sdk-go)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/alidns/alidns.toml -->

View file

@ -1,78 +0,0 @@
---
title: "AlibabaCloud ESA"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: aliesa
dnsprovider:
since: "v4.29.0"
code: "aliesa"
url: "https://www.alibabacloud.com/en/product/esa"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/aliesa/aliesa.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [AlibabaCloud ESA](https://www.alibabacloud.com/en/product/esa).
<!--more-->
- Code: `aliesa`
- Since: v4.29.0
Here is an example bash command using the AlibabaCloud ESA provider:
```bash
# Setup using instance RAM role
ALIESA_RAM_ROLE=lego \
lego --dns aliesa -d '*.example.com' -d example.com run
# Or, using credentials
ALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \
ALIESA_SECRET_KEY=your-secret-key \
ALIESA_SECURITY_TOKEN=your-sts-token \
lego --dns aliesa - -d '*.example.com' -d example.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `ALIESA_ACCESS_KEY` | Access key ID |
| `ALIESA_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance) |
| `ALIESA_SECRET_KEY` | Access Key secret |
| `ALIESA_SECURITY_TOKEN` | STS Security Token (optional) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `ALIESA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `ALIESA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `ALIESA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `ALIESA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-overview?spm=a2c63.p38356.help-menu-2673927.d_6_0_0.20b224c28PSZDc#:~:text=DNS-,DNS%20records,-DNS%20records)
- [Go client](https://github.com/alibabacloud-go/esa-20240910)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/aliesa/aliesa.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

@ -28,7 +28,7 @@ Here is an example bash command using the all-inkl provider:
```bash
ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \
ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \
lego --dns allinkl -d '*.example.com' -d example.com run
lego --email you@example.com --dns allinkl -d '*.example.com' -d example.com run
```
@ -49,9 +49,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `ALL_INKL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `ALL_INKL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `ALL_INKL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `ALL_INKL_HTTP_TIMEOUT` | API request timeout |
| `ALL_INKL_POLLING_INTERVAL` | Time between DNS propagation check |
| `ALL_INKL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
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" %}}).

View file

@ -1,68 +0,0 @@
---
title: "Alwaysdata"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: alwaysdata
dnsprovider:
since: "v4.31.0"
code: "alwaysdata"
url: "https://alwaysdata.com/"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/alwaysdata/alwaysdata.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [Alwaysdata](https://alwaysdata.com/).
<!--more-->
- Code: `alwaysdata`
- Since: v4.31.0
Here is an example bash command using the Alwaysdata provider:
```bash
ALWAYSDATA_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
lego --dns alwaysdata -d '*.example.com' -d example.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `ALWAYSDATA_API_KEY` | API Key |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `ALWAYSDATA_ACCOUNT` | Account name |
| `ALWAYSDATA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
| `ALWAYSDATA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
| `ALWAYSDATA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
| `ALWAYSDATA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here]({{% ref "dns#configuration-and-credentials" %}}).
## More information
- [API documentation](https://help.alwaysdata.com/en/api/resources/)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/alwaysdata/alwaysdata.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

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