Compare commits

...

731 commits

Author SHA1 Message Date
pchanvallon
42863478c5
azure: new implementation based on the new API client (#1830)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-07-12 18:02:06 +00:00
Ludovic Fernandez
b4a2913a62
Add DNS provider for Efficient IP (#1945) 2023-07-01 12:37:40 +02:00
Michael Braunöder
2d4dc8fb38
Add DNS provider for RcodeZero (#1947)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-07-01 00:51:17 +02:00
darron froese
9d193056f7
cloudflare: update api client to v0.70.0 (#1931)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-06-30 22:10:54 +02:00
Ludovic Fernandez
fdfcaabde6
chore: update dependencies (#1948) 2023-06-30 20:42:28 +02:00
Fernandez Ludovic
7f27124c09 tests: fix flaky 2023-06-27 23:42:01 +02:00
Ludovic Fernandez
2c0c8a03ed
fix: ARI explanationURL (#1944) 2023-06-23 11:44:40 +02:00
Ludovic Fernandez
2471d4582f
fix: list command (#1943) 2023-06-22 23:20:36 +02:00
Fernandez Ludovic
7498a81a13 Detach v4.12.3 2023-06-20 00:08:43 +02:00
Fernandez Ludovic
60658013c3 Prepare release v4.12.3 2023-06-20 00:08:17 +02:00
Fernandez Ludovic
5e632b5b91 chore: reduce disk space consumption during release 2023-06-20 00:05:42 +02:00
Fernandez Ludovic
81b3de0ae1 Detach v4.12.2 2023-06-19 17:40:43 +02:00
Fernandez Ludovic
f8d5b6a111 Prepare release v4.12.2 2023-06-19 17:40:43 +02:00
Ludovic Fernandez
c900b49051
dnsmadeeasy: fix DeleteRecord (#1939) 2023-06-19 15:28:12 +00:00
gsato-godaddy
32b083e537
fix: read status code from response (#1937) 2023-06-19 17:02:21 +02:00
Ludovic Fernandez
c81ccf042e
docs: remove wrong timeout documentation for exec (#1936) 2023-06-13 14:05:02 +02:00
Fernandez Ludovic
60d2b55dd8 Detach v4.12.1 2023-06-06 18:29:00 +02:00
Fernandez Ludovic
ad1f4da729 Prepare release v4.12.1 2023-06-06 18:29:00 +02:00
Ludovic Fernandez
4954d43a33
pdns: fix record value (#1929) 2023-06-06 15:49:59 +00:00
Ludovic Fernandez
dac3a657d2
chore: update linter (#1927) 2023-06-02 14:35:17 +00:00
Fernandez Ludovic
35d80edcae Detach v4.12.0 2023-05-28 17:35:20 +02:00
Fernandez Ludovic
1d0af02ea5 Prepare release v4.12.0 2023-05-28 17:35:20 +02:00
Ludovic Fernandez
c341e6a381
feat: support custom duration for certificate (#1925) 2023-05-28 16:45:48 +02:00
Fernandez Ludovic
8bf0cee70e docs: replace godoc.org references 2023-05-28 10:36:37 +02:00
Fernandez Ludovic
51fd75ef83 docs: update the list of supported RFC 2023-05-28 10:31:08 +02:00
Samantha
8df8c7f08d
Initial ACME Renewal Info (ARI) Implementation (#1912)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-05-27 17:05:22 +02:00
Ludovic Fernandez
8a7fb25edc
websupport: simplify errors (#1924) 2023-05-27 12:19:14 +02:00
Julien Vdbc
8d8a3c1ccd
route53: pass ExternalID property to STS:AssumeRole API operation (#1918) 2023-05-26 20:26:40 +00:00
leonexcc
9bd5890c40
autodns: fixes wrong zone in api call if CNAME is used (#1923) 2023-05-26 22:03:23 +02:00
Rui Chen
91bda258a9
docs: add homebrew installation (#1920) 2023-05-23 22:44:33 +00:00
Chris Marchesi
fc8e49b340
route53: minor correction to documentation (#1916) 2023-05-17 16:42:06 +00:00
Fernandez Ludovic
1a09098d23 doc: improve readme 2023-05-15 11:59:38 +02:00
Aofei Sheng
510aa8f871
fix: archive only domain-related files on revoke (#1874)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-05-06 00:14:07 +02:00
Ludovic Fernandez
3d44184074
Add DNS provider for Derak Cloud (#1909) 2023-05-05 12:37:54 +02:00
Fernandez Ludovic
67d80a1445 chore: update linter 2023-05-05 09:51:42 +02:00
Ludovic Fernandez
aeec5be129
chore: refactor clients (#1868) 2023-05-05 09:49:38 +02:00
alexzorin
0c1303c1bd
docs: fix gdomainsgoogledomains (#1911) 2023-05-03 01:13:09 +00:00
Fernandez Ludovic
c4c401630a Detach v4.11.0 2023-05-02 20:58:58 +02:00
Fernandez Ludovic
659c938975 Prepare release v4.11.0 2023-05-02 20:58:58 +02:00
orangepizza
5a70c3661d
feat: support for certificate with raw IP SAN (RFC8738) (#1838) 2023-05-02 19:02:18 +02:00
Scott Peshak
dfcf4412f7
feat: add LEGO_CERT_PEM_PATH and LEGO_CERT_PFX_PATH to run hook (#1907) 2023-05-02 16:33:13 +00:00
Cole Helbling
c2e4749d17
doc: fix wrong env var name (#1908) 2023-05-01 23:48:44 +00:00
Christian Grøntved
9db046581b
Add Brandit.com as DNS provider (#1890) 2023-04-14 09:44:20 +02:00
Ludovic Fernandez
3b70a85cf7
rimuhosting: fix API base URL (#1894) 2023-04-14 09:00:38 +02:00
Steve De Jong
1a16d1ab9b
ultradns: update link (#1883) 2023-04-07 22:28:44 +02:00
Frederic Hemberger
029e4a4b0d
docs: Add ArchLinux package, fix AUR link (#1884) 2023-04-07 12:27:09 +00:00
Bodenhaltung
86d9e5632c
docs: update dnshomede API documentation (#1871)
Co-authored-by: Bodenhaltung <norbert+git@bodenhaltung.com>
2023-03-18 20:11:54 +00:00
Amir Omidi
64afecac21
Add Google Domains as DNS provider (#1839)
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
2023-03-09 21:22:00 +00:00
Amir Omidi
552339425e
gcloud: update google APIs to latest version (#1866)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-03-09 10:06:10 +01:00
Ludovic Fernandez
e638d79643
chore: replace GetRecord by GetChallengeInfo (#1863) 2023-03-07 09:39:05 +01:00
Paul Dee
2c9f6b19dd
docs: --help text fixes (#1860) 2023-03-02 22:14:25 +00:00
Paul Dee
56a419b0ea
docs: --pem and --pfx text disambiguation (#1859) 2023-03-02 18:29:29 +00:00
Ludovic Fernandez
7dca695fcb
chore: update Pebble (#1857) 2023-03-01 23:56:19 +01:00
Paul Dee
27a2c014f7
feat: add RSA 3072 (#1856) 2023-03-01 15:27:20 +00:00
Ludovic Fernandez
1b98b20058
chore: update linter (#1852) 2023-02-28 09:08:15 +01:00
Technofab
052adf246e
Add DNS provider for Bunny (#1848)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-02-27 00:54:39 +00:00
Ludovic Fernandez
cfafc8cd8a
Add DNS provider for Nodion (#1844) 2023-02-27 00:11:03 +00:00
Ludovic Fernandez
b66c15b2cf
Add DNS provider for Plesk (#1837) 2023-02-27 00:53:57 +01:00
Fernandez Ludovic
5053426051 Detach v4.10.2 2023-02-26 14:28:37 +01:00
Fernandez Ludovic
a53549fa24 Prepare release v4.10.2 2023-02-26 14:28:19 +01:00
Fernandez Ludovic
18c6d8fa03 chore: use buildx for cross-platform image builds 2023-02-26 14:27:16 +01:00
Fernandez Ludovic
3c3da936f8 Detach v4.10.1 2023-02-25 18:49:34 +01:00
Fernandez Ludovic
7153b4e7c4 Prepare release v4.10.1 2023-02-25 18:49:34 +01:00
Ludovic Fernandez
56fda1abd8
acmedns: fix CNAME support (#1847) 2023-02-24 09:59:23 +01:00
Sascha Marcel Hacker
1e2793f9ec
dynu: fix subdomain support (#1842)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-02-19 23:59:17 +00:00
Fernandez Ludovic
532788d42a chore: improve issue templates 2023-02-15 14:47:08 +01:00
Fernandez Ludovic
4d51458855 Detach v4.10.0 2023-02-10 10:06:35 +01:00
Fernandez Ludovic
9b0c869f84 Prepare release v4.10.0 2023-02-10 10:06:35 +01:00
Ludovic Fernandez
fd0eb831a8
chore: update to go1.19 (#1835) 2023-02-09 16:19:58 +00:00
Roman Pedchenko
e76a104315
namesilo: add cleanup before add a DNS record (#1833) 2023-02-05 20:54:41 +00:00
Ludovic Fernandez
4a14c315a2
alidns: filter on record type (#1829) 2023-02-03 13:48:13 +00:00
Ludovic Fernandez
ad612e639e
feat: Add DNS provider for Websupport (#1824) 2023-02-01 14:22:25 +01:00
Fernandez Ludovic
401bf5b4bd fix: dnsHome.de documentation 2023-01-31 17:11:17 +01:00
Ludovic Fernandez
3d32e6a664
Add DNS provider for dnsHome.de (#1821) 2023-01-27 09:28:38 +00:00
Ludovic Fernandez
9b0b4cbbd6
hetzner: improve zone ID detection (#1815) 2023-01-23 17:38:18 +01:00
saltiyazan
5ff9695c09
route53: add CLI example (#1814) 2023-01-20 15:32:33 +00:00
KuberDriver
db0257c6f8
gcloud: add CLI example (#1808) 2023-01-18 08:55:46 +00:00
Chris Marchesi
0122506c23
route53: Allow static credentials to be supplied (#1746)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-01-15 22:50:35 +00:00
Dominik Menke
07d957fdc1
docs: expand description for --dns.resolvers (#1786) 2023-01-12 14:07:31 +00:00
AntonyRohithAkash
afcb2bc6d1
Add DNS provider for UltraDNS (#1806)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-01-11 14:57:25 +01:00
Melchior NOGUES
7f8305e8d8
pdns: fix usage of notify only when zone kind is Master or Slave (#1781)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-01-08 14:12:06 +00:00
Dominik Menke
091e03f071
docs: generate CLI help (#1785) 2023-01-08 13:53:15 +00:00
Sergey Kutovoy
1cad41db65
doc: fix cron schedule in example (#1803) 2023-01-07 17:04:35 +00:00
Alexander Beyn
e982d5a77f
hurricane: fix typo in example command (#1799)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2023-01-05 09:51:35 +01:00
Nicola Murino
fd54758bba
chore: update go-jose package to latest (#1797) 2023-01-04 10:32:15 +00:00
h0m1
33b3399b3a
luadns: removed dot suffix from authzone while searching for zone (#1796) 2023-01-04 11:09:51 +01:00
Fernandez Ludovic
368427a621 fix: skip reg.ru tests on CI 2023-01-03 02:45:25 +01:00
TeoGoddet
a824cab316
infomaniak: CNAME support (#1790)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-12-24 02:24:11 +01:00
Feng.YJ
339b62fdcd
tencentcloud: support punycode domain (#1788) 2022-12-18 17:54:03 +01:00
Ali Afsharzadeh
6695fcc873
chore: upgrade actions (#1784) 2022-12-10 11:17:05 +01:00
Thomas Hofmann
c754882704
ibmcloud: add support for subdomains (#1779)
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
2022-12-06 17:51:44 +00:00
Ludovic Fernandez
7095aa66a2
fix: return an error when extracting record name (#1778) 2022-12-04 15:11:27 +00:00
Ludovic Fernandez
9ec5c8a18f
fix: return an error when extracting record name (#1766) 2022-11-28 17:53:13 +00:00
Ludovic Fernandez
300f42d084
arvancloud: replace arvancloud.com by arvancloud.ir (#1771) 2022-11-28 16:15:56 +00:00
Ludovic Fernandez
a7b11e0447
chore: update linter (#1767)
Co-authored-by: Dominik Menke <git@dmke.org>
2022-11-27 19:04:28 +00:00
Ali Afsharzadeh
2431f83100
Add DNS provider for Liara (#1741) 2022-11-26 02:06:08 +00:00
Fernandez Ludovic
90e478ef23 Detach v4.9.1 2022-11-25 19:29:49 +01:00
Fernandez Ludovic
df79f13022 Prepare release v4.9.1 2022-11-25 19:29:49 +01:00
Ludovic Fernandez
001dd80f6a
cname: add log about CNAME entries (#1764) 2022-11-25 18:41:38 +01:00
Ludovic Fernandez
6cc21f2753
fix: CNAME support (#1735) 2022-11-25 18:12:21 +01:00
Ludovic Fernandez
1b56aa0d2f
regru: improve error handling (#1750) 2022-11-10 02:23:04 +01:00
Ludovic Fernandez
b6b8e57b13
chore: update actions/cache to v3 (#1740) 2022-11-02 17:00:38 +01:00
Paul Galow
6c75aaa9a4
Route 53: Update IAM policy example to grant least privilege access (#1727) 2022-10-12 18:42:06 +00:00
Jürgen Brunink
7cfa075581
hurricane: fix CNAME support (#1734)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-10-11 14:40:00 +02:00
Jürgen Brunink
5e37ee3822
cname: stop trying to traverse cname if none have been found (#1733)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-10-10 20:43:33 +02:00
Ludovic Fernandez
79fcc56402
duckdns: fix CNAME support (#1732) 2022-10-10 08:42:27 +00:00
Omar Aloraini
87ee39a1dc
oraclecloud: use fqdn to resolve zone (#1731) 2022-10-10 07:05:11 +02:00
Fernandez Ludovic
615e601d78 Detach v4.9.0 2022-10-03 22:51:15 +02:00
Fernandez Ludovic
f916826c87 Prepare release v4.9.0 2022-10-03 22:51:15 +02:00
Matthieu Berthomé
ca6784a824
digitalocean: configurable base URL (#1687)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-10-03 13:36:47 +00:00
Ludovic Fernandez
d5a18dc2d5
docs: un-deprecate Yandex (#1719) 2022-09-19 09:40:45 +00:00
Ludovic Fernandez
af37b94b38
feat: take out CNAME support from experimental features (#1718) 2022-09-19 11:21:35 +02:00
Ludovic Fernandez
0d7ee5e750
luadns: fix cname support (#1717) 2022-09-17 18:04:47 +00:00
Ricardo
e7ffbe77f8
Remove embedded issuer certificates from issued certificate if bundle is false (#1655)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-09-17 17:01:08 +00:00
Luca Casonato
e96d2c08fe
gcloud: update golang.org/x/oauth2 (#1673)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-09-17 16:44:45 +00:00
Daniel Albert
0bcada7921
pdns: notify secondary servers after updates (#1679)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-09-17 16:20:41 +00:00
tony-defa
60bd1c205e
allinkl: removed deprecated sha1 hashing (#1713) 2022-09-17 16:02:48 +00:00
danthegoodman1
fd8a9f86ec
lib: add recursive CNAME lookup support (#1677) 2022-09-17 17:47:50 +02:00
Ludovic Fernandez
4c823ed3c0
lightsail: cleanup (#1716) 2022-09-17 09:22:42 +00:00
Ludovic Fernandez
bc3500a951
feat: deprecate Yandex and DNSPod (#1715) 2022-09-17 11:04:54 +02:00
Engin Diri
62a0d11f83
Add DNS provider for CIVO (#1707) 2022-09-09 12:33:24 +00:00
nxadm
438c2f1479
docs: fix typo (#1711) 2022-09-06 19:11:36 +02:00
Ludovic Fernandez
a927c96aae
chore: update dependencies (#1709) 2022-09-03 17:53:23 +02:00
Andrew Kluev
8fe27e0cc3
Add DNS provider for VK Cloud (#1706)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-09-02 22:56:10 +02:00
Ludovic Fernandez
d99c75a08d
chore: update linter (#1708) 2022-09-02 09:05:52 +02:00
Andrew Kluev
be0c6c743d
Add DNS provider for YandexCloud (#1694)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-09-01 21:05:43 +00:00
Fernandez Ludovic
11ac3da542 infoblox: fix documentation 2022-08-25 16:21:04 +02:00
Ludovic Fernandez
ca18391d25
loopia: add configuratble API endpoint (#1704) 2022-08-24 19:32:39 +00:00
Ludovic Fernandez
d7b0593eb6
infoblox: fix documentation (#1702) 2022-08-24 20:15:27 +02:00
Ludovic Fernandez
c45b756237
fix: flaky tests (#1701) 2022-08-22 21:31:06 +02:00
Ludovic Fernandez
409b3c9959
feat: update to go1.18 (#1700) 2022-08-22 15:05:31 +00:00
Predrag Janosevic
791eb3c640
exoscale: migrate to API v2 endpoints (#1682) 2022-08-04 14:35:02 +00:00
Kazumichi Yamamoto
d344d8b87b
sakuracloud: update api client library (#1690) 2022-08-04 13:56:46 +00:00
Manuel Schmidbauer
0aeafc68cf
njalla: fix record id unmarshal error (#1686) 2022-08-04 01:06:25 +02:00
Ludovic Fernandez
978eeed867
docs: update install from sources (#1688) 2022-08-04 00:15:33 +02:00
libla
c270c239b5
tencentcloud: fix subdomain error (#1678)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-07-17 14:40:24 +02:00
Nick Been
6a53daad64
auroradns: update authentification (#1669) 2022-07-04 15:27:27 +02:00
Fernandez Ludovic
f7d8c9668e Detach v4.8.0 2022-06-30 19:21:33 +02:00
Fernandez Ludovic
175164fb47 Prepare release v4.8.0 2022-06-30 19:21:33 +02:00
john dev
806ef243d1
hetzner: set min TTL to 60s (#1666) 2022-06-30 08:55:08 +00:00
Matthew McPherrin
0e614c1d0b
feat: add a --user-agent flag to lego-cli (#1664) 2022-06-29 20:56:56 +02:00
Alexey Bulenkov
525ceda150
docs: fixed invalid environment parameter name in example (#1662) 2022-06-28 00:02:08 +00:00
Ludovic Fernandez
072ae4a95e
docs: new logo (#1660)
Co-authored-by: Dominik Menke <dom@digineo.de>
2022-06-22 16:57:50 +00:00
Dominik Menke
3e9e6ac847
cli/renew: skip random sleep if stdout is a terminal (#1659) 2022-06-17 11:15:20 +02:00
Dominik Menke
85f616f85b
docs: refactoring and cleanup (#1658) 2022-06-17 00:25:42 +02:00
Ludovic Fernandez
257dfa777e
feat: sleep at renewal (#1657)
Co-authored-by: Dominik Menke <git@dmke.org>
2022-06-15 07:08:24 +00:00
Ludovic Fernandez
88a2bab2d9
Add DNS provider for Variomedia (#1654) 2022-06-06 20:22:34 +00:00
Katie
638745cbce
Add NearlyFreeSpeech DNS Provider (#1652)
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
2022-06-06 22:05:43 +02:00
Fernandez Ludovic
9a95c86c4a Detach v4.7.0 2022-05-28 11:43:56 +02:00
Fernandez Ludovic
364439a7c6 Prepare release v4.7.0 2022-05-28 11:43:56 +02:00
nonchan
d95b487af1
route53: add assume role ARN (#1650)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-05-27 18:32:39 +02:00
Fernandez Ludovic
9b029d53d7 chore: update CI 2022-05-20 22:42:26 +02:00
Manabu Sonoda
81f1fabad4
Add DNS Provider for IIJ DNS Platform Service (#1646) 2022-05-20 20:30:19 +00:00
David Dymko
fdb11d7cfe
vultr: bump client from 2.7.1 to 2.16.0 (#1640) 2022-05-07 16:17:26 +00:00
Ludovic Fernandez
d8cc2cef46
chore: update logrus (#1639) 2022-05-07 11:18:10 +02:00
Gregory Schier
122cdb64e4
Add Vercel DNS provider (#1635)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-04-29 20:50:39 +00:00
Ludovic Fernandez
b2a64f0489
chore: update CI (#1636) 2022-04-29 20:57:00 +02:00
Ludovic Fernandez
5228f6dbcf
rfc2136: fix TSIG secret (#1633) 2022-04-27 17:46:39 +02:00
lipovmi
3694559777
gcore: change dns api url (#1629) 2022-04-26 00:09:34 +02:00
Matt Sporleder
e83220c1c5
dnsimple: add debug option (#1630) 2022-04-26 00:08:38 +02:00
Ludovic Fernandez
6b8d5a0afc
bluecat: rewrite provider implementation (#1627) 2022-04-20 01:00:57 +02:00
Charlie Chiang
6641400f41
tencentcloud: fix InvalidParameter.DomainInvalid error when using DNS challenges (#1616) 2022-04-03 17:53:11 +02:00
Ludovic Fernandez
2c0aaf7fef
feat: add LEGO_CERT_PEM_PATH and LEGO_CERT_PFX_PATH (#1609) 2022-03-21 09:36:28 +01:00
Ludovic Fernandez
b4a942def7
docs: fix link in the httpreq page (#1602) 2022-02-24 00:07:58 +01:00
Chris Smith
15762c2277
fix: panic in certcrypto.ParsePEMPrivateKey (#1596) 2022-02-20 15:23:52 +01:00
Johannes Kastl
86b05a9701
docs: fix typos for usage hostingde example (#1595) 2022-02-19 19:36:17 +01:00
Ludovic Fernandez
7d9176bd0d
chore: migrate to github.com/urfave/cli/v2 (#1590) 2022-02-13 12:28:51 +01:00
Fernandez Ludovic
6d474ebb65 chore: add funding 2022-02-12 12:44:18 +01:00
Ludovic Fernandez
4b2f870f3f
Add DNS provider for iwantmyname (#1588) 2022-02-12 12:33:11 +01:00
Uwe Koloska
7f69bdbbeb
docs: fix all-inkl configuration (#1576) 2022-01-30 21:51:04 +01:00
Ludovic Fernandez
3a588ea872
chore: update linter (#1570) 2022-01-27 09:18:53 +01:00
Fernandez Ludovic
c1c11a0ee4 Detach v4.6.0 2022-01-19 18:53:10 +01:00
Fernandez Ludovic
3ad2fa7acb Prepare release v4.6.0 2022-01-19 18:53:10 +01:00
Ludovic Fernandez
6b58241295
feat: extended support of cert pool (#1565)
- `LEGO_CA_CERTIFICATES` now accepts multiple file paths to be added by using `os.PathListSeparator` (`:` on POSIX, `;` on Windows) as a separator.
- `LEGO_CA_SYSTEM_CERT_POOL` initiates the cert pool with a copy of the system cert pool.
2022-01-18 23:22:04 +01:00
Fernandez Ludovic
bbec0a6e50 chore: add link to Discussions 2022-01-18 00:51:24 +01:00
Knut-Erik Johnsen
6dd5d1f814
azure: add support for Azure Private Zone DNS (#1561)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2022-01-17 10:05:59 +01:00
Ludovic Fernandez
27bb3f2256
joker: explains user and password for SVC mode (#1555) 2021-12-30 13:31:31 +01:00
Carlos Alberto Costa Beppler
a6855cb69f
Added a --pfx, and --pfx.pass option to generate a PKCS#12 (.pfx) file. (#1387) 2021-12-25 19:36:01 +01:00
Fernandez Ludovic
f4b153f26f test: force to have a reproducible context for oracle provider tests 2021-12-23 03:57:21 +01:00
Baran
6907a39266
Add DNS Provider for Tencent Cloud (#1527) 2021-12-22 15:58:35 +00:00
Dane
0324783e09
Add DNS provider for UKFast SafeDNS (#1545)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-12-20 14:15:49 +00:00
Sepehr Hosseini
f7c287e520
porkbun: fix typo in dns example (#1550) 2021-12-19 01:10:41 +00:00
Trey Jones
0f3a8351de
lib: added uds capability to http challenge server (#1485)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-12-09 18:27:37 +01:00
Ludovic Fernandez
2b20b13fad
mythicbeasts: fix token expiration (#1539) 2021-12-03 16:19:00 +01:00
Stuart Auld
719341250f
Extend validity of TLS-ALPN-01 certificates to 365 days (#1534) 2021-11-27 04:22:59 +00:00
Fernandez Ludovic
9114733772 test: change LTD for NXDOMAIN 2021-11-22 21:08:38 +01:00
Ludovic Fernandez
2de6e0b0c8
chore: update linter (#1524) 2021-11-03 20:39:12 +01:00
Ludovic Fernandez
d2455c5f53
vinyl: use tag for the API client (#1523) 2021-11-02 21:32:09 +01:00
Ludovic Fernandez
e719fabe96
chore: homogeneous tests setup and close servers. (#1520) 2021-11-01 23:52:38 +00:00
Ludovic Fernandez
15f3a45342
godaddy: add tests on the client (#1521) 2021-11-01 22:02:31 +01:00
Dominik Menke
60ae6e6dc9
chore: migrate RFC links (#1513)
As per announcement [1, 2], tools.ietf.org is going to shut
down in the near future. This updates the links to the referenced
RFCs to their new location, as per [3].

[1]: https://mailarchive.ietf.org/arch/msg/ietf/0n-6EXEmkTp3Uv_vj-5Vnm3o0bo/
[2]: https://mailarchive.ietf.org/arch/msg/ietf-announce/xKzJZIyanPCclTd7DU9PxBAbwhA/
[3]: https://github.com/ietf-tools/tools-transition-plan#new-service-locations
2021-10-22 19:26:08 +00:00
raunsbaekdk
18bcc0fa0c
exec: add detected variable to handle sequence interval (#1510) 2021-10-21 23:33:38 +02:00
Ludovic Fernandez
df54dd233a
feat: Allows defining the reason for the certificate revocation (#1511) 2021-10-21 20:36:11 +02:00
Chris Hepner
88f62f106d
rackspace: change zone ID to string (#1508) 2021-10-18 19:11:07 +02:00
Fernandez Ludovic
dfa0a6da68 Detach v4.5.3 2021-10-06 12:22:51 +02:00
Fernandez Ludovic
3675fe68ae Prepare release v4.5.3 2021-10-06 12:22:51 +02:00
Romain
e9b0b5247b
fix: missing preferred chain param for renew request (#1502) 2021-10-06 11:31:17 +02:00
Fernandez Ludovic
9022382953 Detach v4.5.2 2021-10-01 03:49:25 +02:00
Fernandez Ludovic
99256f209e Prepare release v4.5.2 2021-10-01 03:48:59 +02:00
Fernandez Ludovic
c897b88d86 chore: increase gorelease timeout. 2021-10-01 03:47:03 +02:00
Fernandez Ludovic
2c63b045d2 Detach v4.5.1 2021-10-01 02:56:48 +02:00
Fernandez Ludovic
c013d27e97 Prepare release v4.5.1 2021-10-01 02:56:27 +02:00
Fernandez Ludovic
6ef42e7424 chore: increase gorelease timeout. 2021-10-01 02:50:48 +02:00
Fernandez Ludovic
59cb5c213b Detach v4.5.0 2021-10-01 01:27:31 +02:00
Fernandez Ludovic
6dd983b19c Prepare release v4.5.0 2021-10-01 01:27:31 +02:00
Léopold Jacquot
1bd663882a
infomaniak: fix subzone support (#1497) 2021-09-30 19:06:22 +00:00
Ludovic Fernandez
17f62b8a13
Add DNS provider for Epik (#1491) 2021-09-29 18:40:35 +00:00
Laszlo Janosi
2763e76270
edgedns: fix Present and CleanUp logic (#1494)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-09-29 14:05:55 +00:00
nonchan
b86f37238f
Add DNS Provider for IBM Cloud (SoftLayer) (#1487) 2021-09-11 17:52:37 +02:00
Martin Weindel
6d0e0e16b4
Add AlwaysDeactivateAuthorizations flag to ObtainRequest (#1480)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-09-08 23:14:21 +02:00
Jens L
d2e526e8dd
Add DNS provider for nicmanager (#1473)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-08-27 12:47:28 +02:00
Christian Groschupp
376e7bd78e
desec: bump to v0.6.0 (#1470)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-08-25 12:54:08 +00:00
Adrien DAURIAT
e0b0f22e85
ovh: follow cname (#1467) 2021-08-25 11:25:05 +00:00
Yuanhai He
99ba43f743
alidns: support ECS instance RAM role (#1462) 2021-08-25 10:49:56 +00:00
Ludovic Fernandez
dc2b19e1b1
chore: update Go (#1469) 2021-08-25 11:44:11 +02:00
Ludovic Fernandez
bc8ff4a192
chore: update dependencies. (#1461) 2021-08-08 11:56:07 +02:00
Fernandez Ludovic
3e2ade221f chore: disable blank issue. 2021-08-08 11:05:47 +02:00
Christian Anton
a653611dc8
azure: zone name as environment variable (#1433) 2021-08-07 11:03:14 +00:00
Yuanhai He
6f5f694cd5
alidns: support sts token credential (#1454) 2021-08-07 12:07:31 +02:00
Ludovic Fernandez
8e7bba485f
chore: fix github action cache. (#1457) 2021-07-22 12:46:34 +02:00
Maxim
71a3bbc8dd
Add DNS provider for g-core labs (#1456)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-07-22 12:05:22 +02:00
Ludovic Fernandez
6e982d65ff
chore: update dependencies (#1453) 2021-07-12 09:27:49 +02:00
Ludovic Fernandez
264d39bbc4
Add DNS provider for hosttech (#1447) 2021-07-09 22:50:19 +02:00
Ludovic Fernandez
2dbe8832a5
Add DNS provider for freemyip.com (#1451) 2021-07-04 12:03:32 +02:00
Ludovic Fernandez
505f6d548f
doc: improve issue forms. (#1448) 2021-07-01 23:10:50 +00:00
Ludovic Fernandez
43779d7533
Add DNS provider for all-inkl (#1444) 2021-06-30 22:49:02 +02:00
Clement Jean
2a194d6ab9
aws: detailed credentials (#1439)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-06-28 01:31:18 +00:00
Clement Jean
284d1a3e38
lightsail: wrong Region env var name 2021-06-27 21:51:05 +02:00
Ludovic Fernandez
fbd369e04e
docs: convert issue templates to issue forms. (#1434) 2021-06-23 21:29:10 +00:00
Ludovic Fernandez
b20e8f3394
Add DNS provider for Internet.bs (#1431) 2021-06-14 02:44:13 +02:00
Ludovic Fernandez
1dbebce7a9
lib: use permanent error instead of context cancellation (#1429) 2021-06-11 11:07:38 +02:00
Richard Cooper
9b44e4a262
lib: fix backoff in SolverManager (#1428) 2021-06-11 05:06:30 +02:00
Fernandez Ludovic
8746bc75ff Detach v4.4.0 2021-06-08 18:46:51 +02:00
Fernandez Ludovic
7c24212e8a Prepare release v4.4.0 2021-06-08 18:46:51 +02:00
Ludovic Fernandez
190b38c76a
chore: update linter (#1425) 2021-06-08 09:09:39 +00:00
Ludovic Fernandez
f5da11cd29
simply: fix Go doc. (#1424) 2021-06-08 10:59:55 +02:00
Ludovic Fernandez
e79975cf14
csr: support NEW CERTIFICATE REQUEST as CSR type (#1423) 2021-06-05 13:47:39 +02:00
David Mell
1af3abd16e
Add DNS provider for Simply.com (#1419) 2021-06-03 01:50:45 +00:00
Peter Truman
442137327f
mythicbeasts: update API Auth endpoint in documentation (#1421) 2021-06-02 15:29:43 +00:00
Peter Truman
67d39182ce
mythicbeasts: typo in documentation (#1420) 2021-06-02 14:12:35 +00:00
Ludovic Fernandez
ed5c0a3869
hurricane: add API rate limiter. (#1417) 2021-05-31 21:42:24 +00:00
Brandon Mayfield
e8750f50ae
Add DNS provider for Infoblox (#1413) 2021-05-29 21:18:15 +02:00
Fernandez Ludovic
81bf297aa6 doc: fix build status badge. 2021-05-26 18:31:28 +02:00
Stelfux
90ff2b3f4b
ovh: fix example in documentation (#1410) 2021-05-24 18:21:52 +00:00
Chris Novakovic
25322628e4
hurricane: only treat first word of response body as response code (#1409) 2021-05-24 00:04:42 +00:00
dicksonleong
1227bb9f10
lightsail: improve documentation (#1407) 2021-05-22 11:00:25 +02:00
Ludovic Fernandez
a73d87e4c5
hostingde: autodetection of the zone name. (#1405) 2021-05-14 16:03:01 +00:00
Ludovic Fernandez
687214942e
log: use stderr instead of stdout. (#1402) 2021-05-14 17:51:05 +02:00
Ludovic Fernandez
c53c3d08db
lib: improve wait.For returns. (#1403) 2021-05-14 17:37:45 +02:00
João Neto
e3d7c85cfc
hurricane: documentation typo in domain name (#1398) 2021-05-10 13:49:10 +02:00
Ludovic Fernandez
c22cd40c91
Add DNS provider for Porkbun (#1396) 2021-05-02 13:41:02 +02:00
Van Hau TRAN
6bd3eae58f
scaleway: use official SDK (#1393) 2021-04-28 16:49:05 +02:00
Fernandez Ludovic
8bf9613fff doc: clean up DNS providers. 2021-04-25 11:51:37 +02:00
alexeiser
3da04e69cc
Add DNS provider for Sonic (#1392) 2021-04-25 09:37:35 +00:00
Marc Falzon
d848cc7f40
exoscale: fix DNS provider debugging (#1391) 2021-04-20 12:08:06 +02:00
Ludovic Fernandez
4b22b38d7a
domeneshop: fix documentation (#1390) 2021-04-14 22:23:05 +02:00
lubko
e38e32b906
wedos: fix api call parameters (#1386) 2021-04-14 08:13:48 +00:00
lubko
9002e5c4ab
Add DNS provider for wedos (#1385)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-04-13 21:32:15 +02:00
Jonathan G
7f53f88555
Add DNS provider for VinylDNS (#1384) 2021-04-10 16:18:48 +02:00
kakakikikeke-fork
2334340d7a
nifcloud: Get zone info from dns01.FindZoneByFqdn (#1383) 2021-04-08 21:05:25 +02:00
Ludovic Fernandez
ae7daa7d33
chore: update linter (#1379) 2021-03-27 13:49:40 +01:00
Pieter Lexis
83c626d9a1
powerdns: several improvements (#1374) 2021-03-17 09:53:38 +01:00
Dominik Menke
ee0b4bd0b3
make: fix validate-docs (#1376) 2021-03-17 02:27:37 +01:00
Fernandez Ludovic
60f7cb0598 Detach v4.3.1 2021-03-12 00:47:12 +01:00
Fernandez Ludovic
e8e58ec673 Prepare release v4.3.1 2021-03-12 00:47:12 +01:00
Ludovic Fernandez
42867390e5
exoscale: fix version. (#1372) 2021-03-12 00:33:24 +01:00
Fernandez Ludovic
dd451848ea Detach v4.3.0 2021-03-10 15:27:00 +01:00
Fernandez Ludovic
a7f9a93caa Prepare release v4.3.0 2021-03-10 15:27:00 +01:00
Ludovic Fernandez
773ed46b10
chore: update dependencies (#1370) 2021-03-10 10:42:00 +01:00
Ludovic Fernandez
b3be51f570
chore: add issue templates. (#1368) 2021-03-06 18:29:28 +01:00
Ludovic Fernandez
52ad9d72e8
chore: update linter (#1367) 2021-03-04 19:16:59 +00:00
Fernandez Ludovic
2c0a8ff689 core: prevent release failure
With go1.16 there is a difference between `go mod tidy` and `go mod
download`.
The `go.sum` is not altered in the same way by both commands.
`go mod tidy` must be always called after the `go mod download`.

https://github.com/golang/go/issues/43994
2021-03-03 01:18:08 +01:00
Amir Fouladvand
6b48d8dda8
arvancloud: typo in command example (#1365) 2021-03-02 23:27:01 +00:00
Albert Peschar
72c7034298
Increase HTTP client timeouts (#1336) 2021-02-28 12:59:51 +00:00
david
8b8be6f21e
cloudns: Improve reliability (#1331)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2021-02-28 12:25:15 +00:00
Tobias "Knight" S
b65dfb8661
desec: fix error with default TTL (#1361) 2021-02-28 02:23:28 +00:00
risson
31158bc58c
designate: support for Openstack Application Credentials (#1360) 2021-02-27 23:45:58 +00:00
Mark Scott
2871fd3938
mythicbeasts: implement ProviderTimeout (#1358) 2021-02-27 17:32:18 +00:00
StackoverFrog
22a991d8cc
dnspod: improve search accuracy when a domain have more than 100 records (#1354) 2021-02-27 14:55:45 +00:00
Mark Scott
55baf65065
doc: Fix typo in env var name in Mythic Beasts usage example (#1356) 2021-02-27 14:32:58 +00:00
Andre Sencioles
7cfcc155ee
edgedns: add support for .edgerc file (#1340) 2021-02-21 10:56:56 +00:00
Ludovic Fernandez
f42caa7393
Add DNS provider for Njalla (#1348) 2021-02-19 20:23:48 +00:00
Ludovic Fernandez
c562e5b987
chore: update for go1.16 (#1350) 2021-02-17 09:02:44 +01:00
Ben Lewis
620a48a374
Add DNS provider for Hurricane Electric (#1344) 2021-02-14 15:33:52 +00:00
Ludovic Fernandez
6bec917301
chore: update linter. (#1346) 2021-02-08 22:25:51 +01:00
Eirik Rye
078f84ed34
Add Domeneshop DNS01 provider (#1345) 2021-02-08 20:13:21 +01:00
Ludovic Fernandez
1d24006e69
fix: preferred chain only match root name (#1334) 2021-01-28 20:26:07 +01:00
TeoGoddet
6d068b5269
infomaniak: Make error message more meaningful (#1332) 2021-01-28 00:45:52 +01:00
Fernandez Ludovic
16660b23e4 doc: update Changelog. 2021-01-24 18:00:51 +01:00
Fernandez Ludovic
afd8eb0238 Detach v4.2.0 2021-01-24 16:56:22 +01:00
Fernandez Ludovic
52e6721dca Prepare release v4.2.0 2021-01-24 16:56:22 +01:00
Ludovic Fernandez
79be5072c7
Add DNS provider for Ionos. (#1322) 2021-01-14 20:42:12 +01:00
Niklas Hambüchen
8dd5585c53
errors: Fix instance not being printed. (#1327)
The condition so far printed it only when it was known to be empty.
2021-01-10 05:42:01 +01:00
Daniel McCarney
4d4061ff40
acme-dns: update cpu/goacmedns to v0.1.1. (#1326) 2021-01-06 12:47:20 +01:00
Ludovic Fernandez
af01844596
chore: update linter. (#1325) 2020-12-28 23:39:00 +01:00
hazzeh
3efb14404a
Add DNS provider for Loopia (#1324) 2020-12-26 17:22:01 +01:00
JP Mens
dfdc625f8f
rfc2136: add command example (#1321)
* RFC2136 example

This small patch adds an example to the RFC2136 provider.

* review and generate.

Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
2020-12-19 16:48:31 +01:00
Michael G
af597bac81
inwx: Increase propagation timeout to 360s to improve robustness (#1314) 2020-12-14 22:57:19 +01:00
Khanh Pham
4f7f557a5c
pdns: get exact zone instead of all zones (#1296) 2020-12-12 15:59:22 +01:00
Ludovic Fernandez
cabd839feb
vult, dnspod: fix default HTTP timeout. (#1318) 2020-12-09 22:04:47 +01:00
Adam Dobrawy
b87d5b1efe
hyperone: Minor tweaks of documentation (#1315) 2020-12-09 21:31:54 +01:00
Ludovic Fernandez
1e1ae7f49b
doc: remove landing page button. (#1313) 2020-12-09 21:19:26 +01:00
Ludovic Fernandez
d9248c9d4f
chore: use GitHub Action. (#1319) 2020-12-09 20:52:21 +01:00
Ludovic Fernandez
136f159d53
pdns: fix URL request creation. (#1312) 2020-11-28 17:54:26 +01:00
Iku Iwasa
2766aa6987
vultr: Update to govultr v2 API (#1302) 2020-11-28 14:53:02 +01:00
Adam Dobrawy
09be7e1f3e
hyperone: add 'since' tag in documentation (#1310) 2020-11-28 04:06:24 +01:00
Ludovic Fernandez
1af9299a83
doc: add examples and fix some typos (#1309) 2020-11-27 20:40:44 +01:00
Fernandez Ludovic
6292f5878c Detach v4.1.3 2020-11-25 14:38:06 +01:00
Fernandez Ludovic
086040a8ba Prepare release v4.1.3 2020-11-25 14:38:06 +01:00
Ludovic Fernandez
181926ca0d
azure: fix error handling. (#1307) 2020-11-25 12:25:28 +01:00
Ludovic Fernandez
8006c744f0
chore: update linter. (#1305) 2020-11-24 09:38:11 +01:00
Fernandez Ludovic
2bc55d0057 Detach v4.1.2 2020-11-21 21:06:23 +01:00
Fernandez Ludovic
721cfb5fe2 Prepare release v4.1.2 2020-11-21 21:06:23 +01:00
Ludovic Fernandez
a7387202e4
fix: preferred chain support. (#1298) 2020-11-21 20:24:11 +01:00
Fernandez Ludovic
2029375e04 Detach v4.1.1 2020-11-19 23:13:46 +01:00
Fernandez Ludovic
fca2a4564f Prepare release v4.1.1 2020-11-19 23:13:46 +01:00
Jakob
95bae8c50d
otc: select correct zone if multiple returned (#1292) 2020-11-19 19:36:49 +01:00
Ludovic Fernandez
3158518511
fix: nil pointer on error handling. (#1291) 2020-11-16 20:55:38 +01:00
Fernandez Ludovic
b04d00b8fe Detach v4.1.0 2020-11-06 23:55:21 +01:00
Fernandez Ludovic
dd4f73dd6a Prepare release v4.1.0 2020-11-06 23:55:21 +01:00
Chris Marchesi
73725b6437
acme/api: use postAsGet instead of post for AccountService.Get (#1283)
Co-authored-by: Fernandez Ludovic <lfernandez.dev@gmail.com>
2020-11-06 23:30:51 +01:00
Stamatis Katsaounis
ff0377ac02
Deprecate OS_TENANT_NAME as required field (#1279) 2020-11-03 23:28:25 +01:00
Ludovic Fernandez
38f23c8c52
chore: update linter. (#1278) 2020-10-27 12:01:05 +01:00
Léopold Jacquot
dfc346a00b
Add DNS provider for Infomaniak (#1277) 2020-10-27 01:03:26 +01:00
Ludovic Fernandez
918037b8af
chore: update linter. (#1275) 2020-10-22 22:30:50 +02:00
Ludovic Fernandez
88823a5c92
fix: use http.Header.Set method instead of Add. (#1270) 2020-10-17 14:51:55 +02:00
Ludovic Fernandez
fbab0e3c64
joker: add support for SVC API (#1267) 2020-10-08 16:52:50 +02:00
Dominik Menke
abd783a124
Improve documentation (#1265) 2020-10-03 21:36:56 +02:00
Trenton Broughton
03ca08048c
Fix minor typo in docs (#1261) 2020-09-30 18:07:50 +02:00
Dominik Menke
37746be75f
chore: spell check docs (#1260) 2020-09-30 00:27:08 +02:00
Dominik Menke
c3ca7d2c6d
rfc2136: ensure algorithm is fully qualified (#1259)
The RFC specifies the algorithm to be fully-qualified,
but the CLI docs don't communicate this to the end user.
2020-09-27 21:32:10 +02:00
sebastien-baillet
77aef9deb5
gcloud: adds an option to allow the use of private zones (#1258) 2020-09-26 13:29:42 +02:00
Fernandez Ludovic
ba6eb9729c Detach v4.0.1 2020-09-03 16:02:51 +02:00
Fernandez Ludovic
99b2cd8181 Prepare release v4.0.1 2020-09-03 16:02:51 +02:00
Ludovic Fernandez
79e4559eee
exoscale: change dependency version. (#1244) 2020-09-03 15:20:06 +02:00
Fernandez Ludovic
abdd2efc72 Detach v4.0.0 2020-09-02 20:38:01 +02:00
Fernandez Ludovic
c65edde245 Prepare release v4.0.0 2020-09-02 20:38:01 +02:00
Fernandez Ludovic
2582acc932 feat: changes module version. 2020-09-02 20:38:01 +02:00
Fernandez Ludovic
50a87b031e feat: bump dependencies. 2020-09-02 19:29:28 +02:00
Fernandez Ludovic
bb508a985a chore: update alpine version in the Docker image. 2020-09-02 19:29:28 +02:00
Fernandez Ludovic
23155f049d chore: update Travis configuration. 2020-09-02 19:29:28 +02:00
Fernandez Ludovic
4f16a89b03 feat: replaces FastDNS by EdgeDNS. 2020-09-02 19:29:28 +02:00
Fernandez Ludovic
712b9c4418 feat: removes old Linode provider. 2020-09-02 19:29:28 +02:00
Fernandez Ludovic
1bdab236ea feat: removes AddPreCheck function. 2020-09-02 19:29:28 +02:00
Fernandez Ludovic
53c02b6d5e feat: replaces ec384 by ec256. 2020-09-02 19:29:28 +02:00
Fernandez Ludovic
6ad8c6c16f feat: change ObtainForCSR signature. 2020-09-02 19:29:28 +02:00
Masayuki Matsuki
30e4987f99
Add preferred-chain option to support "alternate" certificate links (#1227)
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
2020-09-02 02:22:53 +02:00
Fernandez Ludovic
dd38dce6aa Detach v3.9.0 2020-09-01 14:50:59 +02:00
Fernandez Ludovic
75c3a49634 Prepare release v3.9.0 2020-09-01 14:50:59 +02:00
Ludovic Fernandez
00a2d5dbf8
namesilo: fix cleanup. (#1240) 2020-09-01 10:35:43 +02:00
Hendrik S
b23d947e17
scaleway: fix example (#1239) 2020-08-31 22:35:53 +02:00
Ludovic Fernandez
8164ae3da5
fix: desec rate limits. (#1237) 2020-08-26 11:25:59 +02:00
Jakub Surdej
7557dbc98c
Add DNS provider for HyperOne (#1233) 2020-08-24 23:50:52 +02:00
Michael G
d14bef50f3
namecheap: add additional specific information (#1230) 2020-08-09 22:10:02 +02:00
Ludovic Fernandez
b62e1f9352
chore: update linter. (#1228) 2020-08-09 16:39:44 +02:00
Boran Car
3fe5329376
azure: allow selecting environments (#1225) 2020-08-09 14:17:49 +02:00
Johannes Scheuermann
5f1f5b6410
designate: add support for Openstack clouds.yaml (#1219) 2020-08-09 11:48:13 +02:00
AndreaM12345
a4574aa2b9
edgsedns: update with init for config (#1226) 2020-08-09 11:18:23 +02:00
s-hamann
8ea7b4520f
desec: fix version in docs (#1216) 2020-07-15 22:29:45 +02:00
Ludovic Fernandez
6b00497f34
chore: update linter. (#1214) 2020-07-10 01:48:18 +02:00
Edward Lynes
23328fcdd2
Add Akamai Edgedns. Deprecate FastDNS (#1207) 2020-07-08 22:16:54 +02:00
Fernandez Ludovic
168a727287 Detach v3.8.0 2020-07-02 12:20:54 +02:00
Fernandez Ludovic
bcb5be49c8 Prepare release v3.8.0 2020-07-02 12:20:54 +02:00
Ludovic Fernandez
deb0ffc5d9
stackpath: fix subdomain support. (#1204) 2020-06-27 23:21:31 +02:00
Ludovic Fernandez
f3ca2c668b
constellix: fix search records API call. (#1200) 2020-06-27 21:28:03 +02:00
Ludovic Fernandez
7bd2666729
doc: improvements. (#1203) 2020-06-27 15:07:19 +02:00
Ludovic Fernandez
6f7c98ee28
fix: multi-va. (#1201) 2020-06-27 14:46:26 +02:00
Ludovic Fernandez
8630274c31
arvandcloud: fix record name. (#1202) 2020-06-27 14:28:59 +02:00
Jürgen Brunink
6ada79b536
hetzner: fix record name. (#1199) 2020-06-27 02:05:08 +02:00
David Dymko
03a768d908
vultr: bumping govultr version (#1193) 2020-06-18 16:29:44 +02:00
Ludovic Fernandez
fb72e1a292
doc: always display CLI help. (#1184) 2020-06-10 23:11:35 +02:00
Ludovic Fernandez
25c653a14c
doc: fix example of env vars file. (#1182) 2020-06-07 12:33:35 +02:00
Ludovic Fernandez
0349e40071
binaries: Add MIPS arch. (#1179) 2020-06-03 22:26:35 +02:00
Ludovic Fernandez
1a82effaaa
inwx: Two-Factor-Authentication (#1176) 2020-05-31 15:18:43 +02:00
Julian Calaby
47ed092fca
Document all AWS credential environment variables (#1171) 2020-05-30 14:00:57 +02:00
Sajjad Hashemian
7c3689d08a
Add DNS provider for ArvanCloud (#1173)
Co-authored-by: FaRzad-845 <f.shami@barox.ir>
2020-05-28 16:57:29 +02:00
Jared Ledvina
3d63e3ec07
Ensure we return a location during account updates (#1158) 2020-05-26 20:04:54 +02:00
Anders Chen
0714fcf679
Registrar.ResolveAccountByKey: Fix issues found testing with Pebble (#1142) 2020-05-26 19:06:38 +02:00
Ludovic Fernandez
e5d4dd166d
doc: improved doc about the build from sources. (#1162) 2020-05-17 00:57:55 +02:00
Ludovic Fernandez
444e311688
desec: improvement of error logs. (#1161) 2020-05-16 20:15:23 +02:00
Ludovic Fernandez
e07bf641ab
feat: add hook on the run command. (#1157) 2020-05-14 23:44:08 +02:00
Fernandez Ludovic
2f1b384411 Detach v3.7.0 2020-05-11 14:40:45 +02:00
Fernandez Ludovic
e774e180a5 Prepare release v3.7.0 2020-05-11 14:40:45 +02:00
Ludovic Fernandez
eb112e381c
fix: review path information. (#1151) 2020-05-11 10:29:39 +02:00
Simone Carletti
f13a88feb5
dnsimple: Upgrade client to 0.60.0 (#1148) 2020-05-10 17:22:48 +02:00
Wesley Moore
25ea7ac6b4
Add DNS provider for LuaDNS (#1135) 2020-05-10 06:03:54 +02:00
Ludovic Fernandez
6043c9347c
chore: update linter. (#1145) 2020-05-08 19:35:25 +02:00
Nir
e90786484a
Adding Hezner DNS provider (#1128) 2020-05-08 14:26:30 +02:00
Ludovic Fernandez
5ab7335987
Add DNS provider for Netlify. (#1144) 2020-05-08 12:26:44 +02:00
Adam
874e0c7980
aws: update sdk (#1139) 2020-05-07 02:45:41 +02:00
Ludovic Fernandez
7e3add6d0d
Add DNS provider for deSEC.io (#1143) 2020-05-07 01:27:35 +02:00
Ludovic Fernandez
b59dffbede
autodns: removes TXT records during CleanUp. (#1141) 2020-05-05 22:38:33 +02:00
Daniel Silverstone
a7eb2cd258
mythicbeasts: Update API base URL for released v2 API (#1136) 2020-05-04 15:56:41 +02:00
wdullaer
19f6a8c868
exoscale: fix HTTP timeout (#1130) 2020-04-29 18:44:37 +02:00
Daniel Silverstone
4057c38364
Add DNS provider for Mythic beasts DNSv2 (#1125) 2020-04-28 09:04:22 +02:00
Dominik Menke
4fe3bcfc6f Fix account storage location warning message (#1127) 2020-04-27 21:28:29 +02:00
Ludovic Fernandez
cb5d3a8fdb
Add DNS provider for Yandex. (#953) 2020-04-27 19:58:18 +02:00
Fernandez Ludovic
e10d6ac4b8 Detach v3.6.0 2020-04-24 11:13:43 +02:00
Fernandez Ludovic
71d61f880c Prepare release v3.6.0 2020-04-24 11:13:43 +02:00
radek-sprta
d7f39ed321
Add DNS provider for CloudDNS. (#1106) 2020-04-24 03:13:25 +02:00
Ludovic Fernandez
5429e50cc2
fix: some typos. (#1116) 2020-04-19 15:08:06 +02:00
svenwiltink
b9f530e352
transip: updated the client to v6 (#1104) 2020-04-19 14:21:42 +02:00
Ludovic Fernandez
b58c9499ca
constellix: improve challenge. (#1115) 2020-04-18 20:39:10 +02:00
Ludovic Fernandez
c0bc316a5f
feat: allow parallel solve. (#1114) 2020-04-17 22:38:13 +02:00
Julien Balestra
1ac1986687
azure: Allow for the use of MSI (#1110) 2020-04-17 18:54:59 +02:00
Ludovic Fernandez
ae818a411c
fix: use HTTP client from config. (#1112) 2020-04-17 09:46:42 +02:00
Kevin Pollet
31baa53761
alicloud: update sdk dependency version to v1.61.112 (#1111) 2020-04-14 09:36:52 +02:00
sdlspr
7a61e681b0
Informations about renewed certificates are now passed to the renew hook (#1108) 2020-04-11 14:57:06 +02:00
Jim Flattery
3213540c52
namedotcom: Improve documentation (#1109) 2020-04-07 23:46:05 +02:00
Anders Chen
cd52839f18
ns1: fix missing domain in log (#1105) 2020-04-06 03:45:19 +02:00
Daniel Pfankuchen
6b6ab3fd51
cloudns: Add subuser support (#1098) 2020-03-25 09:34:23 +01:00
Daniel McCarney
692dd913ea
acmedns: Update cpu/goacmedns v0.0.1 -> v0.0.2 (#1095)
The v0.0.2 release improves compatibility with account information
generated with other tools (`pyacmedns`), or using a raw `acmedns` API
response.
2020-03-23 17:48:31 +01:00
Ludovic Fernandez
b1509a6fd8
fix: broken link to port usage section. (#1094) 2020-03-22 22:29:16 +01:00
Ludovic Fernandez
f3e067df49
feat: use colon instead a arrow. (#1090) 2020-03-20 22:53:09 +01:00
akillcool
6bc93456ad
alicloud: add support for domain with punycode (#1088) 2020-03-19 09:26:48 +01:00
Sven Dowideit
7bb614080b
namedotcom: get the actual registered domain so we can remove just that from the hostname to be created (#545) 2020-03-18 23:04:54 +01:00
Fernandez Ludovic
fa19378184 detach v3.5.0 2020-03-15 23:50:20 +01:00
Fernandez Ludovic
2a1cf86439 Prepare release v3.5.0 2020-03-15 23:50:20 +01:00
Ludovic Fernandez
c90c61df8e
Share the client code between Selectel and VScale. (#1085) 2020-03-15 17:17:17 +01:00
Ludovic Fernandez
1aeac60ab3
Add DNS provider for Zonomi and RimuHosting. (#1084) 2020-03-14 13:32:50 +01:00
Yegor Timoshenko
a5e60d5d05
azure: pass AZURE_CLIENT_SECRET_FILE to autorest.Authorizer (#1083) 2020-03-13 00:08:58 +01:00
Ludovic Fernandez
403070dd9b
Standardization of the definition of env vars. (#1082) 2020-03-11 23:51:10 +01:00
Carlos Alberto Costa Beppler
aeb20f4deb
Adds --name flag to list command. (#1046) 2020-03-11 12:17:50 +01:00
chrisbulgaria
68842b3b70
oraclecloud: fix subdomain support (#1055) 2020-03-11 11:59:08 +01:00
Ludovic Fernandez
4901bb4fe7
Add DNS provider for Dynu. (#1081) 2020-03-10 12:31:33 +01:00
Ludovic Fernandez
006012200e
Add DNS provider for reg.ru (#1078) 2020-03-10 11:08:32 +01:00
Ludovic Fernandez
e01131f2f4
fix: Improve cleanup log messages. (#1076) 2020-03-09 18:10:40 +01:00
Fernandez Ludovic
7aca0f7f24 fix: generate DNS table in the README. 2020-03-09 10:06:13 +01:00
Ludovic Fernandez
ce2cc58485
chore: generate DNS table in the README. (#1080) 2020-03-09 09:43:27 +01:00
Yong Wen Chua
2d6bb48d36
Upgrade cloud.google.com/go (#1073) 2020-03-09 04:14:20 +01:00
Ludovic Fernandez
d64d33a7f9
chore: Build improvements (#1079) 2020-03-08 22:19:12 +01:00
Ludovic Fernandez
b5d99c7b14
feat: multi-arch Docker image. (#1072) 2020-03-01 21:01:41 +01:00
Ludovic Fernandez
2da1ce06ea
chore: wrap errors. (#1070) 2020-02-27 19:14:45 +01:00
Ludovic Fernandez
9d31db300b
chore: update to go1.14 (#1069) 2020-02-26 00:02:29 +01:00
Ludovic Fernandez
52bce2cb07
akamai: update lib. (#1068) 2020-02-25 23:46:57 +01:00
Fernandez Ludovic
6c8f48dd85 detach v3.4.0 2020-02-25 22:26:55 +01:00
Fernandez Ludovic
bf1c12479e Prepare release v3.4.0 2020-02-25 22:26:55 +01:00
Ludovic Fernandez
837082bc1b
Add DNS provider for Constellix (#1051) 2020-02-25 21:56:40 +01:00
Ludovic Fernandez
14329c03df
Add DNS provider for Servercow. (#1056) 2020-02-25 21:41:39 +01:00
Matt Holt
5cdc0002e9
crypto: Treat CommonName as optional (#1064) 2020-02-21 19:28:07 +01:00
Ludovic Fernandez
2e30fd0ba8
Minors changes (#1059) 2020-02-10 18:20:52 +01:00
Jan Polák
8f349e5a5f ovh: Improve provider documentation (#1057) 2020-02-09 17:22:11 +01:00
Joonas
aad2d9ad92
zoneee: fix subdomains. (#1053) 2020-02-08 11:38:57 +01:00
Aidan Steele
87e770cc02
route53: allow custom client to be provided (#1050) 2020-02-01 12:10:40 +01:00
Van Hau TRAN
ee33cff002 Add DNS provider for Scaleway (#1047) 2020-01-23 15:51:47 +01:00
Héctor Molinero Fernández
59ea57daf6 cli: Add "LEGO_PATH" environment variable (#1045)
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
2020-01-18 22:59:06 +01:00
Mohamed Akram
f5180ad521 namecheap: allow external domains (#1042) 2020-01-18 02:25:50 +01:00
Steffen Neubauer
470f04590b designate: Don't clean up managed records like SOA and NS (#1044) 2020-01-18 01:54:07 +01:00
Ludovic Fernandez
add2bea577
chore: update cenkalti/backoff to v4. (#1043) 2020-01-12 17:16:46 +01:00
Ludovic Fernandez
a10027da1c
namecheap: add sandbox support. (#1041) 2020-01-12 16:51:14 +01:00
Ludovic Fernandez
bbc3013d84
chore: update dnspod lib. (#1037) 2020-01-09 11:23:28 +01:00
Ludovic Fernandez
6001372a91
doc: improve Route53 documentation. (#1036) 2020-01-09 09:12:27 +01:00
Fernandez Ludovic
1bd4b7071b detach v3.3.0 2020-01-09 01:07:04 +01:00
Fernandez Ludovic
63758264cb Prepare release v3.3.0 2020-01-09 01:07:04 +01:00
Ludovic Fernandez
6154f47ec3
Update dnspod, because of API breaking changes. (#1034) 2020-01-08 20:53:45 +01:00
Matt Holt
c7bbf62dd4 Successfully parse private key PEM blocks (#1030) 2020-01-06 16:52:56 +01:00
James
4f72de468d Auto-detection of the project ID for gcloud. (#1028) 2020-01-04 17:18:34 +01:00
Dominik Menke
ca42cc42a0 dns/dreamhost: fix GoDoc (#1027) 2020-01-03 21:11:21 +01:00
Dmitri Goutnik
2dbb71b3f6 Add support for freebsd/arm64 (#1019) 2019-12-11 15:45:44 +01:00
Simon
cda5995ab4 Add DNS provider for Checkdomain (#1017) 2019-12-07 23:59:59 +01:00
Aubrey
914d481a07 Update examples to explain http.webroot (#1012) 2019-11-19 01:43:07 +01:00
Rémi Lapeyre
b1dba4f23d Add support to update account (#1002) 2019-11-19 01:07:46 +01:00
Ludovic Fernandez
e345074ff6
fix: some debug code in e2e tests. (#1009) 2019-11-15 11:45:41 +01:00
Fernandez Ludovic
d2a125c759 detach v3.2.0 2019-11-09 22:19:00 +01:00
Fernandez Ludovic
11ee928ace Prepare release v3.2.0 2019-11-09 22:19:00 +01:00
Fernandez Ludovic
5b0879b7e3 doc: fix autodns version. 2019-11-09 22:19:00 +01:00
Dominik Menke
e6ff557596 cloudflare: fix panic when accessing record cache (#1005) 2019-11-07 11:50:44 +01:00
Ludovic Fernandez
fa5555d3f4
Update ACME RFC links from draft to RFC8555. (#1004) 2019-11-05 17:58:30 +01:00
Ludovic Fernandez
738e40f446
fix: use token as unique ID. (#1003) 2019-11-05 12:58:13 +01:00
kolaente
46680f6524 Add support for autodns (#957) 2019-11-01 11:20:34 +01:00
Luis Gracia
a8484e3954 httpreq: Allow use environment vars from a _FILE file (#1000) 2019-10-30 21:40:48 +01:00
Ludovic Fernandez
635b9ac93d
chore: update linter. (#997) 2019-10-25 01:27:40 +02:00
Dominik Menke
8afde164a1 feat: expose more SOA fields found by dns01.FindZoneByFqdn (#980) 2019-10-25 00:58:50 +02:00
seph
aab54da9a2 Don't deactivate valid authorizations (#996) 2019-10-25 00:28:26 +02:00
Dominik Menke
41a9384638 docs: clarify installation-from-source instructions
Using github.com/go-acme/lego/cmd/lego confuses go get. It fetches
github.com/go-acme/lego@v2.7.2+incompatible (among others) and
stumbles over a type error with that version.

Correcting the go get path to github.com/go-acme/lego/v3/cmd/lego
does the right thing.

fixes #993
2019-10-18 20:33:38 +02:00
Fernandez Ludovic
1eda12ee10 detach v3.1.0 2019-10-09 03:24:09 +02:00
Fernandez Ludovic
776850ffc8 Prepare release v3.1.0 2019-10-09 03:24:09 +02:00
Dominik Menke
828b0f3420 cloudflare: handle restricted API tokens (#985) 2019-10-09 02:20:30 +02:00
Tom Bell
415e5343da dreamhost: Add CLI example (#983) 2019-10-05 18:06:12 +02:00
Dominik Menke
f69cd8d63d feat: ease operation behind proxy servers (#974) 2019-10-05 13:44:38 +02:00
Dominik Menke
82778cf77c cloudflare: update client (#979) 2019-10-01 20:51:18 +02:00
Ludovic Fernandez
e225f8d334
ovh: fix int overflow. (#972) 2019-09-19 19:07:00 +02:00
Ludovic Fernandez
6a8aae1694
bindman: fix client version. (#971) 2019-09-18 23:40:00 +02:00
Ludovic Fernandez
6bbba5bbaa
linodev4: configurable propagation timeout. (#961) 2019-09-17 21:25:59 +02:00
Ludovic Fernandez
d7d75bea51
chore: update to go1.13 (#962)
* chore: update to go1.13
* fix: invalid version.
* chore: update linter configuration.
2019-09-17 21:00:42 +02:00
Jarred Trainor
a5a29187fe cloudflare: add support for API tokens (#937) 2019-09-01 14:32:20 +02:00
Simon Legner
f6230a2662 doc: fix typesetting of "GitHub" (#955) 2019-08-20 19:15:39 +02:00
Ludovic Fernandez
cf879d0b3c
fix: typos. (#954) 2019-08-20 18:40:41 +02:00
Caige Nichols
76c850a580 Add Liquid Web DNS Provider (#951) 2019-08-19 21:41:10 +02:00
Fernandez Ludovic
e820634e0e detahc v3.0.2 2019-08-15 15:33:32 +02:00
Fernandez Ludovic
fd11248e65 Prepare release v3.0.2 2019-08-15 15:33:12 +02:00
Fernandez Ludovic
bd238c80e8 detach v3.0.1 2019-08-14 22:33:08 +02:00
Fernandez Ludovic
ef7460d060 Prepare release v3.0.1 2019-08-14 22:33:08 +02:00
Ludovic Fernandez
59d6d0d192
fix: invalid pseudo version. (#948) 2019-08-14 01:23:56 +02:00
Ludovic Fernandez
a7bf8c3197
Fix invalid pseudo version (#945)
* fix: invalid pseudo version.
* fix: goreleaser configuration.
* chore: ci configuration.
* doc: add requirements.
* doc: publish the doc more often.
2019-08-13 17:07:06 +02:00
Fernandez Ludovic
bc4b57accc detach v3.0.0 2019-08-07 14:07:47 +02:00
Fernandez Ludovic
e7a90b9471 chore: migrate to go module (v3.0.0)
- chore: update dependencies: use version with go modules.
- chore: remove dep.
- chore: update backoff imports.
- chore: init go module.
- chore: update CI.
- chore: mod v3
- chore: update docker image.
2019-08-07 14:07:47 +02:00
Fernandez Ludovic
99bf9f4e8f detach v2.7.2 2019-07-31 17:22:08 +02:00
Fernandez Ludovic
295dd66f2a Prepare release v2.7.2 2019-07-31 17:22:08 +02:00
David Dymko
905b1eb80b vultr: quote TXT record (#940) 2019-07-30 20:39:25 +02:00
Matthew Horan
58d6d9f476 Add install instructions for FreeBSD ports (#935) 2019-07-26 22:08:04 -07:00
Fernandez Ludovic
c220df0328 detach v2.7.1 2019-07-22 23:46:11 +02:00
Fernandez Ludovic
b6808bf0d9 Prepare release v2.7.1 2019-07-22 23:46:11 +02:00
Ludovic Fernandez
7f6155e865
vultr: invalid record type. (#932) 2019-07-22 23:29:32 +02:00
Fernandez Ludovic
d594e35ca6 detach v2.7.0 2019-07-17 21:35:47 +02:00
Fernandez Ludovic
789f170ee6 Prepare release v2.7.0 2019-07-17 21:35:47 +02:00
David Dymko
87be19588b Switch Vultr to official client (#929) 2019-07-17 21:01:50 +02:00
Ludovic Fernandez
0dbc28193b
feat: joker username and password. (#927) 2019-07-17 02:39:17 +02:00
Ludovic Fernandez
84d9795793
fix: too early deprecation. (#924) 2019-07-11 08:32:16 +02:00
Ludovic Fernandez
09caec4158
namesilo: fix link in the readme. (#920) 2019-07-10 19:53:40 +02:00
Ludovic Fernandez
79f6b35841
fix: manage write error during the display of the DNS help. (#923) 2019-07-10 19:34:10 +02:00
Ludovic Fernandez
f7dcb6ff22
fix: confusing message. (#922) 2019-07-10 19:20:31 +02:00
Wettle
c4a05dfab4 Add DNS provider for namesilo (#916) 2019-07-05 03:38:43 +02:00
Ludovic Fernandez
341407b482
chore: update golangci-lint. (#914) 2019-07-04 19:20:26 +02:00
Ludovic Fernandez
f5cd138ea6
chore: update DNS providers dependencies. (#915) 2019-07-04 18:24:33 +02:00
Ludovic Fernandez
8dcc55b828
doc: improve Cloudflare documentation. (#913) 2019-07-04 18:10:43 +02:00
Frans Kuipers
6647ce7b1d Add support for versio.nl (#909) 2019-06-27 19:30:10 +02:00
Ludovic Fernandez
ac65f6c6a9
chore: update dns providers libs. (#877) 2019-06-24 20:08:55 +02:00
Ludovic Fernandez
83618fed79
vegadns: refactor tests. (#910) 2019-06-24 18:34:04 +02:00
Felix Meißner
346d3222d7 Prevent sending empty body in OTC DNS Provider (#903) 2019-06-14 14:19:38 +02:00
Fernandez Ludovic
99741b0b92 detach v2.6.0 2019-05-27 19:36:40 +02:00
Fernandez Ludovic
01903cdfb9 Prepare release v2.6.0 2019-05-27 19:36:40 +02:00
Ludovic Fernandez
72a9baf027
CLI help indentation. (#898) 2019-05-27 19:20:45 +02:00
Chris Marchesi
dac7c11895 Get an existing certificate by URL (#897) 2019-05-25 16:12:37 +02:00
Zimbo Boyd
a0d59b85fe digitalocean: LEGO_EXPERIMENTAL_CNAME_SUPPORT support (#885) 2019-05-14 15:20:49 +02:00
Alexey Zagarin
b9bafc582c Strip newlines when reading environment variables from _FILE suffixed files. (#872) 2019-05-09 13:58:24 +02:00
Ludovic Fernandez
75a4315464
manual: serial behavior. (#880) 2019-05-06 20:11:06 +02:00
Ludovic Fernandez
437d6a0408
doc: remove TODO. (#878) 2019-05-06 19:37:51 +02:00
Ludovic Fernandez
b186acb858
chore: validate the DNS documentation generation. (#864) 2019-05-06 19:09:36 +02:00
Zlatko Čalušić
5bf36f544a exec: serial behavior (#881) 2019-05-05 20:11:58 +02:00
Ludovic Fernandez
985f3f8b7b
fix: cli disable-cp option. (#868) 2019-05-03 17:56:38 +02:00
Ludovic Fernandez
29c63545ce
gcloud: fix zone visibility. (#875) 2019-05-03 01:27:46 +02:00
Jason Harmon
007888f9dc Add support for EasyDNS (#867) 2019-04-29 02:03:04 +02:00
Alexander D. Kanevskiy
4c9d1979fc Add support for Joker.com DMAPI (#870) 2019-04-28 14:33:50 +02:00
Werberson
a72639ef51 Add support for Bindman DNS provider (#869) 2019-04-27 18:50:40 +02:00
Matthew Horan
62e3f4fc01 Use fqdn to get zone in gcloud Present/CleanUp (#861) 2019-04-27 16:15:31 +02:00
Fernandez Ludovic
f0cfdff395 detach v2.5.0 2019-04-18 20:23:34 +02:00
Fernandez Ludovic
3d13faf689 Prepare release v2.5.0 2019-04-18 20:23:34 +02:00
Ludovic Fernandez
4b0d8830de
Add tzdata to the Docker image. (#863) 2019-04-18 19:44:44 +02:00
Ludovic Fernandez
8edce3b2cf
cloudns: fix TTL and status validation (#856) 2019-04-17 22:41:51 +02:00
Ludovic Fernandez
7c680a2438
Adds 'Since' to DNS providers documentation (#854) 2019-04-17 21:32:38 +02:00
Ludovic Fernandez
6878dbbeba
doc: enhance Route53 documentation. (#853) 2019-04-17 20:58:34 +02:00
Ludovic Fernandez
cc19ab810c
fix: Disable authz when solve fail. (#851) 2019-04-17 20:47:59 +02:00
Chris Allen
ca7664d856 gcloud: use public DNS zones (#852) 2019-04-12 20:37:29 +02:00
Kazumichi Yamamoto
f400ec5163 sakuracloud: supports concurrent update (#850) 2019-04-09 22:56:12 +02:00
Ludovic Fernandez
5d557fdc6d
Adds renew hook (#845)
* chore: update golangci-lint.
* feat: support renew-hook.
2019-04-02 18:38:23 +02:00
Fernandez Ludovic
55572c2606 detach v2.4.0 2019-03-26 20:29:23 +01:00
Fernandez Ludovic
aaecc1ca72 Prepare release v2.4.0 2019-03-26 20:29:23 +01:00
evs-ch
51447bb2e7 Use provided ZoneName instead of domain (#839) 2019-03-25 12:19:13 +01:00
tbe
b668bde5e4 pdns: fix wildcard with SANs (#837)
The current implementation of the DNS challenge does not allow
to set multiple TXT records at once.

As PowerDNS has the concept of record sets, and so all records
for the same type and name must set during one call, we would override
existing records.

To avoid this, we merge the new TXT record with existing ones
2019-03-21 15:46:21 +01:00
andig
0ce6ba36b9 Add DNS Provider for Domain Offensive (do.de) (#835) 2019-03-21 11:52:01 +01:00
Daniel McCarney
8ed39fe981 fix broken link to library usage docs. (#832) 2019-03-19 16:50:05 +01:00
Ludovic Fernandez
3684cc738d
doc: adds information about _FILE suffix. (#829) 2019-03-18 17:42:54 +01:00
Ian Bishop
0c87df143e Add 'manual' provider to the output of dnshelp (#825) 2019-03-12 09:51:23 +01:00
Fernandez Ludovic
5e741000cc doc: fix badges. 2019-03-11 20:00:46 +01:00
Fernandez Ludovic
11ddd58253 doc: fix badges. 2019-03-11 19:29:22 +01:00
Ludovic Fernandez
3edb75872d
chore: migrate to new org. (#824) 2019-03-11 17:56:48 +01:00
Fernandez Ludovic
a483b4afaa detach v2.3.0 2019-03-11 17:23:46 +01:00
Fernandez Ludovic
2952cdaebd Prepate release v2.3.0 2019-03-11 17:23:46 +01:00
Ludovic Fernandez
b7bb9b0ac1
feat: adds log when no renewal. (#819) 2019-03-11 17:08:48 +01:00
Ludovic Fernandez
219222fdda
Use canonical imports (#822)
* fix: challenge are not required for revoke.
2019-03-11 16:54:35 +01:00
Ludovic Fernandez
62fea05e21
Automatic generation of documentation (#818)
* generate a detailed CLI help
* generate a documentation site
* new readme
2019-03-08 19:47:06 +01:00
Ludovic Fernandez
235ab6653f
oraclecloud: better way to get private key. (#814) 2019-03-05 19:57:04 +01:00
Jens Sauer
67b329e3e3 OVH: Refresh zone after deleting challenge record (#815)
After removing the challenge record from OVH, the zone itself
does not get refreshed and leaving the obsolete record in place.

Calling '/domain/zone/ZONE/refresh' after deleting the record will
apply the changes to the zone.
2019-03-04 19:17:53 +01:00
tzakrajs
1c309c9c80 Add DNS Provider for ClouDNS.net (#813) 2019-03-01 19:09:00 +01:00
Sugi
52eceeb8d2 OracleCloud: ttl config and timeout (#812) 2019-02-28 08:19:42 +01:00
Ayan George
9f3daf38b6
Merge pull request #799 from ldez/refactor/update-golangci-lint
chore: update golangci-lint.
2019-02-26 11:03:46 -05:00
Ayan George
4c346c9d92
Merge branch 'master' into refactor/update-golangci-lint 2019-02-26 10:58:15 -05:00
Sugi
af67943e94 Add DNS Provider for Oracle Cloud (#808) 2019-02-26 15:41:02 +01:00
jkahrs
a144800896 hostingde: fix client fails if customer has no access to dns-groups (#809) 2019-02-26 15:25:34 +01:00
Daniil Rutskiy
7f6b708439 vscale: getting sub-domain (#805) 2019-02-22 17:33:12 +01:00
Daniil Rutskiy
6cb9b249d2 selectel: getting sub-domain (#803) 2019-02-22 17:24:17 +01:00
Fernandez Ludovic
b9c712cde6 fix: wrong option. 2019-02-18 13:42:10 +01:00
Fernandez Ludovic
931ab3d8bf chore: update to golangci-lint v1.15.0 2019-02-18 13:28:19 +01:00
Yoan Blanc
7cdfe9b36b exoscale: update library (#800) 2019-02-18 10:49:41 +01:00
Fernandez Ludovic
a8b2397631 chore: update golangci-lint. 2019-02-16 14:56:15 +01:00
Daniil Rutskiy
d891600a89 fix: TXT records clean up for Vscale DNS provider (#798) 2019-02-14 15:35:49 +01:00
Daniil Rutskiy
938ba435d6 fix: TXT records clean up for Selectel DNS provider (#797) 2019-02-14 15:30:03 +01:00
Danek Duvall
1c6f67f47a Add a mechanism to wrap a PreCheckFunc (#783) 2019-02-12 17:36:44 +01:00
Fernandez Ludovic
19303d3ac6 detach v2.2.0 2019-02-09 05:46:18 +01:00
Fernandez Ludovic
52e43eb318 Prepare release v2.2.0 2019-02-09 05:46:18 +01:00
Ludovic Fernandez
fe7a60ee0e
Uses a jittered exponential backoff (#794) 2019-02-09 05:40:45 +01:00
Matthew Horan
348b6f3721 Resolve CNAME when creating dns-01 challenge (#791)
* Resolve CNAME when creating dns-01 challenge

It may be desirable to host the dns-01 challenge in a zone other than
the one where the challenge is presented. For example, when validating
a.example.com, the challenge may need to live on example.org.

This change resolves CNAMEs encountered when determining the FQDN of the
challenge, and replaces them with the alias.

This PR is based on the original work in #584.

Co-authored-by: Gurvinder Singh <gurvinder.singh@uninett.no>

* review: feature-flip.

* review: restore acmedns test.
2019-02-09 05:02:58 +01:00
Ludovic Fernandez
9409b92ed5
fix: CLI and key type. (#790) 2019-02-08 19:29:28 +01:00
Wim Fournier
86d709a1c6 removing lego from the dockerignore (#789) 2019-02-08 11:42:26 +01:00
Ludovic Fernandez
cc0d112e34
Applies Let’s Encrypt’s recommendation about renew. (#787) 2019-02-08 02:43:05 +01:00
Wim Fournier
62e0e54f23 Adds support for Openstack Designate as a DNS provider (#786) 2019-02-07 16:10:07 +01:00
Ludovic Fernandez
6fdf45c474
chore: update golangci-lint. (#782) 2019-02-06 18:15:53 +01:00
Ludovic Fernandez
f05aa4c241
fix: endpoint with path. (#781) 2019-02-04 22:54:54 +01:00
Ludovic Fernandez
3019d10801
chore: migrate to nrdcg. (#779) 2019-02-04 22:43:10 +01:00
Ludovic Fernandez
c4730a6ec3
fastdns: Do not overwrite existing TXT records (#774) 2019-02-04 22:12:03 +01:00
Christopher Banck
0e6e4807b1 option to specify gcloud service account json by env as string (#776)
Added the option to provide the gcloud service account as a string via the environment variable `GCE_SERVICE_ACCOUNT` in addition to the already available option to specify a filepath to a keyfile `GCE_SERVICE_ACCOUNT_FILE`.
2019-02-01 13:14:57 +01:00
Danek Duvall
68568b7ded Log wildcard domain correctly in validation (#773) 2019-01-26 01:11:45 +01:00
Fernandez Ludovic
0574362e13 detach v2.1.0 2019-01-24 23:18:29 +01:00
Fernandez Ludovic
00ad82dec1 Prepare release v2.1.0 2019-01-24 23:18:29 +01:00
Ludovic Fernandez
27f26ed3d1
namecheap: fix panic. (#764) 2019-01-24 21:40:44 +01:00
Ludovic Fernandez
4f6d1077a2
alicloud: fix pagination. (#767) 2019-01-24 19:56:19 +01:00
fuku
977aa94061 nifcloud: Change DNS base url (#765) 2019-01-22 08:34:45 +01:00
Elen Eisendle
77afd0f732 Add support for zone.ee as a DNS provider (#751) 2019-01-20 12:27:35 +01:00
Ludovic Fernandez
a43ec709e8
gcloud: More detailed information about Google Cloud DNS. (#761) 2019-01-16 21:01:53 +01:00
Ludovic Fernandez
ec6c22d70b
fix: OCSP, set HTTP client. (#759) 2019-01-12 00:17:29 +01:00
Ludovic Fernandez
38f335dbe1
chore: update golangci-lint. (#757) 2019-01-11 19:23:27 +01:00
Fernandez Ludovic
b91dbb6615 detach v2.0.1 2019-01-09 20:05:11 +01:00
Fernandez Ludovic
86c9de3db6 Prepare release v2.0.1 2019-01-09 20:05:11 +01:00
Fernandez Ludovic
eb2440e3ff detach v2.0 2019-01-09 19:02:36 +01:00
Fernandez Ludovic
9b4afbb998 Prepare release v2.0 2019-01-09 19:02:36 +01:00
Ludovic Fernandez
3857d889a3
Change AUR repository. (#748) 2019-01-09 08:35:15 +01:00
Ludovic Fernandez
7e1f4948ec
Add configurable timeout when obtaining certificates. (#747) 2019-01-09 08:29:17 +01:00
Ludovic Fernandez
b1fd570987
fix: rfc2136 must be run sequentially. (#745) 2019-01-09 08:23:33 +01:00
Ludovic Fernandez
41737739f2
transip: concurrent access to the API. (#744) 2019-01-09 08:18:12 +01:00
bzub
3105a01a1c dns/gcloud: Fix check for GCE_PROJECT when using gcloud (#752) 2019-01-09 08:12:22 +01:00
Ludovic Fernandez
891b50656c
contrib: adds a guide to make a pull request. (#746) 2019-01-08 22:53:13 +01:00
bzub
c938de68f2 Use GCE_PROJECT for project always, if specified (#750) 2019-01-08 14:39:12 +01:00
Ludovic Fernandez
43401f2475
New challenges management. (#741) 2019-01-03 16:59:53 +01:00
Ludovic Fernandez
9979087572
fix: gcloud. (#742) 2019-01-02 20:45:17 +01:00
Fredrik Sandebert
8d8ec2c92c Removed an outdated instruction (#743)
The latest version of the policy doesn't need any manual adjusting; it's fine to use as-is.
2018-12-28 16:19:36 +01:00
Ludovic Fernandez
820c2b7531
gcloud: fix for wildcard (#740) 2018-12-22 00:53:05 +01:00
smartwang
cb3c4c7937 alidns: add support to handle more than 20 domains (#739) 2018-12-20 11:52:34 +01:00
Ludovic Fernandez
15764a17b6
fix: errors management in the prober. (#736) 2018-12-19 21:06:16 +01:00
Ludovic Fernandez
765b80cd41
fix: use new challtestsrv. (#735) 2018-12-19 14:09:32 +01:00
Ludovic Fernandez
b05b54d1f6
Release helper (#728) 2018-12-11 01:14:49 +01:00
Ludovic Fernandez
42941ccea6
Refactor the core of the lib (#700)
- Packages
- Isolate code used by the CLI into the package `cmd`
- (experimental) Add e2e tests for HTTP01, TLS-ALPN-01 and DNS-01, use [Pebble](https://github.com/letsencrypt/pebble) and [challtestsrv](https://github.com/letsencrypt/boulder/tree/master/test/challtestsrv) 
- Support non-ascii domain name (punnycode)
- Check all challenges in a predictable order
- No more global exported variables
- Archive revoked certificates
- Fixes revocation for subdomains and non-ascii domains
- Disable pending authorizations
- use pointer for RemoteError/ProblemDetails
- Poll authz URL instead of challenge URL
- The ability for a DNS provider to solve the challenge sequentially
- Check all nameservers in a predictable order
- Option to disable the complete propagation Requirement
- CLI, support for renew with CSR
- CLI, add SAN on renew
- Add command to list certificates.
- Logs every iteration of waiting for the propagation
- update DNSimple client
- update github.com/miekg/dns
2018-12-06 22:50:17 +01:00
YAEGASHI Takeshi
4e842a5eb6 Azure: Do not overwrite existing TXT records (#726) 2018-12-04 21:04:39 +01:00
Jacob Hoffman-Andrews
a5f0a3ff80 Add version to xenolf-acme in User-Agent. (#719)
Also, remove "Go-http-client/1.1". In practice this added detail doesn't
wind up being useful in diagnosing problems, particularly since it can
be deduced from the xenolf-acme version.

* add UA comments.
2018-11-15 23:02:34 +01:00
Ludovic Fernandez
09a3292757
fix: clouflare error. (#721) 2018-11-15 22:41:54 +01:00
Ayan George
fac6e4995c
Merge pull request #710 from ldez/feature/httpreq
Add DNS provider for "HTTP request".
2018-11-09 06:55:29 -05:00
Fernandez Ludovic
eb04d860c4 feat: add to list. 2018-11-09 01:53:21 +01:00
Fernandez Ludovic
265e523138 refactor: add realm because it's fun. 2018-11-09 01:43:56 +01:00
Fernandez Ludovic
bba525fbce doc: fix readme. 2018-11-08 23:39:19 +01:00
Fernandez Ludovic
58b757cd76 feat: add provider httpreq. 2018-11-08 19:22:52 +01:00
Daniil Rutskiy
e89afae4f8 Add DNS Provider for Vscale (#705) 2018-11-08 10:31:42 +01:00
Maarten den Braber
1837a3bb1c Add DNS Provider for TransIP (#703) 2018-11-06 11:56:28 +01:00
Stephan Müller
42d8637d87 Add DNS Provider for inwx (#687) 2018-11-06 11:49:50 +01:00
Ludovic Fernandez
286c44337e
fix: Docker image. (#704) 2018-11-05 17:54:38 +01:00
Ludovic Fernandez
88f86af113
Prepare release v1.2.0 (#701) 2018-11-05 15:14:30 +01:00
mattn
5f383ec4f4 Add DNS Provider for MyDNS.jp (#628) 2018-11-04 15:37:29 +01:00
Daniil Rutskiy
2131723cc8 Support DNS Provider Selectel (#698) 2018-11-04 02:36:04 +01:00
Ludovic Fernandez
4f36f4354b
Support POST-as-GET. (#695) 2018-11-04 01:51:53 +01:00
Kazuki Sawada
5992cc0ece conoha: fix finding domain id (#697) 2018-11-02 12:59:41 +01:00
Kazuki Sawada
8556397ec0 add ConoHa DNS provider (#692) 2018-11-02 02:30:09 +01:00
Daniel McCarney
2b0aa0aadf TLS-ALPN-01: Update idPeAcmeIdentifierV1, draft refs. (#690)
The latest versions of draft-ietf-acme-tls-alpn specify a different
idPeAcmeIdentifierV1 than LEGO was previously using. The old value had
a conflict with an existing assignment.

This commit updates the idPeAcmeIdentifierV1 value to match draft-05 and
updates any references to the draft RFC to use the latest draft number.
2018-10-29 18:20:49 +01:00
Daniel McCarney
1164f441bd Client: Do not send a JWS body when POSTing challenges. (#689)
* Client: Do not send a JWS body when POSTing challenges.

In legacy ACME there was a requirement to send a JWS body that contained
a key authorization as part of all challenge initiation POSTs. Since
both the client and server can reconstitute the key authorization there
is no need to send it and modern ACME expects challenges to be initiated
with a JWS carrying the trivial empty JSON object (`{}`).  Some ACME
servers (e.g. Pebble in `-strict` mode) will reject all challenge POSTs
that have a legacy JWS body.

This commit updates the LEGO `acme/client.go`'s `validate` function to
send the correct JWS payload for challenge POSTs.
2018-10-29 17:35:49 +01:00
Jonathan Camp
1151b4e3be Update Azure auth to support instance metadata service (#684) 2018-10-23 10:03:31 +02:00
Konni Hartmann
9b2a3a1873 netcup: make unmarshalling of api-responses more leniant. (#685) 2018-10-23 02:01:13 +02:00
Ludovic Fernandez
a68cb214d3
Change Aurora DNS client (#683) 2018-10-22 20:42:22 +02:00
Andrew Savinykh
5511373184 dnsmadeeasy: log response body on error (#682) 2018-10-19 11:49:17 +02:00
Ludovic Fernandez
1d1b08ac15
enhance the WithLiveTestRequirements algorithm (#681)
* refactor: migrate test to envTest.

* refactor: enforce WithLiveTestRequirements logic.

* chore: remove gometalinter.

* fix: godoc.

* doc: add Docker badge.
2018-10-18 22:32:00 +02:00
Ludovic Fernandez
d775f2bcd5
fix: Change release condition. (#679) 2018-10-17 22:34:51 +02:00
Ludovic Fernandez
771524140c
fix: release condition. (#678) 2018-10-17 02:18:49 +02:00
Ludovic Fernandez
32058d57da
Prepare release v1.1.0 (#677) 2018-10-17 01:38:57 +02:00
Nick Maliwacki
6de343314c duckdns: fix subsubdomain (#676) 2018-10-16 21:28:49 +02:00
Ludovic Fernandez
4d21f8eec1
Add a test helper to manage env vars. (#675) 2018-10-16 17:52:57 +02:00
Ludovic Fernandez
4b2605dd52
contrib: add DreamHost documentation. (#673)
* contrib: add DreamHost documentation.
* contrib: add Aurora DNS doc link.
2018-10-15 19:55:24 +02:00
muratmoon
160d6fe603 cloudflare: removed dot suffix from authzone while searching for zone. (#672) 2018-10-14 21:11:35 +02:00
Craig Steinberger
484f0e5e35 Add DNS Provider for DreamHost (#668)
* add support for DreamHost DNS
2018-10-13 16:01:46 +02:00
Ludovic Fernandez
122c354163
Homogenization of the DNS provider tests (#671)
* refactor: min TTL
* refactor: sandbox.
* refactor: tests homogenization.
* refactor: missing require.
2018-10-12 19:29:18 +02:00
SALLEYRON Julien
01c63ec08d fix: bugs. (#670) 2018-10-11 15:49:33 +02:00
Brent Hughes
8a5cdcb4ef Add DNS Provider for Stackpath (#660) 2018-10-09 21:58:32 +02:00
Daniel McCarney
dd087560a0 contrib: fix ACME DNS Go client link (#669) 2018-10-09 19:09:25 +02:00
Ludovic Fernandez
20d50a559f
route53: fix challenge. (#665) 2018-10-09 19:03:07 +02:00
Ludovic Fernandez
21f6cd8a12
dnsimple: fix challenge. (#666) 2018-10-09 18:51:49 +02:00
Ludovic Fernandez
7701a8983b
contrib: add documentation about DNS providers API. (#662) 2018-10-09 18:30:32 +02:00
Ludovic Fernandez
79f14b5ab2
The TTL Min limit should throw an error. (#663) 2018-10-09 18:22:45 +02:00
Ludovic Fernandez
a90f03791c
namecheap: fix challenge (#661) 2018-10-09 18:16:05 +02:00
NicoMen
a07a82946f gandiv5: fix DNS Challenge (#664) 2018-10-08 16:51:29 +02:00
Matt Farmer
fa455bc037 Add support for reading DNS provider setup from files (#535) 2018-10-06 15:33:15 +02:00
Ludovic Fernandez
37ef38c4fc Add additional configuration environment variables to CLI help. (#659)
* feat: add additional configuration environment variables to CLI help.

* review: packages order.
2018-10-04 15:28:06 +00:00
Ludovic Fernandez
18fe57183d
cloudflare: use the official go client. (#658) 2018-10-03 00:02:01 +02:00
Ludovic Fernandez
8a8aa2d81b
Cloudflare: support for CF_API_KEY and CF_API_EMAIL (#647) 2018-10-02 22:34:34 +02:00
Ludovic Fernandez
c09b12be08 fix: ns1 wildcard. (#657) 2018-10-02 20:21:02 +00:00
Ludovic Fernandez
ad20bf90ff Migrate to golangci-lint (#644)
* refactor: linting.

- errcheck
- govet
- golint
- goconst
- spellcheck
- ...

* refactor: migrate from gometalinter to golangci-lint.
2018-09-24 19:07:20 +00:00
Ethan Young
38ffd0577b Correct example of renew action with days flag (#646) 2018-09-24 12:05:55 +02:00
Marques Johansson
58a023e92b Linode: updated to use the Linode APIv4 (#622) 2018-09-23 13:01:40 +02:00
Ludovic Fernandez
621237d072
fix: ns1 auth zone. (#640) 2018-09-21 17:47:58 +02:00
Ludovic Fernandez
3a46680b73 Fix: gcloud wildcard (#643)
* fix: gcloud wildcard.

* refactor: minor changes.
2018-09-21 15:28:50 +00:00
Ludovic Fernandez
ab0048544f
dns providers: fix errors. (#642)
* fix: gloud errors.
* fix: azure errors.
* fix: iij errors.
2018-09-20 23:18:13 +02:00
Ludovic Fernandez
039f5cf785
Use interface for logger. (#639) 2018-09-18 00:17:07 +02:00
evs-ch
83e2300e01 Add DNS provider for hosting.de (#624) 2018-09-15 23:25:14 +02:00
Ludovic Fernandez
e94285fcf3
CloudXNS: client isolation. (#629) 2018-09-15 19:26:45 +02:00
Ludovic Fernandez
55361cea8c
Use Testify. (#630) 2018-09-15 19:16:35 +02:00
Ludovic Fernandez
bba134ce87
Allow to configure TTL, interval and timeout (#634)
* feat: add GetOrDefaultXXX methods.
* refactor: configuration (alidns).
* refactor: configuration (azure).
* refactor: configuration (auroradns).
* refactor: configuration (bluecat).
* refactor: configuration (cloudflare).
* refactor: configuration (digitalocean).
* refactor: configuration (dnsimple).
* refactor: configuration (dnmadeeasy).
* refactor: configuration (dnspod).
* refactor: configuration (duckdns).
* refactor: configuration (dyn).
* refactor: configuration (exoscale).
* refactor: configuration (fastdns).
* refactor: configuration (gandi).
* refactor: configuration (gandiv5).
* refactor: configuration (gcloud).
* refactor: configuration (glesys).
* refactor: configuration (godaddy).
* refactor: configuration (iij).
* refactor: configuration (lightsail).
* refactor: configuration (linode).
* refactor: configuration (namecheap).
* refactor: configuration (namedotcom).
* refactor: configuration (netcup).
* refactor: configuration (nifcloud).
* refactor: configuration (ns1).
* refactor: configuration (otc).
* refactor: configuration (ovh).
* refactor: configuration (pdns).
* refactor: configuration (rackspace).
* refactor: configuration (rfc2136).
* refactor: configuration (route53).
* refactor: configuration (sakuracloud).
* refactor: configuration (vegadns).
* refactor: configuration (vultr).
2018-09-15 19:07:24 +02:00
SALLEYRON Julien
ad34a85dad refactor: rename Alibaba DNS env var. (#636) 2018-09-13 16:15:26 +02:00
NicoMen
839b137761 Update iij version to remove CRLF (#635) 2018-09-13 09:54:16 +02:00
mattn
088c707d4c Use path/filepath instead of path (#633) 2018-09-12 00:41:30 +02:00
NicoMen
035c27cdb7 Check error to avoid panic due to nil client (#631) 2018-09-10 09:52:43 +02:00
Sten Spans
cd5479a6b1 Panic on generating a certificate (#627) 2018-09-09 12:37:30 +02:00
yuexiaoyun
c22f8d841f Add DNS Provider for ALIYUN DNS (#620) 2018-09-08 15:17:23 +02:00
Silas Rech
89eff6fc7b Add filename parameter (#612) 2018-09-08 14:58:17 +02:00
Laurendus
5a1c3d2134 Add DNS Provider for netcup (#610) 2018-09-08 14:08:07 +02:00
Conor Mongey
ef7cd04002 Route53: Make it possible to configure from the env (#603) 2018-09-08 13:49:24 +02:00
keisuk-t
725b6b816a Add DNS Provider for IIJ (#606) 2018-09-08 12:52:36 +02:00
Yoan Blanc
f3cfe4a24a dnsprovider: upgrade egoscale to v0.11.1 (#617) 2018-09-08 12:09:59 +02:00
Craig Peterson
de3accf531 Submit all dns records up front, then validate serially (#607) 2018-09-08 11:56:51 +02:00
Ludovic Fernandez
6588bc6fa5 fix: for dep v0.5.0. (#609) 2018-08-01 14:41:40 +00:00
Conor Mongey
8b6701514c Route53: make provider timeouts configurable (#588) 2018-07-18 17:37:35 +02:00
Bill Shupp
baad3dea99 Add EXEC_MODE=RAW support to DNS Provider exec (#586) 2018-07-18 01:19:48 +02:00
fl1pfl0p
6edbd1572e DuckDNS: fix TXT record update url (#601) 2018-07-18 00:17:51 +02:00
Ludovic Fernandez
dd85f25cb3
Exclude TLS ALPN for some CLI arguments (#599) 2018-07-16 22:50:51 +02:00
Steve Coursen
771679fe3d NS1: use the authoritative zone and not the domain name (#463) 2018-07-14 12:31:57 +02:00
JP Phillips
255296ec82 gcloud: update findTxtRecords to use Name=fqdn and Type=TXT, use in Present and Cleanup (#594) 2018-07-14 12:02:03 +02:00
Daniel McCarney
04e2d74406 DNS Providers: Add ACME-DNS provider. (#591)
This commit adds a new DNS provider for
[acme-dns](https://github.com/joohoi/acme-dns) to allow Lego to set
DNS-01 challenge response TXT with an ACME-DNS server automatically.
ACME-DNS allows ceding minimal zone editing permissions to the ACME
client and can be useful when the primary DNS provider for the zone does
not allow scripting/API access but can set a CNAME to an ACME-DNS
server.

Lower level ACME-DNS API calls & account loading/storing is handled by
the `github.com/cpu/goacmedns` library.

The provider loads existing ACME-DNS accounts from the specified JSON
file on disk. Any accounts the provider registers on behalf of the user
will also be saved to this JSON file.

When required, the provider handles registering accounts with the
ACME-DNS server domains that do not already have an ACME-DNS account.
This will halt issuance with an error prompting the user to set the
one-time manual CNAME required to delegate the DNS-01 challenge record
to the ACME-DNS server. Subsequent runs will use the account from disk
and assume the CNAME is in-place.
2018-07-09 19:28:01 +02:00
Area128
d30c293647 SOA-API-EDIT should be SOA-EDIT-API (#593)
according to https://doc.powerdns.com/authoritative/domainmetadata.html#soa-edit-api
2018-07-03 16:14:43 +02:00
Daniel McCarney
8c82824a83 README: Change staging URL example for ACME v2. (#592) 2018-07-01 23:54:35 +02:00
Bill Shupp
e0d512138c Fix missing issuer certificates from Let's Encrypt (#587) 2018-07-01 02:35:39 +02:00
Derek Chen
3a1c6202f6 Lightsail: Fix Domain does not exist error (#512) 2018-07-01 02:24:20 +02:00
fuku
f9c0fbd298 Add DNS Provider for NIFCLOUD DNS (#532) 2018-07-01 02:13:22 +02:00
Bill Shupp
3f09ecc0b1 Add DNS Provider for VegaDNS (#553) 2018-07-01 01:45:24 +02:00
Ludovic Fernandez
9bb5589e17
feat: CA Server Name. (#590) 2018-07-01 01:12:36 +02:00
Ludovic Fernandez
a2543a2fde
Don't trust identifiers order. (#589)
ACME draft Section 7.4 "Applying for Certificate Issuance"
https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4
says:
	Clients SHOULD NOT make any assumptions about the sort order of
	"identifiers" or "authorizations" elements in the returned order
	object.
2018-07-01 01:06:46 +02:00
Ludovic Fernandez
94e14328ab
refactor: replace Dial by DialContext. (#585) 2018-07-01 00:55:57 +02:00
Robert Kästel
54422ab226 Order polling wait (#581) 2018-06-25 23:22:42 +02:00
Kazumichi Yamamoto
05921d9810 Add DNS Provider for SAKURA Cloud (#456) 2018-06-21 21:24:45 +02:00
Ludovic Fernandez
a1585a7b9a
Review DNS providers (#580)
* refactor: create log.Infof and log.Warnf
* refactor: review DNS providers.
    - use one `http.Client` by provider instead of one client by request
    - use the same receiver name `d` for all `DNSProvider`
    - use `http.MethodXXX`
* refactor: logger init.
2018-06-21 19:06:16 +02:00
Ludovic Fernandez
57782ac3c1
tls-alpn: add a function to return PEM blocks. (#579)
* feature(tls-alpn): add function to return PEM blocks.
2018-06-18 15:44:18 +02:00
Wyatt Johnson
d457f70ae0 TLS-ALPN-01 Challenge (#572)
* feat: implemented TLS-ALPN-01 challenge
2018-06-14 01:20:56 +02:00
Ludovic Fernandez
c4bbb4b819
Review DNS providers. (#565)
* refactor: review DNS providers.
2018-06-11 17:32:50 +02:00
Daniel McCarney
8f9e90b2a0 ACME HTTP: Allow customizing HTTP client x509.CertPool (#571)
This commit updates `acme/http.go` to allow customizing the
`*x509.CertPool` used by the `HTTPClient` by specifying the filepath of
a custom CA certificate via the `CA_CERTIFICATE` environment variable.

This allows developers to easily trust a non-standard CA when
interacting with an ACME test server (e.g. Pebble):

```
CA_CERTIFICATE=~/go/src/github.com/letsencrypt/pebble/test/certs/pebble.minica.pem \
lego \
  --server https://localhost:14000/dir \
  --email foo@bar.com \
  -d example.com \
  run
```
2018-06-11 15:45:59 +02:00
AndreKR
5005315fff dns/exec: Allow passing the program to the provider directly (#573)
This adds a function NewDNSProviderProgram() to the exec provider that allows to set the program to run directly instead of via the environment variable. This is similar to how other providers allow to set their credentials.
2018-06-10 13:51:01 -06:00
Ludovic Fernandez
7fedfd1388 fix: user-agent string order. (#566) 2018-06-03 12:23:01 -06:00
xenolf
27e0f97c9d
Fix wrong repository URL (#563) 2018-05-31 19:39:33 +02:00
nelsonkram
f17b1ce516 Added 'processing' status as valid challenge status (#561) 2018-05-31 17:22:37 +02:00
Wyatt Johnson
fefeb24353
Prepare release v1.0.0 (#559)
* Prepare release v1.0.0

* review: remove infrastructure items from CHANGELOG.md

* review: updated CHANGELOG.md message

* review: added ACME v1 reference in README.md

* review: README.md updates

* review: removed experimental note
2018-05-30 18:03:08 -06:00
Ludovic Fernandez
6004e599ed Manage vendor (#557)
* feat: add dep configuration files.

* chore: add vendor folder.

* refactor: update Dockerfile.

* review: remove git from Dockerfile.

* review: remove RUN apk.

* review: dep status.

* feat: added .dockerignore
2018-05-30 16:28:41 -06:00
Ludovic Fernandez
1b12c25e43 Add linters (#556)
* feat: add linters.
* fix: lint.
2018-05-30 16:03:55 -06:00
NicoMen
517f442fa3 Add details to the cloudflare error message (#452) 2018-05-30 20:26:07 +02:00
Dan Lüdtke
0593b64674 Fix gcloud naming inconsistency; parse projectID (#425)
This commit fixes the naming inconsistency of Google Cloud DNS being
named `googlecloud` in the source and `gcloud` in the user interface.
Furthermore, improving my previous commit, I realized that the project ID
is stored in the service account file. We can save one environment variable
when using service account file (e.g. in automation applications).
2018-05-30 20:20:21 +02:00
Ludovic Fernandez
e7fd871a9c
ACME V2 support (#555) 2018-05-30 19:53:04 +02:00
Pall Valmundsson
1d9b0906b1 Update Dockerfile (#484)
This updates the Dockerfile to use multi-stage builds and enables a build argument to specify which version or SHA is built.
2018-05-30 14:01:23 +02:00
Ludovic Fernandez
86f1721281
chore: ignore gitcookies. (#554) 2018-05-30 10:02:14 +02:00
Ludovic Fernandez
084e280934
Prepare release v0.5.0 (#552) 2018-05-30 02:58:11 +02:00
Ludovic Fernandez
3cc9a19a1c Add a release system. (#550)
* feat: add release system.
* feat: goreleaser.
* review: minor changes.
2018-05-29 11:40:00 -06:00
1150 changed files with 116040 additions and 12709 deletions

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
lego.exe
.lego
.gitcookies
.idea
.vscode/
dist/
builds/
docs/

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
github: ldez

104
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,104 @@
name: Bug Report
description: Create a report to help us improve.
labels: [bug]
body:
- type: checkboxes
id: terms
attributes:
label: Welcome
options:
- label: Yes, I'm using a binary release within 2 latest releases.
required: true
- 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
- type: textarea
id: expected
attributes:
label: What did you expect to see?
placeholder: Description.
validations:
required: true
- type: textarea
id: current
attributes:
label: What did you see instead?
placeholder: Description.
validations:
required: true
- type: dropdown
id: type
attributes:
label: How do you use lego?
options:
- Library
- Binary
- Docker image
- Through Traefik
- Through Caddy
- Through Terraform ACME provider
- Through Bitnami
- Other
validations:
required: true
- type: textarea
id: steps
attributes:
label: Reproduction steps
description: "How do you trigger this bug? Please walk us through it step by step."
placeholder: |
1. ...
2. ...
3. ...
...
validations:
required: true
- type: textarea
id: version
attributes:
label: Version of lego
description: |-
```console
$ lego --version
```
placeholder: Paste output here
render: console
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs
value: |-
<details>
```console
# paste output here
```
</details>
validations:
required: true
- type: textarea
id: go-env
attributes:
label: Go environment (if applicable)
value: |-
<details>
```console
$ go version && go env
# paste output here
```
</details>
validations:
required: false

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

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

View file

@ -0,0 +1,34 @@
name: Feature request
description: Suggest an idea for this project.
body:
- type: checkboxes
id: terms
attributes:
label: Welcome
options:
- label: Yes, I've searched similar issues on GitHub and didn't find any.
required: true
- type: dropdown
id: type
attributes:
label: How do you use lego?
options:
- Library
- Binary
- Docker image
- Through Traefik
- Through Caddy
- Through Terraform ACME provider
- Through Bitnami
- Other
validations:
required: true
- type: textarea
id: description
attributes:
label: Detailed Description
placeholder: Description.
validations:
required: true

View file

@ -0,0 +1,60 @@
name: New DNS provider support
description: Request for the support of a new DNS provider.
title: "Support for provider: "
labels: [enhancement, new-provider]
body:
- type: checkboxes
id: terms
attributes:
label: Welcome
options:
- 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
- label: Yes, I'm able to create a pull request and be able to maintain the implementation.
required: false
- label: Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider.
required: false
- type: dropdown
id: type
attributes:
label: How do you use lego?
options:
- Library
- Binary
- Docker image
- Through Traefik
- Through Caddy
- Through Terraform ACME provider
- Through Bitnami
- Other
validations:
required: true
- type: input
id: provider-link
attributes:
label: Link to the DNS provider
placeholder: Put your link here.
validations:
required: true
- type: input
id: api-link
attributes:
label: Link to the API documentation
placeholder: Put your link here.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Additional Notes
placeholder: Your notes.
validations:
required: false

50
.github/workflows/documentation.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Documentation
on:
push:
branches:
- master
jobs:
doc:
name: Build and deploy documentation
runs-on: ubuntu-latest
env:
GO_VERSION: '1.20'
HUGO_VERSION: 0.101.0
CGO_ENABLED: 0
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
# https://github.com/marketplace/actions/checkout
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Generate DNS docs
run: make generate-dns
- name: Install Hugo
run: |
wget -O /tmp/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.deb
sudo dpkg -i /tmp/hugo.deb
- name: Build Documentation
run: make docs-build
# https://github.com/marketplace/actions/github-pages
- name: Deploy to GitHub Pages
uses: crazy-max/ghaction-github-pages@v3
with:
target_branch: gh-pages
build_dir: docs/public
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

55
.github/workflows/go-cross.yml vendored Normal file
View file

@ -0,0 +1,55 @@
name: Go Matrix
on:
push:
branches:
- master
pull_request:
jobs:
cross:
name: Go
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
strategy:
matrix:
go-version: [ '1.19', '1.20', 1.x ]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
# https://github.com/marketplace/actions/checkout
- name: Checkout code
uses: actions/checkout@v3
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v3
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
%LocalAppData%\go-build
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Test
run: go test -v -cover ./...
- name: Build
run: go build -v -ldflags "-s -w" -trimpath -o ./dist/lego ./cmd/lego/

85
.github/workflows/pr.yml vendored Normal file
View file

@ -0,0 +1,85 @@
name: Main
on:
push:
branches:
- master
pull_request:
jobs:
main:
name: Main Process
runs-on: ubuntu-latest
env:
GO_VERSION: '1.20'
GOLANGCI_LINT_VERSION: v1.53.1
HUGO_VERSION: 0.54.0
CGO_ENABLED: 0
LEGO_E2E_TESTS: CI
MEMCACHED_HOSTS: localhost:11211
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
# https://github.com/marketplace/actions/checkout
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 0
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Check and get dependencies
run: |
go mod tidy
git diff --exit-code go.mod
git diff --exit-code go.sum
# https://golangci-lint.run/usage/install#other-ci
- name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }}
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION}
golangci-lint --version
- name: Install Pebble
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@main
- name: Install challtestsrv
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@main
- name: Set up a Memcached server
uses: niden/actions-memcached@v7
- name: Setup /etc/hosts
run: |
echo "127.0.0.1 acme.wtf" | sudo tee -a /etc/hosts
echo "127.0.0.1 lego.wtf" | sudo tee -a /etc/hosts
echo "127.0.0.1 acme.lego.wtf" | sudo tee -a /etc/hosts
echo "127.0.0.1 légô.wtf" | sudo tee -a /etc/hosts
echo "127.0.0.1 xn--lg-bja9b.wtf" | sudo tee -a /etc/hosts
- name: Make
run: |
make
make clean
- name: Install Hugo
run: |
wget -O /tmp/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.deb
sudo dpkg -i /tmp/hugo.deb
- name: Build Documentation
run: make docs-build

47
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Release
on:
push:
tags:
- v*
jobs:
release:
name: Release version
runs-on: ubuntu-latest
env:
GO_VERSION: '1.20'
CGO_ENABLED: 0
steps:
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: Docker Login
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# https://goreleaser.com/ci/actions/
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
with:
version: latest
args: release -p 1 --clean --timeout=90m
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO }}

6
.gitignore vendored
View file

@ -1,4 +1,6 @@
lego.exe
lego
.lego
.gitcookies
.idea
.vscode/
dist/
builds/

225
.golangci.yml Normal file
View file

@ -0,0 +1,225 @@
run:
timeout: 7m
skip-files: []
linters-settings:
govet:
check-shadowing: true
gocyclo:
min-complexity: 12
maligned:
suggest-new: true
goconst:
min-len: 3
min-occurrences: 3
funlen:
lines: -1
statements: 50
misspell:
locale: US
ignore-words:
- internetbs
depguard:
rules:
main:
deny:
- pkg: "github.com/instana/testify"
desc: not allowed
- pkg: "github.com/pkg/errors"
desc: Should be replaced by standard lib errors package
tagalign:
align: false
order:
- xml
- json
- yaml
- yml
- toml
- mapstructure
- url
godox:
keywords:
- FIXME
gocritic:
enabled-tags:
- diagnostic
- style
- performance
disabled-checks:
- paramTypeCombine # already handle by gofumpt.extra-rules
- whyNoLint # already handle by nonolint
- unnamedResult
- hugeParam
- sloppyReassign
- rangeValCopy
- octalLiteral
- ptrToRefParam
- appendAssign
- ruleguard
- httpNoBody
- exposedSyncMutex
revive:
rules:
- name: struct-tag
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
disabled: true
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
linters:
enable-all: true
disable:
- deadcode # deprecated
- exhaustivestruct # deprecated
- golint # deprecated
- ifshort # deprecated
- interfacer # deprecated
- maligned # deprecated
- nosnakecase # deprecated
- scopelint # deprecated
- structcheck # deprecated
- varcheck # deprecated
- cyclop # duplicate of gocyclo
- sqlclosecheck # not relevant (SQL)
- rowserrcheck # not relevant (SQL)
- execinquery # not relevant (SQL)
- lll
- gosec
- dupl # not relevant
- prealloc # too many false-positive
- bodyclose # too many false-positive
- gomnd
- testpackage # not relevant
- tparallel # not relevant
- paralleltest # not relevant
- nestif # too many false-positive
- wrapcheck
- goerr113 # 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
issues:
exclude-use-default: false
max-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: providers/dns/dns_providers.go
linters:
- gocyclo
- path: providers/dns/gcloud/googlecloud_test.go
text: 'string `(lego\.wtf|manhattan)` has (\d+) occurrences, make it a constant'
- path: providers/dns/zoneee/zoneee_test.go
text: 'string `(bar|foo)` has (\d+) occurrences, make it a constant'
- path: certcrypto/crypto.go
text: '(tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable'
- path: challenge/dns01/nameserver.go
text: '(defaultNameservers|recursiveNameservers|fqdnSoaCache|muFqdnSoaCache) is a global variable'
- path: challenge/dns01/nameserver_.+.go
text: 'dnsTimeout is a global variable'
- path: challenge/dns01/nameserver_test.go
text: 'findXByFqdnTestCases is a global variable'
- path: challenge/http01/domain_matcher.go
text: 'string `Host` has \d occurrences, make it a constant'
- path: challenge/http01/domain_matcher.go
text: 'cyclomatic complexity \d+ of func `parseForwardedHeader` is high'
- path: challenge/http01/domain_matcher.go
text: "Function 'parseForwardedHeader' has too many statements"
- path: challenge/tlsalpn01/tls_alpn_challenge.go
text: 'idPeAcmeIdentifierV1 is a global variable'
- path: log/logger.go
text: 'Logger is a global variable'
- path: 'e2e/(dnschallenge/)?[\d\w]+_test.go'
text: load is a global variable
- path: 'providers/dns/([\d\w]+/)*[\d\w]+_test.go'
text: 'envTest is a global variable'
- path: providers/dns/namecheap/namecheap_test.go
text: 'testCases is a global variable'
- path: providers/dns/acmedns/acmedns_test.go
text: 'egTestAccount is a global variable'
- path: providers/http/memcached/memcached_test.go
text: 'memcachedHosts is a global variable'
- path: providers/dns/sakuracloud/client_test.go
text: 'cyclomatic complexity 13 of func `(TestDNSProvider_cleanupTXTRecord_concurrent|TestDNSProvider_addTXTRecord_concurrent)` is high'
- path: providers/dns/dns_providers.go
text: "Function 'NewDNSChallengeProviderByName' has too many statements"
- path: cmd/flags.go
text: "Function 'CreateFlags' is too long"
- path: certificate/certificates.go
text: "Function 'GetOCSP' is too long"
- path: providers/dns/otc/client.go
text: "Function 'loginRequest' is too long"
- path: providers/dns/gandi/gandi.go
text: "Function 'Present' is too long"
- path: cmd/zz_gen_cmd_dnshelp.go
linters:
- gocyclo
- funlen
- path: providers/dns/checkdomain/internal/types.go
text: '`payed` is a misspelling of `paid`'
- path: providers/dns/namecheap/namecheap_test.go
text: 'cognitive complexity (\d+) of func `TestDNSProvider_getHosts` is high'
- path: platform/tester/env_test.go
linters:
- thelper
- path: providers/dns/oraclecloud/oraclecloud_test.go
text: 'SA1019: x509.EncryptPEMBlock has been deprecated since Go 1.16'
- path: challenge/http01/domain_matcher.go
text: 'yodaStyleExpr'
- path: providers/dns/dns_providers.go
text: 'Function name: NewDNSChallengeProviderByName,'
- path: providers/dns/sakuracloud/wrapper.go
text: 'mu is a global variable'
- path: providers/dns/hosttech/internal/client_test.go
text: 'Duplicate words \(0\) found'
- path: cmd/cmd_renew.go
text: 'cyclomatic complexity 16 of func `renewForDomains` is high'

135
.goreleaser.yml Normal file
View file

@ -0,0 +1,135 @@
project_name: lego
builds:
- binary: lego
main: ./cmd/lego/main.go
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -s -w -X main.version={{.Version}}
goos:
- windows
- darwin
- linux
- freebsd
- openbsd
- solaris
goarch:
- amd64
- 386
- arm
- arm64
- mips
- mipsle
- mips64
- mips64le
goarm:
- 7
- 6
- 5
gomips:
- hardfloat
- softfloat
ignore:
- goos: darwin
goarch: 386
- goos: openbsd
goarch: arm
archives:
- id: lego
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}'
format: tar.gz
format_overrides:
- goos: windows
format: zip
files:
- LICENSE
- CHANGELOG.md
docker_manifests:
- name_template: 'goacme/lego:{{ .Tag }}'
image_templates:
- 'goacme/lego:{{ .Tag }}-amd64'
- 'goacme/lego:{{ .Tag }}-arm64'
- 'goacme/lego:{{ .Tag }}-armv7'
- name_template: 'goacme/lego:latest'
image_templates:
- 'goacme/lego:{{ .Tag }}-amd64'
- 'goacme/lego:{{ .Tag }}-arm64'
- 'goacme/lego:{{ .Tag }}-armv7'
- name_template: 'goacme/lego:v{{ .Major }}.{{ .Minor }}'
image_templates:
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-amd64'
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-arm64'
- 'goacme/lego:v{{ .Major }}.{{ .Minor }}-armv7'
dockers:
- use: buildx
goos: linux
goarch: amd64
dockerfile: buildx.Dockerfile
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
- '--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'

View file

@ -1,16 +0,0 @@
language: go
go:
- 1.8.x
- 1.9.x
- tip
services:
- memcached
env:
- MEMCACHED_HOSTS=localhost:11211
install:
- go get -t ./...
script:
- go vet ./...
- go test -v ./...
before_install:
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && openssl aes-256-cbc -K $encrypted_26c593b079d9_key -iv $encrypted_26c593b079d9_iv -in .gitcookies.enc -out .gitcookies -d || true'

View file

@ -1,5 +1,823 @@
# Changelog
## [v4.12.2] - 2023-06-19
### Fixed:
- **[dnsprovider]** dnsmadeeasy: fix DeleteRecord
- **[lib]** fix: read status code from response
## [v4.12.1] - 2023-06-06
### Fixed:
- **[dnsprovider]** pdns: fix record value
## [v4.12.0] - 2023-05-28
### Added:
- **[lib,cli]** Initial ACME Renewal Info (ARI) Implementation
- **[dnsprovider]** Add DNS provider for Derak Cloud
- **[dnsprovider]** route53: pass ExternalID property to STS:AssumeRole API operation
- **[lib,cli]** Support custom duration for certificate
### Changed:
- **[dnsprovider]** Refactor DNS provider and client implementations
### Fixed:
- **[dnsprovider]** autodns: fixes wrong zone in api call if CNAME is used
- **[cli]** fix: archive only domain-related files on revoke
## [v4.11.0] - 2023-05-02
### Added:
- **[lib]** Support for certificate with raw IP SAN (RFC8738)
- **[dnsprovider]** Add Brandit.com as DNS provider
- **[dnsprovider]** Add DNS provider for Bunny
- **[dnsprovider]** Add DNS provider for Nodion
- **[dnsprovider]** Add Google Domains as DNS provider
- **[dnsprovider]** Add DNS provider for Plesk
### Changed:
- **[cli]** feat: add LEGO_CERT_PEM_PATH and LEGO_CERT_PFX_PATH to run hook
- **[lib,cli]** feat: add RSA 3072
- **[dnsprovider]** gcloud: update google APIs to latest version
- **[lib,dnsprovider,cname]** chore: replace GetRecord by GetChallengeInfo
### Fixed:
- **[dnsprovider]** rimuhosting: fix API base URL
## [v4.10.2] - 2023-02-26
Fix Docker image builds.
## [v4.10.1] - 2023-02-25
### Fixed:
- **[dnsprovider,cname]** acmedns: fix CNAME support
- **[dnsprovider]** dynu: fix subdomain support
## [v4.10.0] - 2023-02-10
### Added:
- **[dnsprovider]** Add DNS provider for dnsHome.de
- **[dnsprovider]** Add DNS provider for Liara
- **[dnsprovider]** Add DNS provider for UltraDNS
- **[dnsprovider]** Add DNS provider for Websupport
### Changed:
- **[dnsprovider]** ibmcloud: add support for subdomains
- **[dnsprovider]** infomaniak: CNAME support
- **[dnsprovider]** namesilo: add cleanup before add a DNS record
- **[dnsprovider]** route53: Allow static credentials to be supplied
- **[dnsprovider]** tencentcloud: support punycode domain
### Fixed:
- **[dnsprovider]** alidns: filter on record type
- **[dnsprovider]** arvancloud: replace arvancloud.com by arvancloud.ir
- **[dnsprovider]** hetzner: improve zone ID detection
- **[dnsprovider]** luadns: removed dot suffix from authzone while searching for zone
- **[dnsprovider]** pdns: fix usage of notify only when zone kind is Master or Slave
- **[dnsprovider]** return an error when extracting record name
## [v4.9.1] - 2022-11-25
### Changed:
-
- **[lib,cname]** cname: add log about CNAME entries
- **[dnsprovider]** regru: improve error handling
### Fixed:
-
- **[dnsprovider,cname]** fix CNAME support for multiple DNS providers
- **[dnsprovider,cname]** duckdns: fix CNAME support
- **[dnsprovider,cname]** oraclecloud: use fqdn to resolve zone
- **[dnsprovider]** hurricane: fix CNAME support
- **[lib,cname]** cname: stop trying to traverse cname if none have been found
## [v4.9.0] - 2022-10-03
### Added:
- **[dnsprovider]** Add DNS provider for CIVO
- **[dnsprovider]** Add DNS provider for VK Cloud
- **[dnsprovider]** Add DNS provider for YandexCloud
- **[dnsprovider]** digitalocean: configurable base URL
- **[dnsprovider]** loopia: add configurable API endpoint
- **[dnsprovider]** pdns: notify secondary servers after updates
### Changed:
- **[dnsprovider]** allinkl: removed deprecated sha1 hashing
- **[dnsprovider]** auroradns: update authentification
- **[dnsprovider]** dnspod: deprecated. Use Tencent Cloud instead.
- **[dnsprovider]** exoscale: migrate to API v2 endpoints
- **[dnsprovider]** gcloud: update golang.org/x/oauth2
- **[dnsprovider]** lightsail: cleanup
- **[dnsprovider]** sakuracloud: update api client library
- **[cname]** take out CNAME support from experimental features
- **[lib,cname]** add recursive CNAME lookup support
- **[lib]** Remove embedded issuer certificates from issued certificate if bundle is false
### Fixed:
- **[dnsprovider]** luadns: fix cname support
- **[dnsprovider]** njalla: fix record id unmarshal error
- **[dnsprovider]** tencentcloud: fix subdomain error
## [v4.8.0] - 2022-06-30
### Added
- **[dnsprovider]** Add DNS provider for Variomedia
- **[dnsprovider]** Add NearlyFreeSpeech DNS Provider
- **[cli]** Add a --user-agent flag to lego-cli
### Changed
- new logo
- **[cli]** feat: sleep at renewal
- **[cli]** cli/renew: skip random sleep if stdout is a terminal
- **[dnsprovider]** hetzner: set min TTL to 60s
- **[docs]** refactoring and cleanup
## [v4.7.0] - 2022-05-27
### Added:
- **[dnsprovider]** Add DNS provider for iwantmyname
- **[dnsprovider]** Add DNS Provider for IIJ DNS Platform Service
- **[dnsprovider]** Add DNS provider for Vercel
- **[dnsprovider]** route53: add assume role ARN
- **[dnsprovider]** dnsimple: add debug option
- **[cli]** feat: add `LEGO_CERT_PEM_PATH` and `LEGO_CERT_PFX_PATH`
### Changed:
- **[dnsprovider]** gcore: change dns api url
- **[dnsprovider]** bluecat: rewrite provider implementation
### Fixed:
- **[dnsprovider]** rfc2136: fix TSIG secret
- **[dnsprovider]** tencentcloud: fix InvalidParameter.DomainInvalid error when using DNS challenges
- **[lib]** fix: panic in certcrypto.ParsePEMPrivateKey
## [v4.6.0] - 2022-01-18
### Added
- **[dnsprovider]** Add DNS provider for UKFast SafeDNS
- **[dnsprovider]** Add DNS Provider for Tencent Cloud
- **[dnsprovider]** azure: add support for Azure Private Zone DNS
- **[dnsprovider]** exec: add sequence interval
- **[cli]** Add a `--pfx`, and `--pfx.pas`s option to generate a PKCS#12 (`.pfx`) file.
- **[lib]** Extended support of cert pool (`LEGO_CA_CERTIFICATES` and `LEGO_CA_SYSTEM_CERT_POOL`)
- **[lib,httpprovider]** added uds capability to http challenge server
### Changed
- **[lib]** Extend validity of TLS-ALPN-01 certificates to 365 days
- **[lib,cli]** Allows defining the reason for the certificate revocation
### Fixed
- **[dnsprovider]** mythicbeasts: fix token expiration
- **[dnsprovider]** rackspace: change zone ID to string
## [v4.5.3] - 2021-09-06
### Fixed:
- **[lib,cli]** fix: missing preferred chain param for renew request
## [v4.5.2] - 2021-09-01
### Added:
- **[dnsprovider]** Add DNS provider for all-inkl
- **[dnsprovider]** Add DNS provider for Epik
- **[dnsprovider]** Add DNS provider for freemyip.com
- **[dnsprovider]** Add DNS provider for g-core labs
- **[dnsprovider]** Add DNS provider for hosttech
- **[dnsprovider]** Add DNS Provider for IBM Cloud (SoftLayer)
- **[dnsprovider]** Add DNS provider for Internet.bs
- **[dnsprovider]** Add DNS provider for nicmanager
### Changed:
- **[dnsprovider]** alidns: support ECS instance RAM role
- **[dnsprovider]** alidns: support sts token credential
- **[dnsprovider]** azure: zone name as environment variable
- **[dnsprovider]** ovh: follow cname
- **[lib,cli]** Add AlwaysDeactivateAuthorizations flag to ObtainRequest
### Fixed:
- **[dnsprovider]** infomaniak: fix subzone support
- **[dnsprovider]** edgedns: fix Present and CleanUp logic
- **[dnsprovider]** lightsail: wrong Region env var name
- **[lib]** lib: fix backoff in SolverManager
- **[lib]** lib: use permanent error instead of context cancellation
- **[dnsprovider]** desec: bump to v0.6.0
## [v4.5.1] - 2021-09-01
Cancelled due to a CI issue, replaced by v4.5.2.
## [v4.5.0] - 2021-09-30
Cancelled due to a CI issue, replaced by v4.5.2.
## [v4.4.0] - 2021-06-08
### Added:
- **[dnsprovider]** Add DNS provider for Infoblox
- **[dnsprovider]** Add DNS provider for Porkbun
- **[dnsprovider]** Add DNS provider for Simply.com
- **[dnsprovider]** Add DNS provider for Sonic
- **[dnsprovider]** Add DNS provider for VinylDNS
- **[dnsprovider]** Add DNS provider for wedos
### Changed:
- **[cli]** log: Use stderr instead of stdout.
- **[dnsprovider]** hostingde: autodetection of the zone name.
- **[dnsprovider]** scaleway: use official SDK
- **[dnsprovider]** powerdns: several improvements
- **[lib]** lib: improve wait.For returns.
### Fixed:
- **[dnsprovider]** hurricane: add API rate limiter.
- **[dnsprovider]** hurricane: only treat first word of response body as response code
- **[dnsprovider]** exoscale: fix DNS provider debugging
- **[dnsprovider]** wedos: fix api call parameters
- **[dnsprovider]** nifcloud: Get zone info from dns01.FindZoneByFqdn
- **[cli,lib]** csr: Support the type `NEW CERTIFICATE REQUEST`
## [v4.3.1] - 2021-03-12
### Fixed:
- **[dnsprovider]** exoscale: fix dependency version.
## [v4.3.0] - 2021-03-10
### Added:
- **[dnsprovider]** Add DNS provider for Njalla
- **[dnsprovider]** Add DNS provider for Domeneshop
- **[dnsprovider]** Add DNS provider for Hurricane Electric
- **[dnsprovider]** designate: support for Openstack Application Credentials
- **[dnsprovider]** edgedns: support for .edgerc file
### Changed:
- **[dnsprovider]** infomaniak: Make error message more meaningful
- **[dnsprovider]** cloudns: Improve reliability
- **[dnsprovider]** rfc2163: Removed support for MD5 algorithm. The default algorithm is now SHA1.
### Fixed:
- **[dnsprovider]** desec: fix error with default TTL
- **[dnsprovider]** mythicbeasts: implement `ProviderTimeout`
- **[dnsprovider]** dnspod: improve search accuracy when a domain have more than 100 records
- **[lib]** Increase HTTP client timeouts
- **[lib]** preferred chain only match root name
## [v4.2.0] - 2021-01-24
### Added:
- **[dnsprovider]** Add DNS provider for Loopia
- **[dnsprovider]** Add DNS provider for Ionos.
### Changed:
- **[dnsprovider]** acme-dns: update cpu/goacmedns to v0.1.1.
- **[dnsprovider]** inwx: Increase propagation timeout to 360s to improve robustness
- **[dnsprovider]** vultr: Update to govultr v2 API
- **[dnsprovider]** pdns: get exact zone instead of all zones
### Fixed:
- **[dnsprovider]** vult, dnspod: fix default HTTP timeout.
- **[dnsprovider]** pdns: URL request creation.
- **[lib]** errors: Fix instance not being printed
## [v4.1.3] - 2020-11-25
### Fixed:
- **[dnsprovider]** azure: fix error handling.
## [v4.1.2] - 2020-11-21
### Fixed:
- **[lib]** fix: preferred chain support.
## [v4.1.1] - 2020-11-19
### Fixed:
- **[dnsprovider]** otc: select correct zone if multiple returned
- **[dnsprovider]** azure: fix target must be a non-nil pointer
## [v4.1.0] - 2020-11-06
### Added:
- **[dnsprovider]** Add DNS provider for Infomaniak
- **[dnsprovider]** joker: add support for SVC API
- **[dnsprovider]** gcloud: add an option to allow the use of private zones
### Changed:
- **[dnsprovider]** rfc2136: ensure TSIG algorithm is fully qualified
- **[dnsprovider]** designate: Deprecate OS_TENANT_NAME as required field
### Fixed:
- **[lib]** acme/api: use postAsGet instead of post for AccountService.Get
- **[lib]** fix: use http.Header.Set method instead of Add.
## [v4.0.1] - 2020-09-03
### Fixed:
- **[dnsprovider]** exoscale: change dependency version.
## [v4.0.0] - 2020-09-02
### Added:
- **[cli], [lib]** Support "alternate" certificate links for selecting different signing Chains
### Changed:
- **[cli]** Replaces `ec384` by `ec256` as default key-type
- **[lib]** Changes `ObtainForCSR` method signature
### Removed:
- **[dnsprovider]** Replaces FastDNS by EdgeDNS
- **[dnsprovider]** Removes old Linode provider
- **[lib]** Removes `AddPreCheck` function
## [v3.9.0] - 2020-09-01
### Added:
- **[dnsprovider]** Add Akamai Edgedns. Deprecate FastDNS
- **[dnsprovider]** Add DNS provider for HyperOne
### Changed:
- **[dnsprovider]** designate: add support for Openstack clouds.yaml
- **[dnsprovider]** azure: allow selecting environments
- **[dnsprovider]** desec: applies API rate limits.
### Fixed:
- **[dnsprovider]** namesilo: fix cleanup.
## [v3.8.0] - 2020-07-02
### Added:
- **[cli]** cli: add hook on the run command.
- **[dnsprovider]** inwx: Two-Factor-Authentication
- **[dnsprovider]** Add DNS provider for ArvanCloud
### Changed:
- **[dnsprovider]** vultr: bumping govultr version
- **[dnsprovider]** desec: improve error logs.
- **[lib]** Ensures the return of a location during account updates
- **[dnsprovider]** route53: Document all AWS credential environment variables
### Fixed:
- **[dnsprovider]** stackpath: fix subdomain support.
- **[dnsprovider]** arvandcloud: fix record name.
- **[dnsprovider]** fix: multi-va.
- **[dnsprovider]** constellix: fix search records API call.
- **[dnsprovider]** hetzner: fix record name.
- **[lib]** Registrar.ResolveAccountByKey: Fix malformed request
## [v3.7.0] - 2020-05-11
### Added:
- **[dnsprovider]** Add DNS provider for Netlify.
- **[dnsprovider]** Add DNS provider for deSEC.io
- **[dnsprovider]** Add DNS provider for LuaDNS
- **[dnsprovider]** Adding Hetzner DNS provider
- **[dnsprovider]** Add DNS provider for Mythic beasts DNSv2
- **[dnsprovider]** Add DNS provider for Yandex.
### Changed:
- **[dnsprovider]** Upgrade DNSimple client to 0.60.0
- **[dnsprovider]** update aws sdk
### Fixed:
- **[dnsprovider]** autodns: removes TXT records during CleanUp.
- **[dnsprovider]** Fix exoscale HTTP timeout
- **[cli]** fix: renew path information.
- **[cli]** Fix account storage location warning message
## [v3.6.0] - 2020-04-24
### Added:
- **[dnsprovider]** Add DNS provider for CloudDNS.
- **[dnsprovider]** alicloud: add support for domain with punycode
- **[dnsprovider]** cloudns: Add subuser support
- **[cli]** Information about renewed certificates are now passed to the renew hook
### Changed:
- **[dnsprovider]** acmedns: Update cpu/goacmedns v0.0.1 -&gt; v0.0.2
- **[dnsprovider]** alicloud: update sdk dependency version to v1.61.112
- **[dnsprovider]** azure: Allow for the use of MSI
- **[dnsprovider]** constellix: improve challenge.
- **[dnsprovider]** godaddy: allow parallel solve.
- **[dnsprovider]** namedotcom: get the actual registered domain so we can remove just that from the hostname to be created
- **[dnsprovider]** transip: updated the client to v6
### Fixed:
- **[dnsprovider]** ns1: fix missing domain in log
- **[dnsprovider]** rimuhosting: use HTTP client from config.
## [v3.5.0] - 2020-03-15
### Added:
- **[dnsprovider]** Add DNS provider for Dynu.
- **[dnsprovider]** Add DNS provider for reg.ru
- **[dnsprovider]** Add DNS provider for Zonomi and RimuHosting.
- **[cli]** Building binaries for arm 6 and 5
- **[cli]** Uses CGO_ENABLED=0
- **[cli]** Multi-arch Docker image.
- **[cli]** Adds `--name` flag to list command.
### Changed:
- **[lib]** lib: Improve cleanup log messages.
- **[lib]** Wrap errors.
### Fixed:
- **[dnsprovider]** azure: pass AZURE_CLIENT_SECRET_FILE to autorest.Authorizer
- **[dnsprovider]** gcloud: fixes issues when used with GKE Workload Identity
- **[dnsprovider]** oraclecloud: fix subdomain support
## [v3.4.0] - 2020-02-25
### Added:
- **[dnsprovider]** Add DNS provider for Constellix
- **[dnsprovider]** Add DNS provider for Servercow.
- **[dnsprovider]** Add DNS provider for Scaleway
- **[cli]** Add &#34;LEGO_PATH&#34; environment variable
### Changed:
- **[dnsprovider]** route53: allow custom client to be provided
- **[dnsprovider]** namecheap: allow external domains
- **[dnsprovider]** namecheap: add sandbox support.
- **[dnsprovider]** ovh: Improve provider documentation
- **[dnsprovider]** route53: Improve provider documentation
### Fixed:
- **[dnsprovider]** zoneee: fix subdomains.
- **[dnsprovider]** designate: Don&#39;t clean up managed records like SOA and NS
- **[dnsprovider]** dnspod: update lib.
- **[lib]** crypto: Treat CommonName as optional
- **[lib]** chore: update cenkalti/backoff to v4.
## [v3.3.0] - 2020-01-08
### Added:
- **[dnsprovider]** Add DNS provider for Checkdomain
- **[lib]** Add support to update account
### Changed:
- **[dnsprovider]** gcloud: Auto-detection of the project ID.
- **[lib]** Successfully parse private key PEM blocks
### Fixed:
- **[dnsprovider]** Update dnspod, because of API breaking changes.
## [v3.2.0] - 2019-11-10
### Added:
- **[dnsprovider]** Add support for autodns
### Changed:
- **[dnsprovider]** httpreq: Allow use environment vars from a `_FILE` file
- **[lib]** Don&#39;t deactivate valid authorizations
- **[lib]** Expose more SOA fields found by dns01.FindZoneByFqdn
### Fixed:
- **[dnsprovider]** use token as unique ID.
## [v3.1.0] - 2019-10-07
### Added:
- **[dnsprovider]** Add DNS provider for Liquid Web
- **[dnsprovider]** cloudflare: add support for API tokens
- **[cli]** feat: ease operation behind proxy servers
### Changed:
- **[dnsprovider]** cloudflare: update client
- **[dnsprovider]** linodev4: propagation timeout configuration.
### Fixed:
- **[dnsprovider]** ovh: fix int overflow.
- **[dnsprovider]** bindman: fix client version.
## [v3.0.2] - 2019-08-15
### Fixed:
- Invalid pseudo version (related to Cloudflare client).
## [v3.0.1] - 2019-08-14
There was a problem when creating the tag v3.0.1, this tag has been invalidate.
## [v3.0.0] - 2019-08-05
### Changed:
- migrate to go module (new import github.com/go-acme/lego/v3/)
- update DNS clients
## [v2.7.2] - 2019-07-30
### Fixed:
- **[dnsprovider]** vultr: quote TXT record
## [v2.7.1] - 2019-07-22
### Fixed:
- **[dnsprovider]** vultr: invalid record type.
## [v2.7.0] - 2019-07-17
### Added:
- **[dnsprovider]** Add DNS provider for namesilo
- **[dnsprovider]** Add DNS provider for versio.nl
### Changed:
- **[dnsprovider]** Update DNS providers libs.
- **[dnsprovider]** joker: support username and password.
- **[dnsprovider]** Vultr: Switch to official client
### Fixed:
- **[dnsprovider]** otc: Prevent sending empty body.
## [v2.6.0] - 2019-05-27
### Added:
- **[dnsprovider]** Add support for Joker.com DMAPI
- **[dnsprovider]** Add support for Bindman DNS provider
- **[dnsprovider]** Add support for EasyDNS
- **[lib]** Get an existing certificate by URL
### Changed:
- **[dnsprovider]** digitalocean: LEGO_EXPERIMENTAL_CNAME_SUPPORT support
- **[dnsprovider]** gcloud: Use fqdn to get zone Present/CleanUp
- **[dnsprovider]** exec: serial behavior
- **[dnsprovider]** manual: serial behavior.
- **[dnsprovider]** Strip newlines when reading environment variables from `_FILE` suffixed files.
### Fixed:
- **[cli]** fix: cli disable-cp option.
- **[dnsprovider]** gcloud: fix zone visibility.
## [v2.5.0] - 2019-04-17
### Added:
- **[cli]** Adds renew hook
- **[dnsprovider]** Adds 'Since' to DNS providers documentation
### Changed:
- **[dnsprovider]** gcloud: use public DNS zones
- **[dnsprovider]** route53: enhance documentation.
### Fixed:
- **[dnsprovider]** cloudns: fix TTL and status validation
- **[dnsprovider]** sakuracloud: supports concurrent update
- **[dnsprovider]** Disable authz when solve fail.
- Add tzdata to the Docker image.
## [v2.4.0] - 2019-03-25
- Migrate from xenolf/lego to go-acme/lego.
### Added:
- **[dnsprovider]** Add DNS Provider for Domain Offensive (do.de)
- **[dnsprovider]** Adds information about '_FILE' suffix.
### Fixed:
- **[cli,dnsprovider]** Add 'manual' provider to the output of dnshelp
- **[dnsprovider]** hostingde: Use provided ZoneName instead of domain
- **[dnsprovider]** pdns: fix wildcard with SANs
## [v2.3.0] - 2019-03-11
### Added:
- **[dnsprovider]** Add DNS Provider for ClouDNS.net
- **[dnsprovider]** Add DNS Provider for Oracle Cloud
### Changed:
- **[cli]** Adds log when no renewal.
- **[dnsprovider,lib]** Add a mechanism to wrap a PreCheckFunc
- **[dnsprovider]** oraclecloud: better way to get private key.
- **[dnsprovider]** exoscale: update library
### Fixed:
- **[dnsprovider]** OVH: Refresh zone after deleting challenge record
- **[dnsprovider]** oraclecloud: ttl config and timeout
- **[dnsprovider]** hostingde: fix client fails if customer has no access to dns-groups
- **[dnsprovider]** vscale: getting sub-domain
- **[dnsprovider]** selectel: getting sub-domain
- **[dnsprovider]** vscale: fix TXT records clean up
- **[dnsprovider]** selectel: fix TXT records clean up
## [v2.2.0] - 2019-02-08
### Added:
- **[dnsprovider]** Add support for Openstack Designate as a DNS provider
- **[dnsprovider]** gcloud: Option to specify gcloud service account json by env as string
- **[experimental feature]** Resolve CNAME when creating dns-01 challenge. To enable: set `LEGO_EXPERIMENTAL_CNAME_SUPPORT` to `true`.
### Changed:
- **[cli]** Applies Lets Encrypts recommendation about renew. The option `--days` of the command `renew` has a new default value (`30`)
- **[lib]** Uses a jittered exponential backoff
### Fixed:
- **[cli]** CLI and key type.
- **[dnsprovider]** httpreq: Endpoint with path.
- **[dnsprovider]** fastdns: Do not overwrite existing TXT records
- Log wildcard domain correctly in validation
## [v2.1.0] - 2019-01-24
### Added:
- **[dnsprovider]** Add support for zone.ee as a DNS provider.
### Changed:
- **[dnsprovider]** nifcloud: Change DNS base url.
- **[dnsprovider]** gcloud: More detailed information about Google Cloud DNS.
### Fixed:
- **[lib]** fix: OCSP, set HTTP client.
- **[dnsprovider]** alicloud: fix pagination.
- **[dnsprovider]** namecheap: fix panic.
## [v2.0.0] - 2019-01-09
### Added:
- **[cli,lib]** Option to disable the complete propagation Requirement
- **[lib,cli]** Support non-ascii domain name (punnycode)
- **[cli,lib]** Add configurable timeout when obtaining certificates
- **[cli]** Archive revoked certificates
- **[cli]** Add command to list certificates.
- **[cli]** support for renew with CSR
- **[cli]** add SAN on renew
- **[lib]** Adds `Remove` for challenges
- **[lib]** Add version to xenolf-acme in User-Agent.
- **[dnsprovider]** The ability for a DNS provider to solve the challenge sequentially
- **[dnsprovider]** Add DNS provider for &#34;HTTP request&#34;.
- **[dnsprovider]** Add DNS Provider for Vscale
- **[dnsprovider]** Add DNS Provider for TransIP
- **[dnsprovider]** Add DNS Provider for inwx
- **[dnsprovider]** alidns: add support to handle more than 20 domains
### Changed:
- **[lib]** Check all challenges in a predictable order
- **[lib]** Poll authz URL instead of challenge URL
- **[lib]** Check all nameservers in a predictable order
- **[lib]** Logs every iteration of waiting for the propagation
- **[cli]** `--http`: enable HTTP challenge **important**
- **[cli]** `--http.port`: previously named `--http`
- **[cli]** `--http.webroot`: previously named `--webroot`
- **[cli]** `--http.memcached-host`: previously named `--memcached-host`
- **[cli]** `--tls`: enable TLS challenge **important**
- **[cli]** `--tls.port`: previously named `--tls`
- **[cli]** `--dns.resolvers`: previously named `--dns-resolvers`
- **[cli]** the option `--days` of the command `renew` has default value (`15`)
- **[dnsprovider]** gcloud: Use GCE_PROJECT for project always, if specified
### Removed:
- **[lib]** Remove `SetHTTP01Address`
- **[lib]** Remove `SetTLSALPN01Address`
- **[lib]** Remove `Exclude`
- **[cli]** Remove `--exclude`, `-x`
### Fixed:
- **[lib]** Fixes revocation for subdomains and non-ascii domains
- **[lib]** Disable pending authorizations
- **[dnsprovider]** transip: concurrent access to the API.
- **[dnsprovider]** gcloud: fix for wildcard
- **[dnsprovider]** Azure: Do not overwrite existing TXT records
- **[dnsprovider]** fix: Cloudflare error.
## [v1.2.0] - 2018-11-04
### Added:
- **[dnsprovider]** Add DNS Provider for ConoHa DNS
- **[dnsprovider]** Add DNS Provider for MyDNS.jp
- **[dnsprovider]** Add DNS Provider for Selectel
### Fixed:
- **[dnsprovider]** netcup: make unmarshalling of api-responses more lenient.
### Changed:
- **[dnsprovider]** aurora: change DNS client
- **[dnsprovider]** azure: update auth to support instance metadata service
- **[dnsprovider]** dnsmadeeasy: log response body on error
- **[lib]** TLS-ALPN-01: Update idPeAcmeIdentifierV1, draft refs.
- **[lib]** Do not send a JWS body when POSTing challenges.
- **[lib]** Support POST-as-GET.
## [v1.1.0] - 2018-10-16
### Added:
- **[lib]** TLS-ALPN-01 Challenge
- **[cli]** Add filename parameter
- **[dnsprovider]** Allow to configure TTL, interval and timeout
- **[dnsprovider]** Add support for reading DNS provider setup from files
- **[dnsprovider]** Add DNS Provider for ACME-DNS
- **[dnsprovider]** Add DNS Provider for ALIYUN DNS
- **[dnsprovider]** Add DNS Provider for DreamHost
- **[dnsprovider]** Add DNS provider for hosting.de
- **[dnsprovider]** Add DNS Provider for IIJ
- **[dnsprovider]** Add DNS Provider for netcup
- **[dnsprovider]** Add DNS Provider for NIFCLOUD DNS
- **[dnsprovider]** Add DNS Provider for SAKURA Cloud
- **[dnsprovider]** Add DNS Provider for Stackpath
- **[dnsprovider]** Add DNS Provider for VegaDNS
- **[dnsprovider]** exec: add EXEC_MODE=RAW support.
- **[dnsprovider]** cloudflare: support for CF_API_KEY and CF_API_EMAIL
### Fixed:
- **[lib]** Don't trust identifiers order.
- **[lib]** Fix missing issuer certificates from Let's Encrypt
- **[dnsprovider]** duckdns: fix TXT record update url
- **[dnsprovider]** duckdns: fix subsubdomain
- **[dnsprovider]** gcloud: update findTxtRecords to use Name=fqdn and Type=TXT
- **[dnsprovider]** lightsail: Fix Domain does not exist error
- **[dnsprovider]** ns1: use the authoritative zone and not the domain name
- **[dnsprovider]** ovh: check error to avoid panic due to nil client
### Changed:
- **[lib]** Submit all dns records up front, then validate serially
## [v1.0.0] - 2018-05-30
### Changed:
- **[lib]** ACME v2 Support.
- **[dnsprovider]** Renamed `/providers/dns/googlecloud` to `/providers/dns/gcloud`.
- **[dnsprovider]** Modified Google Cloud provider `gcloud.NewDNSProviderServiceAccount` function to extract the project id directly from the service account file.
- **[dnsprovider]** Made errors more verbose for the Cloudflare provider.
## [v0.5.0] - 2018-05-29
### Added:
- **[dnsprovider]** Add DNS challenge provider `exec`
- **[dnsprovider]** Add DNS Provider for Akamai FastDNS
- **[dnsprovider]** Add DNS Provider for Bluecat DNS
- **[dnsprovider]** Add DNS Provider for CloudXNS
- **[dnsprovider]** Add DNS Provider for Duck DNS
- **[dnsprovider]** Add DNS Provider for Gandi Beta Platform (LiveDNS)
- **[dnsprovider]** Add DNS Provider for GleSYS API
- **[dnsprovider]** Add DNS Provider for GoDaddy
- **[dnsprovider]** Add DNS Provider for Lightsail
- **[dnsprovider]** Add DNS Provider for Name.com
### Fixed:
- **[dnsprovider]** Azure: Added missing environment variable in the comments
- **[dnsprovider]** PowerDNS: Fix zone URL, add leading slash.
- **[dnsprovider]** DNSimple: Fix api
- **[cli]** Correct help text for `--dns-resolvers` default.
- **[cli]** renew/revoke - don't panic on wrong account.
- **[lib]** Fix zone detection for cross-zone cnames.
- **[lib]** Use proxies from environment when making outbound http connections.
### Changed:
- **[lib]** Users of an effective top-level domain can use the DNS challenge.
- **[dnsprovider]** Azure: Refactor to work with new Azure SDK version.
- **[dnsprovider]** Cloudflare and Azure: Adding output of which envvars are missing.
- **[dnsprovider]** Dyn DNS: Slightly improve provider error reporting.
- **[dnsprovider]** Exoscale: update to latest egoscale version.
- **[dnsprovider]** Route53: Use NewSessionWithOptions instead of deprecated New.
## [0.4.1] - 2017-09-26
### Added:
@ -147,8 +965,8 @@
## [0.1.0] - 2015-12-03
- Initial release
[0.3.1]: https://github.com/xenolf/lego/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/xenolf/lego/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/xenolf/lego/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/xenolf/lego/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/xenolf/lego/tree/v0.1.0
[0.3.1]: https://github.com/go-acme/lego/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/go-acme/lego/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/go-acme/lego/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/go-acme/lego/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/go-acme/lego/tree/v0.1.0

View file

@ -20,7 +20,7 @@ It is up to you to make a strong point about your proposal and convince us of th
## Pull requests
Patches, new features and improvements are a great way to help the project.
Patches, new features and improvements are a great way to help the project.
Please keep them focused on one thing and do not include unrelated commits.
All pull requests which alter the behaviour of the program, add new behaviour or somehow alter code in a non-trivial way should **always** include tests.
@ -30,3 +30,51 @@ a lot of time on something the project's developers might not want to merge into
**IMPORTANT**: By submitting a patch, you agree to allow the project
owners to license your work under the terms of the [MIT License](LICENSE).
### How to create a pull request
Requirements:
- `go` v1.15+
- 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
# Add the go-acme/lego remote
git remote add upstream git@github.com:go-acme/lego.git
git fetch upstream
```
```bash
# Create your branch
git checkout -b my-feature
## Create your code ##
```
```bash
# Format
make fmt
# Linters
make checks
# Tests
make test
# Compile
make build
```
```bash
# push your branch
git push -u origin my-feature
## create a pull request on GitHub ##
```

View file

@ -1,15 +1,24 @@
FROM alpine:3.6
FROM golang:1-alpine as builder
ENV GOPATH /go
ENV LEGO_VERSION tags/v0.4.1
RUN apk --no-cache --no-progress add make git
RUN apk update && apk add --no-cache --virtual run-dependencies ca-certificates && \
apk add --no-cache --virtual build-dependencies go git musl-dev && \
go get -u github.com/xenolf/lego && \
cd ${GOPATH}/src/github.com/xenolf/lego && \
git checkout ${LEGO_VERSION} && \
go build -o /usr/bin/lego . && \
apk del build-dependencies && \
rm -rf ${GOPATH}
WORKDIR /go/lego
ENV GO111MODULE on
# Download go modules
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN make build
FROM alpine:3
RUN apk update \
&& apk add --no-cache ca-certificates tzdata \
&& update-ca-certificates
COPY --from=builder /go/lego/dist/lego /usr/bin/lego
ENTRYPOINT [ "/usr/bin/lego" ]

80
Makefile Normal file
View file

@ -0,0 +1,80 @@
.PHONY: clean checks test build image e2e fmt
export GO111MODULE=on
export CGO_ENABLED=0
LEGO_IMAGE := goacme/lego
MAIN_DIRECTORY := ./cmd/lego/
BIN_OUTPUT := $(if $(filter $(shell go env GOOS), windows), dist/lego.exe, dist/lego)
TAG_NAME := $(shell git tag -l --contains HEAD)
SHA := $(shell git rev-parse HEAD)
VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA))
default: clean generate-dns checks test build
clean:
@echo BIN_OUTPUT: ${BIN_OUTPUT}
rm -rf dist/ builds/ cover.out
build: clean
@echo Version: $(VERSION)
go build -trimpath -ldflags '-X "main.version=${VERSION}"' -o ${BIN_OUTPUT} ${MAIN_DIRECTORY}
image:
@echo Version: $(VERSION)
docker build -t $(LEGO_IMAGE) .
test: clean
go test -v -cover ./...
e2e: clean
LEGO_E2E_TESTS=local go test -count=1 -v ./e2e/...
checks:
golangci-lint run
# Release helper
.PHONY: patch minor major detach
patch:
go run internal/release.go release -m patch
minor:
go run internal/release.go release -m minor
major:
go run internal/release.go release -m major
detach:
go run internal/release.go detach
# Docs
.PHONY: docs-build docs-serve docs-themes
docs-build: generate-dns
@make -C ./docs hugo-build
docs-serve: generate-dns
@make -C ./docs hugo
docs-themes:
@make -C ./docs hugo-themes
# DNS Documentation
.PHONY: generate-dns validate-doc
generate-dns:
go generate ./...
validate-doc: generate-dns
validate-doc: DOC_DIRECTORIES := ./docs/ ./cmd/
validate-doc:
@if git diff --exit-code --quiet $(DOC_DIRECTORIES) 2>/dev/null; then \
echo 'All documentation changes are done the right way.'; \
else \
echo 'The documentation must be regenerated, please use `make generate-dns`.'; \
git status --porcelain -- $(DOC_DIRECTORIES) 2>/dev/null; \
exit 2; \
fi

309
README.md
View file

@ -1,273 +1,90 @@
# lego
Let's Encrypt client and ACME library written in Go
<div align="center">
<img alt="lego logo" src="./docs/static/images/lego-logo.min.svg">
<p>Automatic Certificates and HTTPS for everyone.</p>
</div>
[![GoDoc](https://godoc.org/github.com/xenolf/lego/acme?status.svg)](https://godoc.org/github.com/xenolf/lego/acme)
[![Build Status](https://travis-ci.org/xenolf/lego.svg?branch=master)](https://travis-ci.org/xenolf/lego)
[![Dev Chat](https://img.shields.io/badge/dev%20chat-gitter-blue.svg?label=dev+chat)](https://gitter.im/xenolf/lego)
[![Beerpay](https://beerpay.io/xenolf/lego/badge.svg)](https://beerpay.io/xenolf/lego)
# Lego
#### General
This is a work in progress. Please do *NOT* run this on a production server and please report any bugs you find!
Let's Encrypt client and ACME library written in Go.
#### Installation
lego supports both binary installs and install from source.
[![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/)
To get the binary just download the latest release for your OS/Arch from [the release page](https://github.com/xenolf/lego/releases)
and put the binary somewhere convenient. lego does not assume anything about the location you run it from.
To install from source, just run
```
go get -u github.com/xenolf/lego
```
To build lego inside a Docker container, just run
```
docker build -t lego .
```
##### From the package manager
- [ArchLinux (AUR)](https://aur.archlinux.org/packages/lego-git):
```
yaourt -S lego-git
```
#### Features
## 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 [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 all ACME challenges
- HTTP (http-01)
- TLS with Server Name Indication (tls-sni-01)
- DNS (dns-01)
- TLS (tls-alpn-01)
- SAN certificate support
- Comes with multiple optional [DNS providers](https://github.com/xenolf/lego/tree/master/providers/dns)
- [Custom challenge solvers](https://github.com/xenolf/lego/wiki/Writing-a-Challenge-Solver)
- [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
Please keep in mind that CLI switches and APIs are still subject to change.
## Installation
When using the standard `--path` option, all certificates and account configurations are saved to a folder *.lego* in the current working directory.
How to [install](https://go-acme.github.io/lego/installation/).
#### Sudo
The CLI does not require root permissions but needs to bind to port 80 and 443 for certain challenges.
To run the CLI without sudo, you have four options:
## Usage
- Use setcap 'cap_net_bind_service=+ep' /path/to/program
- Pass the `--http` or/and the `--tls` option and specify a custom port to bind to. In this case you have to forward port 80/443 to these custom ports (see [Port Usage](#port-usage)).
- Pass the `--webroot` option and specify the path to your webroot folder. In this case the challenge will be written in a file in `.well-known/acme-challenge/` inside your webroot.
- Pass the `--dns` option and specify a DNS provider.
- as a [CLI](https://go-acme.github.io/lego/usage/cli)
- as a [library](https://go-acme.github.io/lego/usage/library)
#### Port Usage
By default lego assumes it is able to bind to ports 80 and 443 to solve challenges.
If this is not possible in your environment, you can use the `--http` and `--tls` options to instruct
lego to listen on that interface:port for any incoming challenges.
## Documentation
If you are using this option, make sure you proxy all of the following traffic to these ports.
Documentation is hosted live at https://go-acme.github.io/lego/.
HTTP Port:
- All plaintext HTTP requests to port 80 which begin with a request path of `/.well-known/acme-challenge/` for the HTTP challenge.
## DNS providers
TLS Port:
- All TLS handshakes on port 443 for the TLS-SNI challenge.
Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
This traffic redirection is only needed as long as lego solves challenges. As soon as you have received your certificates you can deactivate the forwarding.
<!-- START DNS PROVIDERS LIST -->
#### Usage
| | | | |
|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [Akamai EdgeDNS](https://go-acme.github.io/lego/dns/edgedns/) | [Alibaba Cloud DNS](https://go-acme.github.io/lego/dns/alidns/) | [all-inkl](https://go-acme.github.io/lego/dns/allinkl/) | [Amazon Lightsail](https://go-acme.github.io/lego/dns/lightsail/) |
| [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) | [ArvanCloud](https://go-acme.github.io/lego/dns/arvancloud/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | [Autodns](https://go-acme.github.io/lego/dns/autodns/) |
| [Azure (deprecated)](https://go-acme.github.io/lego/dns/azure/) | [AzureDNS](https://go-acme.github.io/lego/dns/azuredns/) | [Bindman](https://go-acme.github.io/lego/dns/bindman/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) |
| [Brandit](https://go-acme.github.io/lego/dns/brandit/) | [Bunny](https://go-acme.github.io/lego/dns/bunny/) | [Checkdomain](https://go-acme.github.io/lego/dns/checkdomain/) | [Civo](https://go-acme.github.io/lego/dns/civo/) |
| [CloudDNS](https://go-acme.github.io/lego/dns/clouddns/) | [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) | [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) |
| [ConoHa](https://go-acme.github.io/lego/dns/conoha/) | [Constellix](https://go-acme.github.io/lego/dns/constellix/) | [Derak Cloud](https://go-acme.github.io/lego/dns/derak/) | [deSEC.io](https://go-acme.github.io/lego/dns/desec/) |
| [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) | [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) | [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) | [dnsHome.de](https://go-acme.github.io/lego/dns/dnshomede/) |
| [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | [DNSPod (deprecated)](https://go-acme.github.io/lego/dns/dnspod/) | [Domain Offensive (do.de)](https://go-acme.github.io/lego/dns/dode/) | [Domeneshop](https://go-acme.github.io/lego/dns/domeneshop/) |
| [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) | [Dyn](https://go-acme.github.io/lego/dns/dyn/) | [Dynu](https://go-acme.github.io/lego/dns/dynu/) |
| [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | [Efficient IP](https://go-acme.github.io/lego/dns/efficientip/) | [Epik](https://go-acme.github.io/lego/dns/epik/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) |
| [External program](https://go-acme.github.io/lego/dns/exec/) | [freemyip.com](https://go-acme.github.io/lego/dns/freemyip/) | [G-Core Labs](https://go-acme.github.io/lego/dns/gcore/) | [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) |
| [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) | [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) |
| [Google Domains](https://go-acme.github.io/lego/dns/googledomains/) | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | [Hosttech](https://go-acme.github.io/lego/dns/hosttech/) |
| [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [IBM Cloud (SoftLayer)](https://go-acme.github.io/lego/dns/ibmcloud/) |
| [IIJ DNS Platform Service](https://go-acme.github.io/lego/dns/iijdpf/) | [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) | [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) |
| [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Ionos](https://go-acme.github.io/lego/dns/ionos/) | [iwantmyname](https://go-acme.github.io/lego/dns/iwantmyname/) |
| [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Liara](https://go-acme.github.io/lego/dns/liara/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) |
| [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) |
| [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) |
| [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [NearlyFreeSpeech.NET](https://go-acme.github.io/lego/dns/nearlyfreespeech/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) |
| [Nicmanager](https://go-acme.github.io/lego/dns/nicmanager/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [Njalla](https://go-acme.github.io/lego/dns/njalla/) | [Nodion](https://go-acme.github.io/lego/dns/nodion/) |
| [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) |
| [plesk.com](https://go-acme.github.io/lego/dns/plesk/) | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) |
| [RcodeZero](https://go-acme.github.io/lego/dns/rcodezero/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) |
| [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) |
| [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) |
| [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/) | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/) |
| [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) |
| [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Websupport](https://go-acme.github.io/lego/dns/websupport/) |
| [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) | [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) |
| [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | | |
```
NAME:
lego - Let's Encrypt client written in Go
<!-- END DNS PROVIDERS LIST -->
USAGE:
lego [global options] command [command options] [arguments...]
VERSION:
0.4.1
COMMANDS:
run Register an account, then create and install a certificate
revoke Revoke a certificate
renew Renew a certificate
dnshelp Shows additional help for the --dns global option
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--domains value, -d value Add a domain to the process. Can be specified multiple times.
--csr value, -c value Certificate signing request filename, if an external CSR is to be used
--server value, -s value CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "https://acme-v01.api.letsencrypt.org/directory")
--email value, -m value Email used for registration and recovery contact.
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
--key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default: "rsa2048")
--path value Directory to use for storing the data (default: "/.lego")
--exclude value, -x value Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01".
--webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge
--memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.
--http value Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port
--tls value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port
--dns value Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.
--http-timeout value Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)
--dns-timeout value Set the DNS timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)
--dns-resolvers value Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use Google's DNS resolvers.
--pem Generate a .pem file by concatanating the .key and .crt files together.
--help, -h show help
--version, -v print the version
```
##### CLI Example
Assumes the `lego` binary has permission to bind to ports 80 and 443. You can get a pre-built binary from the [releases](https://github.com/xenolf/lego/releases) page.
If your environment does not allow you to bind to these ports, please read [Port Usage](#port-usage).
Obtain a certificate:
```bash
$ lego --email="foo@bar.com" --domains="example.com" run
```
(Find your certificate in the `.lego` folder of current working directory.)
To renew the certificate:
```bash
$ lego --email="foo@bar.com" --domains="example.com" renew
```
To renew the certificate only if it's older than 30 days
```bash
$ lego --email="foo@bar.com" --domains="example.com" renew --days 30
```
Obtain a certificate using the DNS challenge and AWS Route 53:
```bash
$ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" run
```
Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead.
Obtain a certificate given a certificate signing request (CSR) generated by something else:
```bash
$ lego --email="foo@bar.com" --csr=/path/to/csr.pem run
```
(lego will infer the domains to be validated based on the contents of the CSR, so make sure the CSR's Common Name and optional SubjectAltNames are set correctly.)
lego defaults to communicating with the production Let's Encrypt ACME server. If you'd like to test something without issuing real certificates, consider using the staging endpoint instead:
```bash
$ lego --server=https://acme-staging.api.letsencrypt.org/directory …
```
#### DNS Challenge API Details
##### AWS Route 53
The following AWS IAM policy document describes the permissions required for lego to complete the DNS challenge.
Replace `<INSERT_YOUR_HOSTED_ZONE_ID_HERE>` with the Route 53 zone ID of the domain you are authorizing.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:GetChange",
"route53:ListHostedZonesByName"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": [
"arn:aws:route53:::hostedzone/<INSERT_YOUR_HOSTED_ZONE_ID_HERE>"
]
}
]
}
```
#### ACME Library Usage
A valid, but bare-bones example use of the acme package:
```go
// You'll need a user or account type that implements acme.User
type MyUser struct {
Email string
Registration *acme.RegistrationResource
key crypto.PrivateKey
}
func (u MyUser) GetEmail() string {
return u.Email
}
func (u MyUser) GetRegistration() *acme.RegistrationResource {
return u.Registration
}
func (u MyUser) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// Create a user. New accounts need an email and private key to start.
const rsaKeySize = 2048
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
if err != nil {
log.Fatal(err)
}
myUser := MyUser{
Email: "you@yours.com",
key: privateKey,
}
// A client facilitates communication with the CA server. This CA URL is
// configured for a local dev instance of Boulder running in Docker in a VM.
client, err := acme.NewClient("http://192.168.99.100:4000/directory", &myUser, acme.RSA2048)
if err != nil {
log.Fatal(err)
}
// We specify an http port of 5002 and an tls port of 5001 on all interfaces
// because we aren't running as root and can't bind a listener to port 80 and 443
// (used later when we attempt to pass challenges). Keep in mind that we still
// need to proxy challenge traffic to port 5002 and 5001.
client.SetHTTPAddress(":5002")
client.SetTLSAddress(":5001")
// New users will need to register
reg, err := client.Register()
if err != nil {
log.Fatal(err)
}
myUser.Registration = reg
// SAVE THE USER.
// The client has a URL to the current Let's Encrypt Subscriber
// Agreement. The user will need to agree to it.
err = client.AgreeToTOS()
if err != nil {
log.Fatal(err)
}
// The acme library takes care of completing the challenges to obtain the certificate(s).
// The domains must resolve to this machine or you have to use the DNS challenge.
bundle := false
certificates, failures := client.ObtainCertificate([]string{"mydomain.com"}, bundle, nil, false)
if len(failures) > 0 {
log.Fatal(failures)
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL. SAVE THESE TO DISK.
fmt.Printf("%#v\n", certificates)
// ... all done.
```
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

@ -1,109 +0,0 @@
package main
import (
"crypto"
"encoding/json"
"io/ioutil"
"os"
"path"
"github.com/xenolf/lego/acme"
)
// Account represents a users local saved credentials
type Account struct {
Email string `json:"email"`
key crypto.PrivateKey
Registration *acme.RegistrationResource `json:"registration"`
conf *Configuration
}
// NewAccount creates a new account for an email address
func NewAccount(email string, conf *Configuration) *Account {
accKeysPath := conf.AccountKeysPath(email)
// TODO: move to function in configuration?
accKeyPath := accKeysPath + string(os.PathSeparator) + email + ".key"
if err := checkFolder(accKeysPath); err != nil {
logger().Fatalf("Could not check/create directory for account %s: %v", email, err)
}
var privKey crypto.PrivateKey
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
logger().Printf("No key found for account %s. Generating a curve P384 EC key.", email)
privKey, err = generatePrivateKey(accKeyPath)
if err != nil {
logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err)
}
logger().Printf("Saved key to %s", accKeyPath)
} else {
privKey, err = loadPrivateKey(accKeyPath)
if err != nil {
logger().Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err)
}
}
accountFile := path.Join(conf.AccountPath(email), "account.json")
if _, err := os.Stat(accountFile); os.IsNotExist(err) {
return &Account{Email: email, key: privKey, conf: conf}
}
fileBytes, err := ioutil.ReadFile(accountFile)
if err != nil {
logger().Fatalf("Could not load file for account %s -> %v", email, err)
}
var acc Account
err = json.Unmarshal(fileBytes, &acc)
if err != nil {
logger().Fatalf("Could not parse file for account %s -> %v", email, err)
}
acc.key = privKey
acc.conf = conf
if acc.Registration == nil {
logger().Fatalf("Could not load account for %s. Registration is nil.", email)
}
if acc.conf == nil {
logger().Fatalf("Could not load account for %s. Configuration is nil.", email)
}
return &acc
}
/** Implementation of the acme.User interface **/
// GetEmail returns the email address for the account
func (a *Account) GetEmail() string {
return a.Email
}
// GetPrivateKey returns the private RSA account key.
func (a *Account) GetPrivateKey() crypto.PrivateKey {
return a.key
}
// GetRegistration returns the server registration
func (a *Account) GetRegistration() *acme.RegistrationResource {
return a.Registration
}
/** End **/
// Save the account to disk
func (a *Account) Save() error {
jsonBytes, err := json.MarshalIndent(a, "", "\t")
if err != nil {
return err
}
return ioutil.WriteFile(
path.Join(a.conf.AccountPath(a.Email), "account.json"),
jsonBytes,
0600,
)
}

85
acme/api/account.go Normal file
View file

@ -0,0 +1,85 @@
package api
import (
"encoding/base64"
"errors"
"fmt"
"github.com/go-acme/lego/v4/acme"
)
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)
if len(location) > 0 {
a.core.jws.SetKid(location)
}
if err != nil {
return acme.ExtendedAccount{Location: location}, err
}
return acme.ExtendedAccount{Account: account, Location: location}, nil
}
// NewEAB Creates a new account with an External Account Binding.
func (a *AccountService) NewEAB(accMsg acme.Account, kid, hmacEncoded string) (acme.ExtendedAccount, error) {
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
if err != nil {
return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %w", err)
}
eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac)
if err != nil {
return acme.ExtendedAccount{}, fmt.Errorf("acme: error signing eab content: %w", err)
}
accMsg.ExternalAccountBinding = eabJWS
return a.New(accMsg)
}
// Get Retrieves an account.
func (a *AccountService) Get(accountURL string) (acme.Account, error) {
if accountURL == "" {
return acme.Account{}, errors.New("account[get]: empty URL")
}
var account acme.Account
_, err := a.core.postAsGet(accountURL, &account)
if err != nil {
return acme.Account{}, err
}
return account, nil
}
// Update Updates an account.
func (a *AccountService) Update(accountURL string, req acme.Account) (acme.Account, error) {
if accountURL == "" {
return acme.Account{}, errors.New("account[update]: empty URL")
}
var account acme.Account
_, err := a.core.post(accountURL, req, &account)
if err != nil {
return acme.Account{}, err
}
return account, nil
}
// Deactivate Deactivates an account.
func (a *AccountService) Deactivate(accountURL string) error {
if accountURL == "" {
return errors.New("account[deactivate]: empty URL")
}
req := acme.Account{Status: acme.StatusDeactivated}
_, err := a.core.post(accountURL, req, nil)
return err
}

165
acme/api/api.go Normal file
View file

@ -0,0 +1,165 @@
package api
import (
"bytes"
"crypto"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"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"
"github.com/go-acme/lego/v4/acme/api/internal/sender"
"github.com/go-acme/lego/v4/log"
)
// Core ACME/LE core API.
type Core struct {
doer *sender.Doer
nonceManager *nonces.Manager
jws *secure.JWS
directory acme.Directory
HTTPClient *http.Client
common service // Reuse a single struct instead of allocating one for each service on the heap.
Accounts *AccountService
Authorizations *AuthorizationService
Certificates *CertificateService
Challenges *ChallengeService
Orders *OrderService
}
// New Creates a new Core.
func New(httpClient *http.Client, userAgent, caDirURL, kid string, privateKey crypto.PrivateKey) (*Core, error) {
doer := sender.NewDoer(httpClient, userAgent)
dir, err := getDirectory(doer, caDirURL)
if err != nil {
return nil, err
}
nonceManager := nonces.NewManager(doer, dir.NewNonceURL)
jws := secure.NewJWS(privateKey, kid, nonceManager)
c := &Core{doer: doer, nonceManager: nonceManager, jws: jws, directory: dir, HTTPClient: httpClient}
c.common.core = c
c.Accounts = (*AccountService)(&c.common)
c.Authorizations = (*AuthorizationService)(&c.common)
c.Certificates = (*CertificateService)(&c.common)
c.Challenges = (*ChallengeService)(&c.common)
c.Orders = (*OrderService)(&c.common)
return c, nil
}
// post performs an HTTP POST request and parses the response body as JSON,
// into the provided respBody object.
func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) {
content, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.New("failed to marshal message")
}
return a.retrievablePost(uri, content, response)
}
// postAsGet performs an HTTP POST ("POST-as-GET") request.
// https://www.rfc-editor.org/rfc/rfc8555.html#section-6.3
func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) {
return a.retrievablePost(uri, []byte{}, response)
}
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
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 err
}
return backoff.Permanent(err)
}
return nil
}
notify := func(err error, duration time.Duration) {
log.Infof("retry due to: %v", err)
}
err := backoff.RetryNotify(operation, bo, notify)
if err != nil {
return resp, err
}
return resp, nil
}
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)
}
signedBody := bytes.NewBufferString(signedContent.FullSerialize())
resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response)
// nonceErr is ignored to keep the root error.
nonce, nonceErr := nonces.GetFromResponse(resp)
if nonceErr == nil {
a.nonceManager.Push(nonce)
}
return resp, err
}
func (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) {
eabJWS, err := a.jws.SignEABContent(newAccountURL, kid, hmac)
if err != nil {
return nil, err
}
return []byte(eabJWS.FullSerialize()), nil
}
// GetKeyAuthorization Gets the key authorization.
func (a *Core) GetKeyAuthorization(token string) (string, error) {
return a.jws.GetKeyAuthorization(token)
}
func (a *Core) GetDirectory() acme.Directory {
return a.directory
}
func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) {
var dir acme.Directory
if _, err := do.Get(caDirURL, &dir); err != nil {
return dir, fmt.Errorf("get directory at '%s': %w", caDirURL, err)
}
if dir.NewAccountURL == "" {
return dir, errors.New("directory missing new registration URL")
}
if dir.NewOrderURL == "" {
return dir, errors.New("directory missing new order URL")
}
return dir, nil
}

34
acme/api/authorization.go Normal file
View file

@ -0,0 +1,34 @@
package api
import (
"errors"
"github.com/go-acme/lego/v4/acme"
)
type AuthorizationService service
// Get Gets an authorization.
func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) {
if authzURL == "" {
return acme.Authorization{}, errors.New("authorization[get]: empty URL")
}
var authz acme.Authorization
_, err := c.core.postAsGet(authzURL, &authz)
if err != nil {
return acme.Authorization{}, err
}
return authz, nil
}
// Deactivate Deactivates an authorization.
func (c *AuthorizationService) Deactivate(authzURL string) error {
if authzURL == "" {
return errors.New("authorization[deactivate]: empty URL")
}
var disabledAuth acme.Authorization
_, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth)
return err
}

138
acme/api/certificate.go Normal file
View file

@ -0,0 +1,138 @@
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.
const maxBodySize = 1024 * 1024
type CertificateService service
// Get Returns the certificate and the issuer certificate.
// 'bundle' is only applied if the issuer is provided by the 'up' link.
func (c *CertificateService) Get(certURL string, bundle bool) ([]byte, []byte, error) {
cert, _, err := c.get(certURL, bundle)
if err != nil {
return nil, nil, err
}
return cert.Cert, cert.Issuer, nil
}
// GetAll the certificates and the alternate certificates.
// bundle' is only applied if the issuer is provided by the 'up' link.
func (c *CertificateService) GetAll(certURL string, bundle bool) (map[string]*acme.RawCertificate, error) {
cert, headers, err := c.get(certURL, bundle)
if err != nil {
return nil, err
}
certs := map[string]*acme.RawCertificate{certURL: cert}
// URLs of "alternate" link relation
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2
alts := getLinks(headers, "alternate")
for _, alt := range alts {
altCert, _, err := c.get(alt, bundle)
if err != nil {
return nil, err
}
certs[alt] = altCert
}
return certs, nil
}
// Revoke Revokes a certificate.
func (c *CertificateService) Revoke(req acme.RevokeCertMessage) error {
_, err := c.core.post(c.core.GetDirectory().RevokeCertURL, req, nil)
return err
}
// get Returns the certificate and the "up" link.
func (c *CertificateService) get(certURL string, bundle bool) (*acme.RawCertificate, http.Header, error) {
if certURL == "" {
return nil, nil, errors.New("certificate[get]: empty URL")
}
resp, err := c.core.postAsGet(certURL, nil)
if err != nil {
return nil, nil, err
}
data, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if err != nil {
return nil, resp.Header, err
}
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, 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}
}
// 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

@ -0,0 +1,130 @@
package api
import (
"crypto/rand"
"crypto/rsa"
"encoding/pem"
"net/http"
"testing"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const certResponseMock = `-----BEGIN CERTIFICATE-----
MIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD
Ex1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa
Fw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag
bxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5
y3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy
144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3
BJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE
zcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO
BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG
A1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD
ggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4
jXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9
IDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE
HBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd
TqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri
OPPkKtAKAbQkKbUIfsHpBZjKZMU=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw
NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl
NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT
SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh
0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen
SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx
HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt
D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu
mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD
AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm
iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd
QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ
wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv
rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2
7R4IbHGnj0BJA2vMYC4hSw==
-----END CERTIFICATE-----
`
const issuerMock = `-----BEGIN CERTIFICATE-----
MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw
NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl
NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT
SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh
0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen
SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx
HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt
D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu
mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD
AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm
iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd
QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ
wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv
rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2
7R4IbHGnj0BJA2vMYC4hSw==
-----END CERTIFICATE-----
`
func TestCertificateService_Get_issuerRelUp(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(t)
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`)
_, err := w.Write([]byte(certResponseMock))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
mux.HandleFunc("/issuer", func(w http.ResponseWriter, _ *http.Request) {
p, _ := pem.Decode([]byte(issuerMock))
_, err := w.Write(p.Bytes)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
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) {
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(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
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")
}

45
acme/api/challenge.go Normal file
View file

@ -0,0 +1,45 @@
package api
import (
"errors"
"github.com/go-acme/lego/v4/acme"
)
type ChallengeService service
// New Creates a challenge.
func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
if chlgURL == "" {
return acme.ExtendedChallenge{}, errors.New("challenge[new]: empty URL")
}
// 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
}
chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil
}
// Get Gets a challenge.
func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
if chlgURL == "" {
return acme.ExtendedChallenge{}, errors.New("challenge[get]: empty URL")
}
var chlng acme.ExtendedChallenge
resp, err := c.core.postAsGet(chlgURL, &chlng)
if err != nil {
return acme.ExtendedChallenge{}, err
}
chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil
}

View file

@ -0,0 +1,78 @@
package nonces
import (
"errors"
"fmt"
"net/http"
"sync"
"github.com/go-acme/lego/v4/acme/api/internal/sender"
)
// Manager Manages nonces.
type Manager struct {
do *sender.Doer
nonceURL string
nonces []string
sync.Mutex
}
// NewManager Creates a new Manager.
func NewManager(do *sender.Doer, nonceURL string) *Manager {
return &Manager{
do: do,
nonceURL: nonceURL,
}
}
// Pop Pops a nonce.
func (n *Manager) Pop() (string, bool) {
n.Lock()
defer n.Unlock()
if len(n.nonces) == 0 {
return "", false
}
nonce := n.nonces[len(n.nonces)-1]
n.nonces = n.nonces[:len(n.nonces)-1]
return nonce, true
}
// Push Pushes a nonce.
func (n *Manager) Push(nonce string) {
n.Lock()
defer n.Unlock()
n.nonces = append(n.nonces, nonce)
}
// Nonce implement jose.NonceSource.
func (n *Manager) Nonce() (string, error) {
if nonce, ok := n.Pop(); ok {
return nonce, nil
}
return n.getNonce()
}
func (n *Manager) getNonce() (string, error) {
resp, err := n.do.Head(n.nonceURL)
if err != nil {
return "", fmt.Errorf("failed to get nonce from HTTP HEAD: %w", err)
}
return GetFromResponse(resp)
}
// GetFromResponse Extracts a nonce from a HTTP response.
func GetFromResponse(resp *http.Response) (string, error) {
if resp == nil {
return "", errors.New("nil response")
}
nonce := resp.Header.Get("Replay-Nonce")
if nonce == "" {
return "", errors.New("server did not respond with a proper nonce header")
}
return nonce, nil
}

View file

@ -0,0 +1,55 @@
package nonces
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"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"
)
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(250 * time.Millisecond)
w.Header().Set("Replay-Nonce", "12345")
w.Header().Set("Retry-After", "0")
err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}))
t.Cleanup(server.Close)
doer := sender.NewDoer(http.DefaultClient, "lego-test")
j := NewManager(doer, server.URL)
ch := make(chan bool)
resultCh := make(chan bool)
go func() {
_, errN := j.Nonce()
if errN != nil {
t.Log(errN)
}
ch <- true
}()
go func() {
_, 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):
t.Fatal("JWS is probably holding a lock while making HTTP request")
}
}

View file

@ -0,0 +1,130 @@
package secure
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"fmt"
"github.com/go-acme/lego/v4/acme/api/internal/nonces"
jose "github.com/go-jose/go-jose/v3"
)
// JWS Represents a JWS.
type JWS struct {
privKey crypto.PrivateKey
kid string // Key identifier
nonces *nonces.Manager
}
// NewJWS Create a new JWS.
func NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager *nonces.Manager) *JWS {
return &JWS{
privKey: privateKey,
nonces: nonceManager,
kid: kid,
}
}
// SetKid Sets a key identifier.
func (j *JWS) SetKid(kid string) {
j.kid = kid
}
// 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
case *ecdsa.PrivateKey:
if k.Curve == elliptic.P256() {
alg = jose.ES256
} else if k.Curve == elliptic.P384() {
alg = jose.ES384
}
}
signKey := jose.SigningKey{
Algorithm: alg,
Key: jose.JSONWebKey{Key: j.privKey, KeyID: j.kid},
}
options := jose.SignerOptions{
NonceSource: j.nonces,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
}
if j.kid == "" {
options.EmbedJWK = true
}
signer, err := jose.NewSigner(signKey, &options)
if err != nil {
return nil, fmt.Errorf("failed to create jose signer: %w", err)
}
signed, err := signer.Sign(content)
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)
}
signer, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
&jose.SignerOptions{
EmbedJWK: false,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"kid": kid,
"url": url,
},
},
)
if err != nil {
return nil, fmt.Errorf("failed to create External Account Binding jose signer: %w", err)
}
signed, err := signer.Sign(jwkJSON)
if err != nil {
return nil, fmt.Errorf("failed to External Account Binding sign content: %w", err)
}
return signed, nil
}
// 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()
case *rsa.PrivateKey:
publicKey = k.Public()
}
// Generate the Key Authorization for the challenge
jwk := &jose.JSONWebKey{Key: publicKey}
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
if err != nil {
return "", err
}
// unpad the base64URL
keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes)
return token + "." + keyThumb, nil
}

View file

@ -0,0 +1,56 @@
package secure
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"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"
)
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(250 * time.Millisecond)
w.Header().Set("Replay-Nonce", "12345")
w.Header().Set("Retry-After", "0")
err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}))
t.Cleanup(server.Close)
doer := sender.NewDoer(http.DefaultClient, "lego-test")
j := nonces.NewManager(doer, server.URL)
ch := make(chan bool)
resultCh := make(chan bool)
go func() {
_, errN := j.Nonce()
if errN != nil {
t.Log(errN)
}
ch <- true
}()
go func() {
_, 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):
t.Fatal("JWS is probably holding a lock while making HTTP request")
}
}

View file

@ -0,0 +1,148 @@
package sender
import (
"encoding/json"
"fmt"
"io"
"net/http"
"runtime"
"strings"
"github.com/go-acme/lego/v4/acme"
)
type RequestOption func(*http.Request) error
func contentType(ct string) RequestOption {
return func(req *http.Request) error {
req.Header.Set("Content-Type", ct)
return nil
}
}
type Doer struct {
httpClient *http.Client
userAgent string
}
// NewDoer Creates a new Doer.
func NewDoer(client *http.Client, userAgent string) *Doer {
return &Doer{
httpClient: client,
userAgent: userAgent,
}
}
// Get performs a GET request with a proper User-Agent string.
// If "response" is not provided, callers should close resp.Body when done reading from it.
func (d *Doer) Get(url string, response interface{}) (*http.Response, error) {
req, err := d.newRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return d.do(req, response)
}
// Head performs a HEAD request with a proper User-Agent string.
// The response body (resp.Body) is already closed when this function returns.
func (d *Doer) Head(url string) (*http.Response, error) {
req, err := d.newRequest(http.MethodHead, url, nil)
if err != nil {
return nil, err
}
return d.do(req, nil)
}
// Post performs a POST request with a proper User-Agent string.
// If "response" is not provided, callers should close resp.Body when done reading from it.
func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) {
req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType))
if err != nil {
return nil, err
}
return d.do(req, response)
}
func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
req, err := http.NewRequest(method, uri, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", d.formatUserAgent())
for _, opt := range opts {
err = opt(req)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
}
return req, nil
}
func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) {
resp, err := d.httpClient.Do(req)
if err != nil {
return nil, err
}
if err = checkError(req, resp); err != nil {
return resp, err
}
if response != nil {
raw, err := io.ReadAll(resp.Body)
if err != nil {
return resp, err
}
defer resp.Body.Close()
err = json.Unmarshal(raw, response)
if err != nil {
return resp, fmt.Errorf("failed to unmarshal %q to type %T: %w", raw, response, err)
}
}
return resp, nil
}
// formatUserAgent builds and returns the User-Agent string to use in requests.
func (d *Doer) formatUserAgent() string {
ua := fmt.Sprintf("%s %s (%s; %s; %s)", d.userAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)
return strings.TrimSpace(ua)
}
func checkError(req *http.Request, resp *http.Response) error {
if resp.StatusCode >= http.StatusBadRequest {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err)
}
var errorDetails *acme.ProblemDetails
err = json.Unmarshal(body, &errorDetails)
if err != nil {
return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body))
}
errorDetails.Method = req.Method
errorDetails.URL = req.URL.String()
if errorDetails.HTTPStatus == 0 {
errorDetails.HTTPStatus = resp.StatusCode
}
// Check for errors we handle specifically
if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr {
return &acme.NonceError{ProblemDetails: errorDetails}
}
return errorDetails
}
return nil
}

View file

@ -0,0 +1,67 @@
package sender
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDo_UserAgentOnAllHTTPMethod(t *testing.T) {
var ua, method string
server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
ua = r.Header.Get("User-Agent")
method = r.Method
}))
t.Cleanup(server.Close)
doer := NewDoer(http.DefaultClient, "")
testCases := []struct {
method string
call func(u string) (*http.Response, error)
}{
{
method: http.MethodGet,
call: func(u string) (*http.Response, error) {
return doer.Get(u, nil)
},
},
{
method: http.MethodHead,
call: doer.Head,
},
{
method: http.MethodPost,
call: func(u string) (*http.Response, error) {
return doer.Post(u, strings.NewReader("falalalala"), "text/plain", nil)
},
},
}
for _, test := range testCases {
t.Run(test.method, func(t *testing.T) {
_, err := test.call(server.URL)
require.NoError(t, err)
assert.Equal(t, test.method, method)
assert.Contains(t, ua, ourUserAgent, "User-Agent")
})
}
}
func TestDo_CustomUserAgent(t *testing.T) {
customUA := "MyApp/1.2.3"
doer := NewDoer(http.DefaultClient, customUA)
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)
}

View file

@ -0,0 +1,14 @@
package sender
// CODE GENERATED AUTOMATICALLY
// THIS FILE MUST NOT BE EDITED BY HAND
const (
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme/4.12.3"
// 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"
)

94
acme/api/order.go Normal file
View file

@ -0,0 +1,94 @@
package api
import (
"encoding/base64"
"errors"
"net"
"time"
"github.com/go-acme/lego/v4/acme"
)
// OrderOptions used to create an order (optional).
type OrderOptions struct {
NotBefore time.Time
NotAfter time.Time
}
type OrderService service
// New Creates a new order.
func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
return o.NewWithOptions(domains, nil)
}
// NewWithOptions Creates a new order.
func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acme.ExtendedOrder, error) {
var identifiers []acme.Identifier
for _, domain := range domains {
ident := acme.Identifier{Value: domain, Type: "dns"}
if net.ParseIP(domain) != nil {
ident.Type = "ip"
}
identifiers = append(identifiers, ident)
}
orderReq := acme.Order{Identifiers: identifiers}
if opts != nil {
if !opts.NotAfter.IsZero() {
orderReq.NotAfter = opts.NotAfter.Format(time.RFC3339)
}
if !opts.NotBefore.IsZero() {
orderReq.NotBefore = opts.NotBefore.Format(time.RFC3339)
}
}
var order acme.Order
resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
if err != nil {
return acme.ExtendedOrder{}, err
}
return acme.ExtendedOrder{
Order: order,
Location: resp.Header.Get("Location"),
}, nil
}
// Get Gets an order.
func (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) {
if orderURL == "" {
return acme.ExtendedOrder{}, errors.New("order[get]: empty URL")
}
var order acme.Order
_, err := o.core.postAsGet(orderURL, &order)
if err != nil {
return acme.ExtendedOrder{}, err
}
return acme.ExtendedOrder{Order: order}, nil
}
// UpdateForCSR Updates an order for a CSR.
func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.ExtendedOrder, error) {
csrMsg := acme.CSRMessage{
Csr: base64.RawURLEncoding.EncodeToString(csr),
}
var order acme.Order
_, err := o.core.post(orderURL, csrMsg, &order)
if err != nil {
return acme.ExtendedOrder{}, err
}
if order.Status == acme.StatusInvalid {
return acme.ExtendedOrder{}, order.Error
}
return acme.ExtendedOrder{Order: order}, nil
}

129
acme/api/order_test.go Normal file
View file

@ -0,0 +1,129 @@
package api
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/go-jose/go-jose/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOrderService_NewWithOptions(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(t)
// small value keeps test fast
privateKey, errK := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, errK, "Could not generate test key")
mux.HandleFunc("/newOrder", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
body, err := readSignedBody(r, privateKey)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
order := acme.Order{}
err = json.Unmarshal(body, &order)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = tester.WriteJSONResponse(w, acme.Order{
Status: acme.StatusValid,
Expires: order.Expires,
Identifiers: order.Identifiers,
NotBefore: order.NotBefore,
NotAfter: order.NotAfter,
Error: order.Error,
Authorizations: order.Authorizations,
Finalize: order.Finalize,
Certificate: order.Certificate,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
testCases := []struct {
desc string
opts *OrderOptions
expected acme.ExtendedOrder
}{
{
desc: "simple",
expected: acme.ExtendedOrder{
Order: acme.Order{
Status: "valid",
Identifiers: []acme.Identifier{{Type: "dns", Value: "example.com"}},
},
},
},
{
desc: "with options",
opts: &OrderOptions{
NotBefore: time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC),
NotAfter: time.Date(2023, 1, 2, 1, 0, 0, 0, time.UTC),
},
expected: acme.ExtendedOrder{
Order: acme.Order{
Status: "valid",
Identifiers: []acme.Identifier{{Type: "dns", Value: "example.com"}},
NotBefore: "2023-01-01T01:00:00Z",
NotAfter: "2023-01-02T01:00:00Z",
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
order, err := core.Orders.NewWithOptions([]string{"example.com"}, test.opts)
require.NoError(t, err)
assert.Equal(t, test.expected, order)
})
}
}
func readSignedBody(r *http.Request, privateKey *rsa.PrivateKey) ([]byte, error) {
reqBody, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
jws, err := jose.ParseSigned(string(reqBody))
if err != nil {
return nil, err
}
body, err := jws.Verify(&jose.JSONWebKey{
Key: privateKey.Public(),
Algorithm: "RSA",
})
if err != nil {
return nil, err
}
return body, nil
}

53
acme/api/renewal.go Normal file
View file

@ -0,0 +1,53 @@
package api
import (
"errors"
"net/http"
"github.com/go-acme/lego/v4/acme"
)
// ErrNoARI is returned when the server does not advertise a renewal info endpoint.
var ErrNoARI = errors.New("renewalInfo[get/post]: server does not advertise a renewal info endpoint")
// GetRenewalInfo GETs renewal information for a certificate from the renewalInfo endpoint.
// This is used to determine if a certificate needs to be renewed.
//
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
//
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
func (c *CertificateService) GetRenewalInfo(certID string) (*http.Response, error) {
if c.core.GetDirectory().RenewalInfo == "" {
return nil, ErrNoARI
}
if certID == "" {
return nil, errors.New("renewalInfo[get]: 'certID' cannot be empty")
}
return c.core.HTTPClient.Get(c.core.GetDirectory().RenewalInfo + "/" + certID)
}
// UpdateRenewalInfo POSTs updated renewal information for a certificate to the renewalInfo endpoint.
// This is used to indicate that a certificate has been replaced.
//
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
//
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
func (c *CertificateService) UpdateRenewalInfo(req acme.RenewalInfoUpdateRequest) (*http.Response, error) {
if c.core.GetDirectory().RenewalInfo == "" {
return nil, ErrNoARI
}
if req.CertID == "" {
return nil, errors.New("renewalInfo[post]: 'certID' cannot be empty")
}
if !req.Replaced {
return nil, errors.New("renewalInfo[post]: 'replaced' cannot be false")
}
return c.core.post(c.core.GetDirectory().RenewalInfo, req, nil)
}

56
acme/api/service.go Normal file
View file

@ -0,0 +1,56 @@
package api
import (
"net/http"
"regexp"
)
type service struct {
core *Core
}
// getLink get a rel into the Link header.
func getLink(header http.Header, rel string) string {
links := getLinks(header, rel)
if len(links) < 1 {
return ""
}
return links[0]
}
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])
}
}
}
return links
}
// getLocation get the value of the header Location.
func getLocation(resp *http.Response) string {
if resp == nil {
return ""
}
return resp.Header.Get("Location")
}
// getRetryAfter get the value of the header Retry-After.
func getRetryAfter(resp *http.Response) string {
if resp == nil {
return ""
}
return resp.Header.Get("Retry-After")
}

56
acme/api/service_test.go Normal file
View file

@ -0,0 +1,56 @@
package api
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_getLink(t *testing.T) {
testCases := []struct {
desc string
header http.Header
relName string
expected string
}{
{
desc: "success",
header: http.Header{
"Link": []string{`<https://acme-staging-v02.api.letsencrypt.org/next>; rel="next", <https://acme-staging-v02.api.letsencrypt.org/up?query>; rel="up"`},
},
relName: "up",
expected: "https://acme-staging-v02.api.letsencrypt.org/up?query",
},
{
desc: "success several lines",
header: http.Header{
"Link": []string{`<https://acme-staging-v02.api.letsencrypt.org/next>; rel="next"`, `<https://acme-staging-v02.api.letsencrypt.org/up?query>; rel="up"`},
},
relName: "up",
expected: "https://acme-staging-v02.api.letsencrypt.org/up?query",
},
{
desc: "no link",
header: http.Header{},
relName: "up",
expected: "",
},
{
desc: "no header",
relName: "up",
expected: "",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
link := getLink(test.header, test.relName)
assert.Equal(t, test.expected, link)
})
}
}

View file

@ -1,16 +0,0 @@
package acme
// Challenge is a string that identifies a particular type and version of ACME challenge.
type Challenge string
const (
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
HTTP01 = Challenge("http-01")
// TLSSNI01 is the "tls-sni-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#tls-with-server-name-indication-tls-sni
// Note: TLSSNI01ChallengeCert returns a certificate to fulfill this challenge
TLSSNI01 = Challenge("tls-sni-01")
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
// Note: DNS01Record returns a DNS record which will fulfill this challenge
DNS01 = Challenge("dns-01")
)

View file

@ -1,825 +0,0 @@
// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers.
package acme
import (
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"time"
)
var (
// Logger is an optional custom logger.
Logger *log.Logger
)
const (
// maxBodySize is the maximum size of body that we will read.
maxBodySize = 1024 * 1024
// overallRequestLimit is the overall number of request per second limited on the
// “new-reg”, “new-authz” and “new-cert” endpoints. From the documentation the
// limitation is 20 requests per second, but using 20 as value doesn't work but 18 do
overallRequestLimit = 18
)
// logf writes a log entry. It uses Logger if not
// nil, otherwise it uses the default log.Logger.
func logf(format string, args ...interface{}) {
if Logger != nil {
Logger.Printf(format, args...)
} else {
log.Printf(format, args...)
}
}
// User interface is to be implemented by users of this library.
// It is used by the client type to get user specific information.
type User interface {
GetEmail() string
GetRegistration() *RegistrationResource
GetPrivateKey() crypto.PrivateKey
}
// Interface for all challenge solvers to implement.
type solver interface {
Solve(challenge challenge, domain string) error
}
type validateFunc func(j *jws, domain, uri string, chlng challenge) error
// Client is the user-friendy way to ACME
type Client struct {
directory directory
user User
jws *jws
keyType KeyType
solvers map[Challenge]solver
}
// NewClient creates a new ACME client on behalf of the user. The client will depend on
// the ACME directory located at caDirURL for the rest of its actions. A private
// key of type keyType (see KeyType contants) will be generated when requesting a new
// certificate if one isn't provided.
func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) {
privKey := user.GetPrivateKey()
if privKey == nil {
return nil, errors.New("private key was nil")
}
var dir directory
if _, err := getJSON(caDirURL, &dir); err != nil {
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
}
if dir.NewRegURL == "" {
return nil, errors.New("directory missing new registration URL")
}
if dir.NewAuthzURL == "" {
return nil, errors.New("directory missing new authz URL")
}
if dir.NewCertURL == "" {
return nil, errors.New("directory missing new certificate URL")
}
if dir.RevokeCertURL == "" {
return nil, errors.New("directory missing revoke certificate URL")
}
jws := &jws{privKey: privKey, directoryURL: caDirURL}
// REVIEW: best possibility?
// Add all available solvers with the right index as per ACME
// spec to this map. Otherwise they won`t be found.
solvers := make(map[Challenge]solver)
solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}}
return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil
}
// SetChallengeProvider specifies a custom provider p that can solve the given challenge type.
func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) error {
switch challenge {
case HTTP01:
c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
case TLSSNI01:
c.solvers[challenge] = &tlsSNIChallenge{jws: c.jws, validate: validate, provider: p}
case DNS01:
c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p}
default:
return fmt.Errorf("Unknown challenge %v", challenge)
}
return nil
}
// SetHTTPAddress specifies a custom interface:port to be used for HTTP based challenges.
// If this option is not used, the default port 80 and all interfaces will be used.
// To only specify a port and no interface use the ":port" notation.
//
// NOTE: This REPLACES any custom HTTP provider previously set by calling
// c.SetChallengeProvider with the default HTTP challenge provider.
func (c *Client) SetHTTPAddress(iface string) error {
host, port, err := net.SplitHostPort(iface)
if err != nil {
return err
}
if chlng, ok := c.solvers[HTTP01]; ok {
chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port)
}
return nil
}
// SetTLSAddress specifies a custom interface:port to be used for TLS based challenges.
// If this option is not used, the default port 443 and all interfaces will be used.
// To only specify a port and no interface use the ":port" notation.
//
// NOTE: This REPLACES any custom TLS-SNI provider previously set by calling
// c.SetChallengeProvider with the default TLS-SNI challenge provider.
func (c *Client) SetTLSAddress(iface string) error {
host, port, err := net.SplitHostPort(iface)
if err != nil {
return err
}
if chlng, ok := c.solvers[TLSSNI01]; ok {
chlng.(*tlsSNIChallenge).provider = NewTLSProviderServer(host, port)
}
return nil
}
// ExcludeChallenges explicitly removes challenges from the pool for solving.
func (c *Client) ExcludeChallenges(challenges []Challenge) {
// Loop through all challenges and delete the requested one if found.
for _, challenge := range challenges {
delete(c.solvers, challenge)
}
}
// Register the current account to the ACME server.
func (c *Client) Register() (*RegistrationResource, error) {
if c == nil || c.user == nil {
return nil, errors.New("acme: cannot register a nil client or user")
}
logf("[INFO] acme: Registering account for %s", c.user.GetEmail())
regMsg := registrationMessage{
Resource: "new-reg",
}
if c.user.GetEmail() != "" {
regMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
} else {
regMsg.Contact = []string{}
}
var serverReg Registration
var regURI string
hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg)
if err != nil {
remoteErr, ok := err.(RemoteError)
if ok && remoteErr.StatusCode == 409 {
regURI = hdr.Get("Location")
regMsg = registrationMessage{
Resource: "reg",
}
if hdr, err = postJSON(c.jws, regURI, regMsg, &serverReg); err != nil {
return nil, err
}
} else {
return nil, err
}
}
reg := &RegistrationResource{Body: serverReg}
links := parseLinks(hdr["Link"])
if regURI == "" {
regURI = hdr.Get("Location")
}
reg.URI = regURI
if links["terms-of-service"] != "" {
reg.TosURL = links["terms-of-service"]
}
if links["next"] != "" {
reg.NewAuthzURL = links["next"]
} else {
return nil, errors.New("acme: The server did not return 'next' link to proceed")
}
return reg, nil
}
// DeleteRegistration deletes the client's user registration from the ACME
// server.
func (c *Client) DeleteRegistration() error {
if c == nil || c.user == nil {
return errors.New("acme: cannot unregister a nil client or user")
}
logf("[INFO] acme: Deleting account for %s", c.user.GetEmail())
regMsg := registrationMessage{
Resource: "reg",
Delete: true,
}
_, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, nil)
if err != nil {
return err
}
return nil
}
// QueryRegistration runs a POST request on the client's registration and
// returns the result.
//
// This is similar to the Register function, but acting on an existing
// registration link and resource.
func (c *Client) QueryRegistration() (*RegistrationResource, error) {
if c == nil || c.user == nil {
return nil, errors.New("acme: cannot query the registration of a nil client or user")
}
// Log the URL here instead of the email as the email may not be set
logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI)
regMsg := registrationMessage{
Resource: "reg",
}
var serverReg Registration
hdr, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, &serverReg)
if err != nil {
return nil, err
}
reg := &RegistrationResource{Body: serverReg}
links := parseLinks(hdr["Link"])
// Location: header is not returned so this needs to be populated off of
// existing URI
reg.URI = c.user.GetRegistration().URI
if links["terms-of-service"] != "" {
reg.TosURL = links["terms-of-service"]
}
if links["next"] != "" {
reg.NewAuthzURL = links["next"]
} else {
return nil, errors.New("acme: No new-authz link in response to registration query")
}
return reg, nil
}
// AgreeToTOS updates the Client registration and sends the agreement to
// the server.
func (c *Client) AgreeToTOS() error {
reg := c.user.GetRegistration()
reg.Body.Agreement = c.user.GetRegistration().TosURL
reg.Body.Resource = "reg"
_, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil)
return err
}
// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it.
// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key
// for this CSR is not required.
// If bundle is true, the []byte contains both the issuer certificate and
// your issued certificate as a bundle.
// This function will never return a partial certificate. If one domain in the list fails,
// the whole certificate will fail.
func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) {
// figure out what domains it concerns
// start with the common name
domains := []string{csr.Subject.CommonName}
// loop over the SubjectAltName DNS names
DNSNames:
for _, sanName := range csr.DNSNames {
for _, existingName := range domains {
if existingName == sanName {
// duplicate; skip this name
continue DNSNames
}
}
// name is unique
domains = append(domains, sanName)
}
if bundle {
logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
} else {
logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
}
challenges, failures := c.getChallenges(domains)
// If any challenge fails - return. Do not generate partial SAN certificates.
if len(failures) > 0 {
for _, auth := range challenges {
c.disableAuthz(auth)
}
return CertificateResource{}, failures
}
errs := c.solveChallenges(challenges)
// If any challenge fails - return. Do not generate partial SAN certificates.
if len(errs) > 0 {
return CertificateResource{}, errs
}
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
cert, err := c.requestCertificateForCsr(challenges, bundle, csr.Raw, nil)
if err != nil {
for _, chln := range challenges {
failures[chln.Domain] = err
}
}
// Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = pemEncode(&csr)
return cert, failures
}
// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
// The first domain in domains is used for the CommonName field of the certificate, all other
// domains are added using the Subject Alternate Names extension. A new private key is generated
// for every invocation of this function. If you do not want that you can supply your own private key
// in the privKey parameter. If this parameter is non-nil it will be used instead of generating a new one.
// If bundle is true, the []byte contains both the issuer certificate and
// your issued certificate as a bundle.
// This function will never return a partial certificate. If one domain in the list fails,
// the whole certificate will fail.
func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, map[string]error) {
if bundle {
logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
} else {
logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
}
challenges, failures := c.getChallenges(domains)
// If any challenge fails - return. Do not generate partial SAN certificates.
if len(failures) > 0 {
for _, auth := range challenges {
c.disableAuthz(auth)
}
return CertificateResource{}, failures
}
errs := c.solveChallenges(challenges)
// If any challenge fails - return. Do not generate partial SAN certificates.
if len(errs) > 0 {
return CertificateResource{}, errs
}
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
cert, err := c.requestCertificate(challenges, bundle, privKey, mustStaple)
if err != nil {
for _, chln := range challenges {
failures[chln.Domain] = err
}
}
return cert, failures
}
// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
func (c *Client) RevokeCertificate(certificate []byte) error {
certificates, err := parsePEMBundle(certificate)
if err != nil {
return err
}
x509Cert := certificates[0]
if x509Cert.IsCA {
return fmt.Errorf("Certificate bundle starts with a CA certificate")
}
encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw)
_, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert}, nil)
return err
}
// RenewCertificate takes a CertificateResource and tries to renew the certificate.
// If the renewal process succeeds, the new certificate will ge returned in a new CertResource.
// Please be aware that this function will return a new certificate in ANY case that is not an error.
// If the server does not provide us with a new cert on a GET request to the CertURL
// this function will start a new-cert flow where a new certificate gets generated.
// 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 CertificateResource should be non-nil.
func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (CertificateResource, error) {
// Input certificate is PEM encoded. Decode it here as we may need the decoded
// cert later on in the renewal process. The input may be a bundle or a single certificate.
certificates, err := parsePEMBundle(cert.Certificate)
if err != nil {
return CertificateResource{}, err
}
x509Cert := certificates[0]
if x509Cert.IsCA {
return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain)
}
// This is just meant to be informal for the user.
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours()))
// We always need to request a new certificate to renew.
// Start by checking to see if the certificate was based off a CSR, and
// use that if it's defined.
if len(cert.CSR) > 0 {
csr, err := pemDecodeTox509CSR(cert.CSR)
if err != nil {
return CertificateResource{}, err
}
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
return newCert, failures[cert.Domain]
}
var privKey crypto.PrivateKey
if cert.PrivateKey != nil {
privKey, err = parsePEMPrivateKey(cert.PrivateKey)
if err != nil {
return CertificateResource{}, err
}
}
var domains []string
var failures map[string]error
// check for SAN certificate
if len(x509Cert.DNSNames) > 1 {
domains = append(domains, x509Cert.Subject.CommonName)
for _, sanDomain := range x509Cert.DNSNames {
if sanDomain == x509Cert.Subject.CommonName {
continue
}
domains = append(domains, sanDomain)
}
} else {
domains = append(domains, x509Cert.Subject.CommonName)
}
newCert, failures := c.ObtainCertificate(domains, bundle, privKey, mustStaple)
return newCert, failures[cert.Domain]
}
// Looks through the challenge combinations to find a solvable match.
// Then solves the challenges in series and returns.
func (c *Client) solveChallenges(challenges []authorizationResource) map[string]error {
// loop through the resources, basically through the domains.
failures := make(map[string]error)
for _, authz := range challenges {
if authz.Body.Status == "valid" {
// Boulder might recycle recent validated authz (see issue #267)
logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Domain)
continue
}
// no solvers - no solving
if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil {
for i, solver := range solvers {
// TODO: do not immediately fail if one domain fails to validate.
err := solver.Solve(authz.Body.Challenges[i], authz.Domain)
if err != nil {
c.disableAuthz(authz)
failures[authz.Domain] = err
}
}
} else {
c.disableAuthz(authz)
failures[authz.Domain] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Domain)
}
}
return failures
}
// Checks all combinations from the server and returns an array of
// solvers which should get executed in series.
func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver {
for _, combination := range auth.Combinations {
solvers := make(map[int]solver)
for _, idx := range combination {
if solver, ok := c.solvers[auth.Challenges[idx].Type]; ok {
solvers[idx] = solver
} else {
logf("[INFO][%s] acme: Could not find solver for: %s", domain, auth.Challenges[idx].Type)
}
}
// If we can solve the whole combination, return the solvers
if len(solvers) == len(combination) {
return solvers
}
}
return nil
}
// Get the challenges needed to proof our identifier to the ACME server.
func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) {
resc, errc := make(chan authorizationResource), make(chan domainError)
delay := time.Second / overallRequestLimit
for _, domain := range domains {
time.Sleep(delay)
go func(domain string) {
authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}}
var authz authorization
hdr, err := postJSON(c.jws, c.user.GetRegistration().NewAuthzURL, authMsg, &authz)
if err != nil {
errc <- domainError{Domain: domain, Error: err}
return
}
links := parseLinks(hdr["Link"])
if links["next"] == "" {
logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain)
errc <- domainError{Domain: domain, Error: errors.New("Server did not provide next link to proceed")}
return
}
resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: hdr.Get("Location"), Domain: domain}
}(domain)
}
responses := make(map[string]authorizationResource)
failures := make(map[string]error)
for i := 0; i < len(domains); i++ {
select {
case res := <-resc:
responses[res.Domain] = res
case err := <-errc:
failures[err.Domain] = err.Error
}
}
challenges := make([]authorizationResource, 0, len(responses))
for _, domain := range domains {
if challenge, ok := responses[domain]; ok {
challenges = append(challenges, challenge)
}
}
logAuthz(challenges)
close(resc)
close(errc)
return challenges, failures
}
func logAuthz(authz []authorizationResource) {
for _, auth := range authz {
logf("[INFO][%s] AuthURL: %s", auth.Domain, auth.AuthURL)
}
}
// cleanAuthz loops through the passed in slice and disables any auths which are not "valid"
func (c *Client) disableAuthz(auth authorizationResource) error {
var disabledAuth authorization
_, err := postJSON(c.jws, auth.AuthURL, deactivateAuthMessage{Resource: "authz", Status: "deactivated"}, &disabledAuth)
return err
}
func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, error) {
if len(authz) == 0 {
return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
}
var err error
if privKey == nil {
privKey, err = generatePrivateKey(c.keyType)
if err != nil {
return CertificateResource{}, err
}
}
// determine certificate name(s) based on the authorization resources
commonName := authz[0]
var san []string
for _, auth := range authz[1:] {
san = append(san, auth.Domain)
}
// TODO: should the CSR be customizable?
csr, err := generateCsr(privKey, commonName.Domain, san, mustStaple)
if err != nil {
return CertificateResource{}, err
}
return c.requestCertificateForCsr(authz, bundle, csr, pemEncode(privKey))
}
func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) {
commonName := authz[0]
var authURLs []string
for _, auth := range authz[1:] {
authURLs = append(authURLs, auth.AuthURL)
}
csrString := base64.URLEncoding.EncodeToString(csr)
jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs})
if err != nil {
return CertificateResource{}, err
}
resp, err := c.jws.post(commonName.NewCertURL, jsonBytes)
if err != nil {
return CertificateResource{}, err
}
certRes := CertificateResource{
Domain: commonName.Domain,
CertURL: resp.Header.Get("Location"),
PrivateKey: privateKeyPem,
}
maxChecks := 1000
for i := 0; i < maxChecks; i++ {
done, err := c.checkCertResponse(resp, &certRes, bundle)
resp.Body.Close()
if err != nil {
return CertificateResource{}, err
}
if done {
break
}
if i == maxChecks-1 {
return CertificateResource{}, fmt.Errorf("polled for certificate %d times; giving up", i)
}
resp, err = httpGet(certRes.CertURL)
if err != nil {
return CertificateResource{}, err
}
}
return certRes, nil
}
// checkCertResponse checks resp to see if a certificate is contained in the
// response, and if so, loads it into certRes and returns true. If the cert
// is not yet ready, it returns false. This function honors the waiting period
// required by the Retry-After header of the response, if specified. This
// function may read from resp.Body but does NOT close it. The certRes input
// should already have the Domain (common name) field populated. If bundle is
// true, the certificate will be bundled with the issuer's cert.
func (c *Client) checkCertResponse(resp *http.Response, certRes *CertificateResource, bundle bool) (bool, error) {
switch resp.StatusCode {
case 201, 202:
cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return false, err
}
// The server returns a body with a length of zero if the
// certificate was not ready at the time this request completed.
// Otherwise the body is the certificate.
if len(cert) > 0 {
certRes.CertStableURL = resp.Header.Get("Content-Location")
certRes.AccountRef = c.user.GetRegistration().URI
issuedCert := pemEncode(derCertificateBytes(cert))
// The issuer certificate link is always supplied via an "up" link
// in the response headers of a new certificate.
links := parseLinks(resp.Header["Link"])
issuerCert, err := c.getIssuerCertificate(links["up"])
if err != nil {
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err)
} else {
issuerCert = pemEncode(derCertificateBytes(issuerCert))
// 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 {
issuedCert = append(issuedCert, issuerCert...)
}
}
certRes.Certificate = issuedCert
certRes.IssuerCertificate = issuerCert
logf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
return true, nil
}
// The certificate was granted but is not yet issued.
// Check retry-after and loop.
ra := resp.Header.Get("Retry-After")
retryAfter, err := strconv.Atoi(ra)
if err != nil {
return false, err
}
logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", certRes.Domain, retryAfter)
time.Sleep(time.Duration(retryAfter) * time.Second)
return false, nil
default:
return false, handleHTTPError(resp)
}
}
// getIssuerCertificate requests the issuer certificate
func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
logf("[INFO] acme: Requesting issuer cert from %s", url)
resp, err := httpGet(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return nil, err
}
_, err = x509.ParseCertificate(issuerBytes)
if err != nil {
return nil, err
}
return issuerBytes, err
}
func parseLinks(links []string) map[string]string {
aBrkt := regexp.MustCompile("[<>]")
slver := regexp.MustCompile("(.+) *= *\"(.+)\"")
linkMap := make(map[string]string)
for _, link := range links {
link = aBrkt.ReplaceAllString(link, "")
parts := strings.Split(link, ";")
matches := slver.FindStringSubmatch(parts[1])
if len(matches) > 0 {
linkMap[matches[2]] = parts[0]
}
}
return linkMap
}
// validate makes the ACME server start validating a
// challenge response, only returning once it is done.
func validate(j *jws, domain, uri string, chlng challenge) error {
var challengeResponse challenge
hdr, err := postJSON(j, uri, chlng, &challengeResponse)
if err != nil {
return err
}
// After the path is sent, the ACME server will access our server.
// Repeatedly check the server for an updated status on our request.
for {
switch challengeResponse.Status {
case "valid":
logf("[INFO][%s] The server validated our request", domain)
return nil
case "pending":
break
case "invalid":
return handleChallengeError(challengeResponse)
default:
return errors.New("The server returned an unexpected state.")
}
ra, err := strconv.Atoi(hdr.Get("Retry-After"))
if err != nil {
// The ACME server MUST return a Retry-After.
// If it doesn't, we'll just poll hard.
ra = 1
}
time.Sleep(time.Duration(ra) * time.Second)
hdr, err = getJSON(uri, &challengeResponse)
if err != nil {
return err
}
}
}

View file

@ -1,269 +0,0 @@
package acme
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestNewClient(t *testing.T) {
keyBits := 32 // small value keeps test fast
keyType := RSA2048
key, err := rsa.GenerateKey(rand.Reader, keyBits)
if err != nil {
t.Fatal("Could not generate test key:", err)
}
user := mockUser{
email: "test@test.com",
regres: new(RegistrationResource),
privatekey: key,
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
w.Write(data)
}))
client, err := NewClient(ts.URL, user, keyType)
if err != nil {
t.Fatalf("Could not create client: %v", err)
}
if client.jws == nil {
t.Fatalf("Expected client.jws to not be nil")
}
if expected, actual := key, client.jws.privKey; actual != expected {
t.Errorf("Expected jws.privKey to be %p but was %p", expected, actual)
}
if client.keyType != keyType {
t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType)
}
if expected, actual := 2, len(client.solvers); actual != expected {
t.Fatalf("Expected %d solver(s), got %d", expected, actual)
}
}
func TestClientOptPort(t *testing.T) {
keyBits := 32 // small value keeps test fast
key, err := rsa.GenerateKey(rand.Reader, keyBits)
if err != nil {
t.Fatal("Could not generate test key:", err)
}
user := mockUser{
email: "test@test.com",
regres: new(RegistrationResource),
privatekey: key,
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
w.Write(data)
}))
optPort := "1234"
optHost := ""
client, err := NewClient(ts.URL, user, RSA2048)
if err != nil {
t.Fatalf("Could not create client: %v", err)
}
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
httpSolver, ok := client.solvers[HTTP01].(*httpChallenge)
if !ok {
t.Fatal("Expected http-01 solver to be httpChallenge type")
}
if httpSolver.jws != client.jws {
t.Error("Expected http-01 to have same jws as client")
}
if got := httpSolver.provider.(*HTTPProviderServer).port; got != optPort {
t.Errorf("Expected http-01 to have port %s but was %s", optPort, got)
}
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
}
httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge)
if !ok {
t.Fatal("Expected tls-sni-01 solver to be httpChallenge type")
}
if httpsSolver.jws != client.jws {
t.Error("Expected tls-sni-01 to have same jws as client")
}
if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
}
if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost {
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got)
}
// test setting different host
optHost = "127.0.0.1"
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
}
if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
}
}
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(250 * time.Millisecond)
w.Header().Add("Replay-Nonce", "12345")
w.Header().Add("Retry-After", "0")
writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URI: "http://example.com/", Token: "token"})
}))
defer ts.Close()
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
j := &jws{privKey: privKey, directoryURL: ts.URL}
ch := make(chan bool)
resultCh := make(chan bool)
go func() {
j.Nonce()
ch <- true
}()
go func() {
j.Nonce()
ch <- true
}()
go func() {
<-ch
<-ch
resultCh <- true
}()
select {
case <-resultCh:
case <-time.After(400 * time.Millisecond):
t.Fatal("JWS is probably holding a lock while making HTTP request")
}
}
func TestValidate(t *testing.T) {
var statuses []string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Minimal stub ACME server for validation.
w.Header().Add("Replay-Nonce", "12345")
w.Header().Add("Retry-After", "0")
switch r.Method {
case "HEAD":
case "POST":
st := statuses[0]
statuses = statuses[1:]
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
case "GET":
st := statuses[0]
statuses = statuses[1:]
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
default:
http.Error(w, r.Method, http.StatusMethodNotAllowed)
}
}))
defer ts.Close()
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
j := &jws{privKey: privKey, directoryURL: ts.URL}
tsts := []struct {
name string
statuses []string
want string
}{
{"POST-unexpected", []string{"weird"}, "unexpected"},
{"POST-valid", []string{"valid"}, ""},
{"POST-invalid", []string{"invalid"}, "Error Detail"},
{"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
{"GET-valid", []string{"pending", "valid"}, ""},
{"GET-invalid", []string{"pending", "invalid"}, "Error Detail"},
}
for _, tst := range tsts {
statuses = tst.statuses
if err := validate(j, "example.com", ts.URL, challenge{Type: "http-01", Token: "token"}); err == nil && tst.want != "" {
t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want)
} else if err != nil && !strings.Contains(err.Error(), tst.want) {
t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want)
}
}
}
func TestGetChallenges(t *testing.T) {
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET", "HEAD":
w.Header().Add("Replay-Nonce", "12345")
w.Header().Add("Retry-After", "0")
writeJSONResponse(w, directory{NewAuthzURL: ts.URL, NewCertURL: ts.URL, NewRegURL: ts.URL, RevokeCertURL: ts.URL})
case "POST":
writeJSONResponse(w, authorization{})
}
}))
defer ts.Close()
keyBits := 512 // small value keeps test fast
keyType := RSA2048
key, err := rsa.GenerateKey(rand.Reader, keyBits)
if err != nil {
t.Fatal("Could not generate test key:", err)
}
user := mockUser{
email: "test@test.com",
regres: &RegistrationResource{NewAuthzURL: ts.URL},
privatekey: key,
}
client, err := NewClient(ts.URL, user, keyType)
if err != nil {
t.Fatalf("Could not create client: %v", err)
}
_, failures := client.getChallenges([]string{"example.com"})
if failures["example.com"] == nil {
t.Fatal("Expecting \"Server did not provide next link to proceed\" error, got nil")
}
}
// writeJSONResponse marshals the body as JSON and writes it to the response.
func writeJSONResponse(w http.ResponseWriter, body interface{}) {
bs, err := json.Marshal(body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(bs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// stubValidate is like validate, except it does nothing.
func stubValidate(j *jws, domain, uri string, chlng challenge) error {
return nil
}
type mockUser struct {
email string
regres *RegistrationResource
privatekey *rsa.PrivateKey
}
func (u mockUser) GetEmail() string { return u.email }
func (u mockUser) GetRegistration() *RegistrationResource { return u.regres }
func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey }

341
acme/commons.go Normal file
View file

@ -0,0 +1,341 @@
// Package acme contains all objects related the ACME endpoints.
// https://www.rfc-editor.org/rfc/rfc8555.html
package acme
import (
"encoding/json"
"time"
)
// ACME status values of Account, Order, Authorization and Challenge objects.
// See https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.6 for details.
const (
StatusDeactivated = "deactivated"
StatusExpired = "expired"
StatusInvalid = "invalid"
StatusPending = "pending"
StatusProcessing = "processing"
StatusReady = "ready"
StatusRevoked = "revoked"
StatusUnknown = "unknown"
StatusValid = "valid"
)
// CRL reason codes as defined in RFC 5280.
// https://datatracker.ietf.org/doc/html/rfc5280#section-5.3.1
const (
CRLReasonUnspecified uint = 0
CRLReasonKeyCompromise uint = 1
CRLReasonCACompromise uint = 2
CRLReasonAffiliationChanged uint = 3
CRLReasonSuperseded uint = 4
CRLReasonCessationOfOperation uint = 5
CRLReasonCertificateHold uint = 6
CRLReasonRemoveFromCRL uint = 8
CRLReasonPrivilegeWithdrawn uint = 9
CRLReasonAACompromise uint = 10
)
// Directory the ACME directory object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1
// - https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
type Directory struct {
NewNonceURL string `json:"newNonce"`
NewAccountURL string `json:"newAccount"`
NewOrderURL string `json:"newOrder"`
NewAuthzURL string `json:"newAuthz"`
RevokeCertURL string `json:"revokeCert"`
KeyChangeURL string `json:"keyChange"`
Meta Meta `json:"meta"`
RenewalInfo string `json:"renewalInfo"`
}
// Meta the ACME meta object (related to Directory).
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1
type Meta struct {
// termsOfService (optional, string):
// A URL identifying the current terms of service.
TermsOfService string `json:"termsOfService"`
// website (optional, string):
// An HTTP or HTTPS URL locating a website providing more information about the ACME server.
Website string `json:"website"`
// caaIdentities (optional, array of string):
// The hostnames that the ACME server recognizes as referring to itself
// for the purposes of CAA record validation as defined in [RFC6844].
// Each string MUST represent the same sequence of ASCII code points
// that the server will expect to see as the "Issuer Domain Name" in a CAA issue or issuewild property tag.
// This allows clients to determine the correct issuer domain name to use when configuring CAA records.
CaaIdentities []string `json:"caaIdentities"`
// externalAccountRequired (optional, boolean):
// If this field is present and set to "true",
// 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"`
}
// ExtendedAccount a extended Account.
type ExtendedAccount struct {
Account
// Contains the value of the response header `Location`
Location string `json:"-"`
}
// Account the ACME account Object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.2
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3
type Account struct {
// status (required, string):
// The status of this account.
// Possible values are: "valid", "deactivated", and "revoked".
// The value "deactivated" should be used to indicate client-initiated deactivation
// whereas "revoked" should be used to indicate server- initiated deactivation. (See Section 7.1.6)
Status string `json:"status,omitempty"`
// contact (optional, array of string):
// An array of URLs that the server can use to contact the client for issues related to this account.
// For example, the server may wish to notify the client about server-initiated revocation or certificate expiration.
// For information on supported URL schemes, see Section 7.3
Contact []string `json:"contact,omitempty"`
// termsOfServiceAgreed (optional, boolean):
// Including this field in a new-account request,
// with a value of true, indicates the client's agreement with the terms of service.
// This field is not updateable by the client.
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
// orders (required, string):
// A URL from which a list of orders submitted by this account can be fetched via a POST-as-GET request,
// as described in Section 7.1.2.1.
Orders string `json:"orders,omitempty"`
// onlyReturnExisting (optional, boolean):
// If this field is present with the value "true",
// then the server MUST NOT create a new account if one does not already exist.
// This allows a client to look up an account URL based on an account key (see Section 7.3.1).
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
// externalAccountBinding (optional, object):
// An optional field for binding the new account with an existing non-ACME account (see Section 7.3.4).
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
}
// ExtendedOrder a extended Order.
type ExtendedOrder struct {
Order
// The order URL, contains the value of the response header `Location`
Location string `json:"-"`
}
// Order the ACME order Object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.3
type Order struct {
// status (required, string):
// The status of this order.
// Possible values are: "pending", "ready", "processing", "valid", and "invalid".
Status string `json:"status,omitempty"`
// expires (optional, string):
// The timestamp after which the server will consider this order invalid,
// encoded in the format specified in RFC 3339 [RFC3339].
// This field is REQUIRED for objects with "pending" or "valid" in the status field.
Expires string `json:"expires,omitempty"`
// identifiers (required, array of object):
// An array of identifier objects that the order pertains to.
Identifiers []Identifier `json:"identifiers"`
// notBefore (optional, string):
// The requested value of the notBefore field in the certificate,
// in the date format defined in [RFC3339].
NotBefore string `json:"notBefore,omitempty"`
// notAfter (optional, string):
// The requested value of the notAfter field in the certificate,
// in the date format defined in [RFC3339].
NotAfter string `json:"notAfter,omitempty"`
// error (optional, object):
// The error that occurred while processing the order, if any.
// This field is structured as a problem document [RFC7807].
Error *ProblemDetails `json:"error,omitempty"`
// authorizations (required, array of string):
// For pending orders,
// the authorizations that the client needs to complete before the requested certificate can be issued (see Section 7.5),
// including unexpired authorizations that the client has completed in the past for identifiers specified in the order.
// The authorizations required are dictated by server policy
// and there may not be a 1:1 relationship between the order identifiers and the authorizations required.
// For final orders (in the "valid" or "invalid" state), the authorizations that were completed.
// Each entry is a URL from which an authorization can be fetched with a POST-as-GET request.
Authorizations []string `json:"authorizations,omitempty"`
// finalize (required, string):
// A URL that a CSR must be POSTed to once all of the order's authorizations are satisfied to finalize the order.
// The result of a successful finalization will be the population of the certificate URL for the order.
Finalize string `json:"finalize,omitempty"`
// certificate (optional, string):
// A URL for the certificate that has been issued in response to this order
Certificate string `json:"certificate,omitempty"`
}
// Authorization the ACME authorization object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4
type Authorization struct {
// status (required, string):
// The status of this authorization.
// Possible values are: "pending", "valid", "invalid", "deactivated", "expired", and "revoked".
Status string `json:"status"`
// expires (optional, string):
// The timestamp after which the server will consider this authorization invalid,
// encoded in the format specified in RFC 3339 [RFC3339].
// This field is REQUIRED for objects with "valid" in the "status" field.
Expires time.Time `json:"expires,omitempty"`
// identifier (required, object):
// The identifier that the account is authorized to represent
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.
// For valid authorizations, the challenge that was validated.
// For invalid authorizations, the challenge that was attempted and failed.
// Each array entry is an object with parameters required to validate the challenge.
// A client should attempt to fulfill one of these challenges,
// and a server should consider any one of the challenges sufficient to make the authorization valid.
Challenges []Challenge `json:"challenges,omitempty"`
// wildcard (optional, boolean):
// For authorizations created as a result of a newOrder request containing a DNS identifier
// with a value that contained a wildcard prefix this field MUST be present, and true.
Wildcard bool `json:"wildcard,omitempty"`
}
// 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"
AuthorizationURL string `json:"-"`
}
// Challenge the ACME challenge object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.5
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-8
type Challenge struct {
// type (required, string):
// The type of challenge encoded in the object.
Type string `json:"type"`
// url (required, string):
// The URL to which a response can be posted.
URL string `json:"url"`
// status (required, string):
// The status of this challenge. Possible values are: "pending", "processing", "valid", and "invalid".
Status string `json:"status"`
// validated (optional, string):
// The time at which the server validated this challenge,
// encoded in the format specified in RFC 3339 [RFC3339].
// This field is REQUIRED if the "status" field is "valid".
Validated time.Time `json:"validated,omitempty"`
// error (optional, object):
// Error that occurred while the server was validating the challenge, if any,
// structured as a problem document [RFC7807].
// Multiple errors can be indicated by using subproblems Section 6.7.1.
// A challenge object with an error MUST have status equal to "invalid".
Error *ProblemDetails `json:"error,omitempty"`
// token (required, string):
// A random value that uniquely identifies the challenge.
// This value MUST have at least 128 bits of entropy.
// It MUST NOT contain any characters outside the base64url alphabet,
// and MUST NOT include base64 padding characters ("=").
// See [RFC4086] for additional information on randomness requirements.
// https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3
// https://www.rfc-editor.org/rfc/rfc8555.html#section-8.4
Token string `json:"token"`
// https://www.rfc-editor.org/rfc/rfc8555.html#section-8.1
KeyAuthorization string `json:"keyAuthorization"`
}
// Identifier the ACME identifier object.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-9.7.7
type Identifier struct {
Type string `json:"type"`
Value string `json:"value"`
}
// CSRMessage Certificate Signing Request.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4
type CSRMessage struct {
// csr (required, string):
// A CSR encoding the parameters for the certificate being requested [RFC2986].
// The CSR is sent in the base64url-encoded version of the DER format.
// (Note: Because this field uses base64url, and does not include headers, it is different from PEM.).
Csr string `json:"csr"`
}
// RevokeCertMessage a certificate revocation message.
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.6
// - https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1
type RevokeCertMessage struct {
// certificate (required, string):
// The certificate to be revoked, in the base64url-encoded version of the DER format.
// (Note: Because this field uses base64url, and does not include headers, it is different from PEM.)
Certificate string `json:"certificate"`
// reason (optional, int):
// One of the revocation reasonCodes defined in Section 5.3.1 of [RFC5280] to be used when generating OCSP responses and CRLs.
// If this field is not set the server SHOULD omit the reasonCode CRL entry extension when generating OCSP responses and CRLs.
// The server MAY disallow a subset of reasonCodes from being used by the user.
// If a request contains a disallowed reasonCode the server MUST reject it with the error type "urn:ietf:params:acme:error:badRevocationReason".
// The problem document detail SHOULD indicate which reasonCodes are allowed.
Reason *uint `json:"reason,omitempty"`
}
// RawCertificate raw data of a certificate.
type RawCertificate struct {
Cert []byte
Issuer []byte
}
// Window is a window of time.
type Window struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
}
// RenewalInfoResponse is the response to GET requests made the renewalInfo endpoint.
// - (4.1. Getting Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
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.
SuggestedWindow Window `json:"suggestedWindow"`
// ExplanationURL is a optional URL pointing to a page which may explain why the suggested renewal window is what it is.
// For example, it may be a page explaining the CA's dynamic load-balancing strategy,
// or a page documenting which certificates are affected by a mass revocation event.
// Callers SHOULD provide this URL to their operator, if present.
ExplanationURL string `json:"explanationURL"`
}
// RenewalInfoUpdateRequest is the JWS payload for POST requests made to the renewalInfo endpoint.
// - (4.2. Updating Renewal Information) https://datatracker.ietf.org/doc/draft-ietf-acme-ari/
type RenewalInfoUpdateRequest struct {
// CertID is the base64url-encoded [RFC4648] bytes of a DER-encoded CertID ASN.1 sequence [RFC6960] with any trailing '=' characters stripped.
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,
// for instance because it has been renewed and the new certificate is in use, or because it is no longer in use.
// Clients SHOULD NOT send a request where this value is false.
Replaced bool `json:"replaced"`
}

View file

@ -1,347 +0,0 @@
package acme
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"math/big"
"net/http"
"strings"
"time"
"encoding/asn1"
"golang.org/x/crypto/ocsp"
)
// KeyType represents the key algo as well as the key size or curve to use.
type KeyType string
type derCertificateBytes []byte
// Constants for all key types we support.
const (
EC256 = KeyType("P256")
EC384 = KeyType("P384")
RSA2048 = KeyType("2048")
RSA4096 = KeyType("4096")
RSA8192 = KeyType("8192")
)
const (
// OCSPGood means that the certificate is valid.
OCSPGood = ocsp.Good
// OCSPRevoked means that the certificate has been deliberately revoked.
OCSPRevoked = ocsp.Revoked
// OCSPUnknown means that the OCSP responder doesn't know about the certificate.
OCSPUnknown = ocsp.Unknown
// OCSPServerFailed means that the OCSP responder failed to process the request.
OCSPServerFailed = ocsp.ServerFailed
)
// Constants for OCSP must staple
var (
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05}
)
// GetOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response,
// the parsed response, and an error, if any. The returned []byte can be passed directly
// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the
// issued certificate, this function will try to get the issuer certificate from the
// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return
// values are nil, the OCSP status may be assumed OCSPUnknown.
func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
certificates, err := parsePEMBundle(bundle)
if err != nil {
return nil, nil, err
}
// We expect the certificate slice to be ordered downwards the chain.
// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
// which should always be the first two certificates. If there's no
// OCSP server listed in the leaf cert, there's nothing to do. And if
// we have only one certificate so far, we need to get the issuer cert.
issuedCert := certificates[0]
if len(issuedCert.OCSPServer) == 0 {
return nil, nil, errors.New("no OCSP server specified in cert")
}
if len(certificates) == 1 {
// TODO: build fallback. If this fails, check the remaining array entries.
if len(issuedCert.IssuingCertificateURL) == 0 {
return nil, nil, errors.New("no issuing certificate URL")
}
resp, err := httpGet(issuedCert.IssuingCertificateURL[0])
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
if err != nil {
return nil, nil, err
}
issuerCert, err := x509.ParseCertificate(issuerBytes)
if err != nil {
return nil, nil, err
}
// Insert it into the slice on position 0
// We want it ordered right SRV CRT -> CA
certificates = append(certificates, issuerCert)
}
issuerCert := certificates[1]
// Finally kick off the OCSP request.
ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
if err != nil {
return nil, nil, err
}
reader := bytes.NewReader(ocspReq)
req, err := httpPost(issuedCert.OCSPServer[0], "application/ocsp-request", reader)
if err != nil {
return nil, nil, err
}
defer req.Body.Close()
ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024))
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
if err != nil {
return nil, nil, err
}
return ocspResBytes, ocspRes, nil
}
func getKeyAuthorization(token string, key interface{}) (string, error) {
var publicKey crypto.PublicKey
switch k := key.(type) {
case *ecdsa.PrivateKey:
publicKey = k.Public()
case *rsa.PrivateKey:
publicKey = k.Public()
}
// Generate the Key Authorization for the challenge
jwk := keyAsJWK(publicKey)
if jwk == nil {
return "", errors.New("Could not generate JWK from key.")
}
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
if err != nil {
return "", err
}
// unpad the base64URL
keyThumb := base64.URLEncoding.EncodeToString(thumbBytes)
index := strings.Index(keyThumb, "=")
if index != -1 {
keyThumb = keyThumb[:index]
}
return token + "." + keyThumb, nil
}
// parsePEMBundle parses a certificate bundle from top to bottom and returns
// a slice of x509 certificates. This function will error if no certificates are found.
func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
var certificates []*x509.Certificate
var certDERBlock *pem.Block
for {
certDERBlock, bundle = pem.Decode(bundle)
if certDERBlock == nil {
break
}
if certDERBlock.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(certDERBlock.Bytes)
if err != nil {
return nil, err
}
certificates = append(certificates, cert)
}
}
if len(certificates) == 0 {
return nil, errors.New("No certificates were found while parsing the bundle.")
}
return certificates, nil
}
func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
keyBlock, _ := pem.Decode(key)
switch keyBlock.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(keyBlock.Bytes)
default:
return nil, errors.New("Unknown PEM header value")
}
}
func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
switch keyType {
case EC256:
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case EC384:
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case RSA2048:
return rsa.GenerateKey(rand.Reader, 2048)
case RSA4096:
return rsa.GenerateKey(rand.Reader, 4096)
case RSA8192:
return rsa.GenerateKey(rand.Reader, 8192)
}
return nil, fmt.Errorf("Invalid KeyType: %s", keyType)
}
func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
template := x509.CertificateRequest{
Subject: pkix.Name{
CommonName: domain,
},
}
if len(san) > 0 {
template.DNSNames = san
}
if mustStaple {
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
Id: tlsFeatureExtensionOID,
Value: ocspMustStapleFeature,
})
}
return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
}
func pemEncode(data interface{}) []byte {
var pemBlock *pem.Block
switch key := data.(type) {
case *ecdsa.PrivateKey:
keyBytes, _ := x509.MarshalECPrivateKey(key)
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
case *rsa.PrivateKey:
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
break
case *x509.CertificateRequest:
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
break
case derCertificateBytes:
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))}
}
return pem.EncodeToMemory(pemBlock)
}
func pemDecode(data []byte) (*pem.Block, error) {
pemBlock, _ := pem.Decode(data)
if pemBlock == nil {
return nil, fmt.Errorf("Pem decode did not yield a valid block. Is the certificate in the right format?")
}
return pemBlock, nil
}
func pemDecodeTox509(pem []byte) (*x509.Certificate, error) {
pemBlock, err := pemDecode(pem)
if pemBlock == nil {
return nil, err
}
return x509.ParseCertificate(pemBlock.Bytes)
}
func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
pemBlock, err := pemDecode(pem)
if pemBlock == nil {
return nil, err
}
if pemBlock.Type != "CERTIFICATE REQUEST" {
return nil, fmt.Errorf("PEM block is not a certificate request")
}
return x509.ParseCertificateRequest(pemBlock.Bytes)
}
// GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate.
// The certificate has to be PEM encoded. Any other encodings like DER will fail.
func GetPEMCertExpiration(cert []byte) (time.Time, error) {
pemBlock, err := pemDecode(cert)
if pemBlock == nil {
return time.Time{}, err
}
return getCertExpiration(pemBlock.Bytes)
}
// getCertExpiration returns the "NotAfter" date of a DER encoded certificate.
func getCertExpiration(cert []byte) (time.Time, error) {
pCert, err := x509.ParseCertificate(cert)
if err != nil {
return time.Time{}, err
}
return pCert.NotAfter, nil
}
func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
derBytes, err := generateDerCert(privKey, time.Time{}, domain)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
}
func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, err
}
if expiration.IsZero() {
expiration = time.Now().Add(365)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "ACME Challenge TEMP",
},
NotBefore: time.Now(),
NotAfter: expiration,
KeyUsage: x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
DNSNames: []string{domain},
}
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
}
func limitReader(rd io.ReadCloser, numBytes int64) io.ReadCloser {
return http.MaxBytesReader(nil, rd, numBytes)
}

View file

@ -1,93 +0,0 @@
package acme
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"testing"
"time"
)
func TestGeneratePrivateKey(t *testing.T) {
key, err := generatePrivateKey(RSA2048)
if err != nil {
t.Error("Error generating private key:", err)
}
if key == nil {
t.Error("Expected key to not be nil, but it was")
}
}
func TestGenerateCSR(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
t.Fatal("Error generating private key:", err)
}
csr, err := generateCsr(key, "fizz.buzz", nil, true)
if err != nil {
t.Error("Error generating CSR:", err)
}
if csr == nil || len(csr) == 0 {
t.Error("Expected CSR with data, but it was nil or length 0")
}
}
func TestPEMEncode(t *testing.T) {
buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
reader := MockRandReader{b: buf}
key, err := rsa.GenerateKey(reader, 32)
if err != nil {
t.Fatal("Error generating private key:", err)
}
data := pemEncode(key)
if data == nil {
t.Fatal("Expected result to not be nil, but it was")
}
if len(data) != 127 {
t.Errorf("Expected PEM encoding to be length 127, but it was %d", len(data))
}
}
func TestPEMCertExpiration(t *testing.T) {
privKey, err := generatePrivateKey(RSA2048)
if err != nil {
t.Fatal("Error generating private key:", err)
}
expiration := time.Now().Add(365)
expiration = expiration.Round(time.Second)
certBytes, err := generateDerCert(privKey.(*rsa.PrivateKey), expiration, "test.com")
if err != nil {
t.Fatal("Error generating cert:", err)
}
buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
// Some random string should return an error.
if ctime, err := GetPEMCertExpiration(buf.Bytes()); err == nil {
t.Errorf("Expected getCertExpiration to return an error for garbage string but returned %v", ctime)
}
// A DER encoded certificate should return an error.
if _, err := GetPEMCertExpiration(certBytes); err == nil {
t.Errorf("Expected getCertExpiration to return an error for DER certificates but returned none.")
}
// A PEM encoded certificate should work ok.
pemCert := pemEncode(derCertificateBytes(certBytes))
if ctime, err := GetPEMCertExpiration(pemCert); err != nil || !ctime.Equal(expiration.UTC()) {
t.Errorf("Expected getCertExpiration to return %v but returned %v. Error: %v", expiration, ctime, err)
}
}
type MockRandReader struct {
b *bytes.Buffer
}
func (r MockRandReader) Read(p []byte) (int, error) {
return r.b.Read(p)
}

View file

@ -1,309 +0,0 @@
package acme
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"log"
"net"
"strings"
"time"
"github.com/miekg/dns"
)
type preCheckDNSFunc func(fqdn, value string) (bool, error)
var (
// PreCheckDNS checks DNS propagation before notifying ACME that
// the DNS challenge is ready.
PreCheckDNS preCheckDNSFunc = checkDNSPropagation
fqdnToZone = map[string]string{}
)
const defaultResolvConf = "/etc/resolv.conf"
var defaultNameservers = []string{
"google-public-dns-a.google.com:53",
"google-public-dns-b.google.com:53",
}
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
// DNSTimeout is used to override the default DNS timeout of 10 seconds.
var DNSTimeout = 10 * time.Second
// getNameservers attempts to get systems nameservers before falling back to the defaults
func getNameservers(path string, defaults []string) []string {
config, err := dns.ClientConfigFromFile(path)
if err != nil || len(config.Servers) == 0 {
return defaults
}
systemNameservers := []string{}
for _, server := range config.Servers {
// ensure all servers have a port number
if _, _, err := net.SplitHostPort(server); err != nil {
systemNameservers = append(systemNameservers, net.JoinHostPort(server, "53"))
} else {
systemNameservers = append(systemNameservers, server)
}
}
return systemNameservers
}
// DNS01Record returns a DNS record which will fulfill the `dns-01` challenge
func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
// base64URL encoding without padding
keyAuthSha := base64.URLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
value = strings.TrimRight(keyAuthSha, "=")
ttl = 120
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
return
}
// dnsChallenge implements the dns-01 challenge according to ACME 7.5
type dnsChallenge struct {
jws *jws
validate validateFunc
provider ChallengeProvider
}
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO][%s] acme: Trying to solve DNS-01", domain)
if s.provider == nil {
return errors.New("No DNS Provider configured")
}
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
if err != nil {
return err
}
err = s.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("Error presenting token: %s", err)
}
defer func() {
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Printf("Error cleaning up %s: %v ", domain, err)
}
}()
fqdn, value, _ := DNS01Record(domain, keyAuth)
logf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers)
var timeout, interval time.Duration
switch provider := s.provider.(type) {
case ChallengeProviderTimeout:
timeout, interval = provider.Timeout()
default:
timeout, interval = 60*time.Second, 2*time.Second
}
err = WaitFor(timeout, interval, func() (bool, error) {
return PreCheckDNS(fqdn, value)
})
if err != nil {
return err
}
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func checkDNSPropagation(fqdn, value string) (bool, error) {
// Initial attempt to resolve at the recursive NS
r, err := dnsQuery(fqdn, dns.TypeTXT, RecursiveNameservers, true)
if err != nil {
return false, err
}
if r.Rcode == dns.RcodeSuccess {
// If we see a CNAME here then use the alias
for _, rr := range r.Answer {
if cn, ok := rr.(*dns.CNAME); ok {
if cn.Hdr.Name == fqdn {
fqdn = cn.Target
break
}
}
}
}
authoritativeNss, err := lookupNameservers(fqdn)
if err != nil {
return false, err
}
return checkAuthoritativeNss(fqdn, value, authoritativeNss)
}
// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
for _, ns := range nameservers {
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false)
if err != nil {
return false, err
}
if r.Rcode != dns.RcodeSuccess {
return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
}
var found bool
for _, rr := range r.Answer {
if txt, ok := rr.(*dns.TXT); ok {
if strings.Join(txt.Txt, "") == value {
found = true
break
}
}
}
if !found {
return false, fmt.Errorf("NS %s did not return the expected TXT record", ns)
}
}
return true, nil
}
// dnsQuery will query a nameserver, iterating through the supplied servers as it retries
// The nameserver should include a port, to facilitate testing where we talk to a mock dns server.
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (in *dns.Msg, err error) {
m := new(dns.Msg)
m.SetQuestion(fqdn, rtype)
m.SetEdns0(4096, false)
if !recursive {
m.RecursionDesired = false
}
// Will retry the request based on the number of servers (n+1)
for i := 1; i <= len(nameservers)+1; i++ {
ns := nameservers[i%len(nameservers)]
udp := &dns.Client{Net: "udp", Timeout: DNSTimeout}
in, _, err = udp.Exchange(m, ns)
if err == dns.ErrTruncated {
tcp := &dns.Client{Net: "tcp", Timeout: DNSTimeout}
// If the TCP request succeeds, the err will reset to nil
in, _, err = tcp.Exchange(m, ns)
}
if err == nil {
break
}
}
return
}
// lookupNameservers returns the authoritative nameservers for the given fqdn.
func lookupNameservers(fqdn string) ([]string, error) {
var authoritativeNss []string
zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
if err != nil {
return nil, fmt.Errorf("Could not determine the zone: %v", err)
}
r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true)
if err != nil {
return nil, err
}
for _, rr := range r.Answer {
if ns, ok := rr.(*dns.NS); ok {
authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))
}
}
if len(authoritativeNss) > 0 {
return authoritativeNss, nil
}
return nil, fmt.Errorf("Could not determine authoritative nameservers")
}
// FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the
// domain labels until the nameserver returns a SOA record in the answer section.
func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
// Do we have it cached?
if zone, ok := fqdnToZone[fqdn]; ok {
return zone, nil
}
labelIndexes := dns.Split(fqdn)
for _, index := range labelIndexes {
domain := fqdn[index:]
in, err := dnsQuery(domain, dns.TypeSOA, nameservers, true)
if err != nil {
return "", err
}
// Any response code other than NOERROR and NXDOMAIN is treated as error
if in.Rcode != dns.RcodeNameError && in.Rcode != dns.RcodeSuccess {
return "", fmt.Errorf("Unexpected response code '%s' for %s",
dns.RcodeToString[in.Rcode], domain)
}
// Check if we got a SOA RR in the answer section
if in.Rcode == dns.RcodeSuccess {
// CNAME records cannot/should not exist at the root of a zone.
// So we skip a domain when a CNAME is found.
if dnsMsgContainsCNAME(in) {
continue
}
for _, ans := range in.Answer {
if soa, ok := ans.(*dns.SOA); ok {
zone := soa.Hdr.Name
fqdnToZone[fqdn] = zone
return zone, nil
}
}
}
}
return "", fmt.Errorf("Could not find the start of authority")
}
// dnsMsgContainsCNAME checks for a CNAME answer in msg
func dnsMsgContainsCNAME(msg *dns.Msg) bool {
for _, ans := range msg.Answer {
if _, ok := ans.(*dns.CNAME); ok {
return true
}
}
return false
}
// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
func ClearFqdnCache() {
fqdnToZone = map[string]string{}
}
// ToFqdn converts the name into a fqdn appending a trailing dot.
func ToFqdn(name string) string {
n := len(name)
if n == 0 || name[n-1] == '.' {
return name
}
return name + "."
}
// UnFqdn converts the fqdn into a name removing the trailing dot.
func UnFqdn(name string) string {
n := len(name)
if n != 0 && name[n-1] == '.' {
return name[:n-1]
}
return name
}

View file

@ -1,53 +0,0 @@
package acme
import (
"bufio"
"fmt"
"os"
)
const (
dnsTemplate = "%s %d IN TXT \"%s\""
)
// DNSProviderManual is an implementation of the ChallengeProvider interface
type DNSProviderManual struct{}
// NewDNSProviderManual returns a DNSProviderManual instance.
func NewDNSProviderManual() (*DNSProviderManual, error) {
return &DNSProviderManual{}, nil
}
// Present prints instructions for manually creating the TXT record
func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := DNS01Record(domain, keyAuth)
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value)
authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
if err != nil {
return err
}
logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone)
logf("[INFO] acme: %s", dnsRecord)
logf("[INFO] acme: Press 'Enter' when you are done")
reader := bufio.NewReader(os.Stdin)
_, _ = reader.ReadString('\n')
return nil
}
// CleanUp prints instructions for manually removing the TXT record
func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
fqdn, _, ttl := DNS01Record(domain, keyAuth)
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, "...")
authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
if err != nil {
return err
}
logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone)
logf("[INFO] acme: %s", dnsRecord)
return nil
}

View file

@ -1,200 +0,0 @@
package acme
import (
"bufio"
"crypto/rand"
"crypto/rsa"
"net/http"
"net/http/httptest"
"os"
"reflect"
"sort"
"strings"
"testing"
"time"
)
var lookupNameserversTestsOK = []struct {
fqdn string
nss []string
}{
{"books.google.com.ng.",
[]string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
},
{"www.google.com.",
[]string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
},
{"physics.georgetown.edu.",
[]string{"ns1.georgetown.edu.", "ns2.georgetown.edu.", "ns3.georgetown.edu."},
},
}
var lookupNameserversTestsErr = []struct {
fqdn string
error string
}{
// invalid tld
{"_null.n0n0.",
"Could not determine the zone",
},
}
var findZoneByFqdnTests = []struct {
fqdn string
zone string
}{
{"mail.google.com.", "google.com."}, // domain is a CNAME
{"foo.google.com.", "google.com."}, // domain is a non-existent subdomain
{"example.com.ac.", "ac."}, // domain is a eTLD
{"cross-zone-example.assets.sh.", "assets.sh."}, // domain is a cross-zone CNAME
}
var checkAuthoritativeNssTests = []struct {
fqdn, value string
ns []string
ok bool
}{
// TXT RR w/ expected value
{"8.8.8.8.asn.routeviews.org.", "151698.8.8.024", []string{"asnums.routeviews.org."},
true,
},
// No TXT RR
{"ns1.google.com.", "", []string{"ns2.google.com."},
false,
},
}
var checkAuthoritativeNssTestsErr = []struct {
fqdn, value string
ns []string
error string
}{
// TXT RR /w unexpected value
{"8.8.8.8.asn.routeviews.org.", "fe01=", []string{"asnums.routeviews.org."},
"did not return the expected TXT record",
},
// No TXT RR
{"ns1.google.com.", "fe01=", []string{"ns2.google.com."},
"did not return the expected TXT record",
},
}
var checkResolvConfServersTests = []struct {
fixture string
expected []string
defaults []string
}{
{"testdata/resolv.conf.1", []string{"10.200.3.249:53", "10.200.3.250:5353", "[2001:4860:4860::8844]:53", "[10.0.0.1]:5353"}, []string{"127.0.0.1:53"}},
{"testdata/resolv.conf.nonexistant", []string{"127.0.0.1:53"}, []string{"127.0.0.1:53"}},
}
func TestDNSValidServerResponse(t *testing.T) {
PreCheckDNS = func(fqdn, value string) (bool, error) {
return true, nil
}
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Replay-Nonce", "12345")
w.Write([]byte("{\"type\":\"dns01\",\"status\":\"valid\",\"uri\":\"http://some.url\",\"token\":\"http8\"}"))
}))
manualProvider, _ := NewDNSProviderManual()
jws := &jws{privKey: privKey, directoryURL: ts.URL}
solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider}
clientChallenge := challenge{Type: "dns01", Status: "pending", URI: ts.URL, Token: "http8"}
go func() {
time.Sleep(time.Second * 2)
f := bufio.NewWriter(os.Stdout)
defer f.Flush()
f.WriteString("\n")
}()
if err := solver.Solve(clientChallenge, "example.com"); err != nil {
t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
}
}
func TestPreCheckDNS(t *testing.T) {
ok, err := PreCheckDNS("acme-staging.api.letsencrypt.org", "fe01=")
if err != nil || !ok {
t.Errorf("preCheckDNS failed for acme-staging.api.letsencrypt.org")
}
}
func TestLookupNameserversOK(t *testing.T) {
for _, tt := range lookupNameserversTestsOK {
nss, err := lookupNameservers(tt.fqdn)
if err != nil {
t.Fatalf("#%s: got %q; want nil", tt.fqdn, err)
}
sort.Strings(nss)
sort.Strings(tt.nss)
if !reflect.DeepEqual(nss, tt.nss) {
t.Errorf("#%s: got %v; want %v", tt.fqdn, nss, tt.nss)
}
}
}
func TestLookupNameserversErr(t *testing.T) {
for _, tt := range lookupNameserversTestsErr {
_, err := lookupNameservers(tt.fqdn)
if err == nil {
t.Fatalf("#%s: expected %q (error); got <nil>", tt.fqdn, tt.error)
}
if !strings.Contains(err.Error(), tt.error) {
t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err)
continue
}
}
}
func TestFindZoneByFqdn(t *testing.T) {
for _, tt := range findZoneByFqdnTests {
res, err := FindZoneByFqdn(tt.fqdn, RecursiveNameservers)
if err != nil {
t.Errorf("FindZoneByFqdn failed for %s: %v", tt.fqdn, err)
}
if res != tt.zone {
t.Errorf("%s: got %s; want %s", tt.fqdn, res, tt.zone)
}
}
}
func TestCheckAuthoritativeNss(t *testing.T) {
for _, tt := range checkAuthoritativeNssTests {
ok, _ := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns)
if ok != tt.ok {
t.Errorf("%s: got %t; want %t", tt.fqdn, ok, tt.ok)
}
}
}
func TestCheckAuthoritativeNssErr(t *testing.T) {
for _, tt := range checkAuthoritativeNssTestsErr {
_, err := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns)
if err == nil {
t.Fatalf("#%s: expected %q (error); got <nil>", tt.fqdn, tt.error)
}
if !strings.Contains(err.Error(), tt.error) {
t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err)
continue
}
}
}
func TestResolveConfServers(t *testing.T) {
for _, tt := range checkResolvConfServersTests {
result := getNameservers(tt.fixture, tt.defaults)
sort.Strings(result)
sort.Strings(tt.expected)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("#%s: expected %q; got %q", tt.fixture, tt.expected, result)
}
}
}

View file

@ -1,94 +0,0 @@
package acme
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
)
const (
tosAgreementError = "Must agree to subscriber agreement before any further actions"
invalidNonceError = "JWS has invalid anti-replay nonce"
)
// RemoteError is the base type for all errors specific to the ACME protocol.
type RemoteError struct {
StatusCode int `json:"status,omitempty"`
Type string `json:"type"`
Detail string `json:"detail"`
}
func (e RemoteError) Error() string {
return fmt.Sprintf("acme: Error %d - %s - %s", e.StatusCode, e.Type, e.Detail)
}
// TOSError represents the error which is returned if the user needs to
// accept the TOS.
// TODO: include the new TOS url if we can somehow obtain it.
type TOSError struct {
RemoteError
}
// NonceError represents the error which is returned if the
// nonce sent by the client was not accepted by the server.
type NonceError struct {
RemoteError
}
type domainError struct {
Domain string
Error error
}
type challengeError struct {
RemoteError
records []validationRecord
}
func (c challengeError) Error() string {
var errStr string
for _, validation := range c.records {
errStr = errStr + fmt.Sprintf("\tValidation for %s:%s\n\tResolved to:\n\t\t%s\n\tUsed: %s\n\n",
validation.Hostname, validation.Port, strings.Join(validation.ResolvedAddresses, "\n\t\t"), validation.UsedAddress)
}
return fmt.Sprintf("%s\nError Detail:\n%s", c.RemoteError.Error(), errStr)
}
func handleHTTPError(resp *http.Response) error {
var errorDetail RemoteError
contentType := resp.Header.Get("Content-Type")
if contentType == "application/json" || contentType == "application/problem+json" {
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
if err != nil {
return err
}
} else {
detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return err
}
errorDetail.Detail = string(detailBytes)
}
errorDetail.StatusCode = resp.StatusCode
// Check for errors we handle specifically
if errorDetail.StatusCode == http.StatusForbidden && errorDetail.Detail == tosAgreementError {
return TOSError{errorDetail}
}
if errorDetail.StatusCode == http.StatusBadRequest && strings.HasPrefix(errorDetail.Detail, invalidNonceError) {
return NonceError{errorDetail}
}
return errorDetail
}
func handleChallengeError(chlng challenge) error {
return challengeError{chlng.Error, chlng.ValidationRecords}
}

58
acme/errors.go Normal file
View file

@ -0,0 +1,58 @@
package acme
import (
"fmt"
)
// Errors types.
const (
errNS = "urn:ietf:params:acme:error:"
BadNonceErr = errNS + "badNonce"
)
// ProblemDetails the problem details object.
// - https://www.rfc-editor.org/rfc/rfc7807.html#section-3.1
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3.3
type ProblemDetails struct {
Type string `json:"type,omitempty"`
Detail string `json:"detail,omitempty"`
HTTPStatus int `json:"status,omitempty"`
Instance string `json:"instance,omitempty"`
SubProblems []SubProblem `json:"subproblems,omitempty"`
// additional values to have a better error message (Not defined by the RFC)
Method string `json:"method,omitempty"`
URL string `json:"url,omitempty"`
}
// SubProblem a "subproblems".
// - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.7.1
type SubProblem struct {
Type string `json:"type,omitempty"`
Detail string `json:"detail,omitempty"`
Identifier Identifier `json:"identifier,omitempty"`
}
func (p ProblemDetails) Error() string {
msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus)
if p.Method != "" || p.URL != "" {
msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL)
}
msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail)
for _, sub := range p.SubProblems {
msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail)
}
if p.Instance != "" {
msg += ", url: " + p.Instance
}
return msg
}
// NonceError represents the error which is returned
// if the nonce sent by the client was not accepted by the server.
type NonceError struct {
*ProblemDetails
}

View file

@ -1,160 +0,0 @@
package acme
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"runtime"
"strings"
"time"
)
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
var UserAgent string
// HTTPClient is an HTTP client with a reasonable timeout value.
var HTTPClient = http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 15 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
const (
// defaultGoUserAgent is the Go HTTP package user agent string. Too
// bad it isn't exported. If it changes, we should update it here, too.
defaultGoUserAgent = "Go-http-client/1.1"
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme"
)
// httpHead performs a HEAD request with a proper User-Agent string.
// The response body (resp.Body) is already closed when this function returns.
func httpHead(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to head %q: %v", url, err)
}
req.Header.Set("User-Agent", userAgent())
resp, err = HTTPClient.Do(req)
if err != nil {
return resp, fmt.Errorf("failed to do head %q: %v", url, err)
}
resp.Body.Close()
return resp, err
}
// httpPost performs a POST request with a proper User-Agent string.
// Callers should close resp.Body when done reading from it.
func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, fmt.Errorf("failed to post %q: %v", url, err)
}
req.Header.Set("Content-Type", bodyType)
req.Header.Set("User-Agent", userAgent())
return HTTPClient.Do(req)
}
// httpGet performs a GET request with a proper User-Agent string.
// Callers should close resp.Body when done reading from it.
func httpGet(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to get %q: %v", url, err)
}
req.Header.Set("User-Agent", userAgent())
return HTTPClient.Do(req)
}
// getJSON performs an HTTP GET request and parses the response body
// as JSON, into the provided respBody object.
func getJSON(uri string, respBody interface{}) (http.Header, error) {
resp, err := httpGet(uri)
if err != nil {
return nil, fmt.Errorf("failed to get json %q: %v", uri, err)
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
return resp.Header, handleHTTPError(resp)
}
return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
}
// postJSON performs an HTTP POST request and parses the response body
// as JSON, into the provided respBody object.
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
jsonBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.New("Failed to marshal network message...")
}
resp, err := j.post(uri, jsonBytes)
if err != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
err := handleHTTPError(resp)
switch err.(type) {
case NonceError:
// Retry once if the nonce was invalidated
retryResp, err := j.post(uri, jsonBytes)
if err != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
}
defer retryResp.Body.Close()
if retryResp.StatusCode >= http.StatusBadRequest {
return retryResp.Header, handleHTTPError(retryResp)
}
if respBody == nil {
return retryResp.Header, nil
}
return retryResp.Header, json.NewDecoder(retryResp.Body).Decode(respBody)
default:
return resp.Header, err
}
}
if respBody == nil {
return resp.Header, nil
}
return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
}
// userAgent builds and returns the User-Agent string to use in requests.
func userAgent() string {
ua := fmt.Sprintf("%s (%s; %s) %s %s", defaultGoUserAgent, runtime.GOOS, runtime.GOARCH, ourUserAgent, UserAgent)
return strings.TrimSpace(ua)
}

View file

@ -1,41 +0,0 @@
package acme
import (
"fmt"
"log"
)
type httpChallenge struct {
jws *jws
validate validateFunc
provider ChallengeProvider
}
// HTTP01ChallengePath returns the URL path for the `http-01` challenge
func HTTP01ChallengePath(token string) string {
return "/.well-known/acme-challenge/" + token
}
func (s *httpChallenge) Solve(chlng challenge, domain string) error {
logf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
if err != nil {
return err
}
err = s.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
}
defer func() {
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Printf("[%s] error cleaning up: %v", domain, err)
}
}()
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}

View file

@ -1,79 +0,0 @@
package acme
import (
"fmt"
"net"
"net/http"
"strings"
)
// HTTPProviderServer implements ChallengeProvider for `http-01` challenge
// It may be instantiated without using the NewHTTPProviderServer function if
// you want only to use the default values.
type HTTPProviderServer struct {
iface string
port string
done chan bool
listener net.Listener
}
// NewHTTPProviderServer creates a new HTTPProviderServer on the selected interface and port.
// Setting iface and / or port to an empty string will make the server fall back to
// the "any" interface and port 80 respectively.
func NewHTTPProviderServer(iface, port string) *HTTPProviderServer {
return &HTTPProviderServer{iface: iface, port: port}
}
// Present starts a web server and makes the token available at `HTTP01ChallengePath(token)` for web requests.
func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error {
if s.port == "" {
s.port = "80"
}
var err error
s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port))
if err != nil {
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
}
s.done = make(chan bool)
go s.serve(domain, token, keyAuth)
return nil
}
// CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)`
func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error {
if s.listener == nil {
return nil
}
s.listener.Close()
<-s.done
return nil
}
func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
path := HTTP01ChallengePath(token)
// The handler validates the HOST header and request type.
// For validation it then writes the token the server returned with the challenge
mux := http.NewServeMux()
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(keyAuth))
logf("[INFO][%s] Served key authentication", domain)
} else {
logf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method)
w.Write([]byte("TEST"))
}
})
httpServer := &http.Server{
Handler: mux,
}
// Once httpServer is shut down we don't want any lingering
// connections, so disable KeepAlives.
httpServer.SetKeepAlivesEnabled(false)
httpServer.Serve(s.listener)
s.done <- true
}

View file

@ -1,57 +0,0 @@
package acme
import (
"crypto/rand"
"crypto/rsa"
"io/ioutil"
"strings"
"testing"
)
func TestHTTPChallenge(t *testing.T) {
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
j := &jws{privKey: privKey}
clientChallenge := challenge{Type: HTTP01, Token: "http1"}
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
resp, err := httpGet(uri)
if err != nil {
return err
}
defer resp.Body.Close()
if want := "text/plain"; resp.Header.Get("Content-Type") != want {
t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
bodyStr := string(body)
if bodyStr != chlng.KeyAuthorization {
t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization)
}
return nil
}
solver := &httpChallenge{jws: j, validate: mockValidate, provider: &HTTPProviderServer{port: "23457"}}
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
t.Errorf("Solve error: got %v, want nil", err)
}
}
func TestHTTPChallengeInvalidPort(t *testing.T) {
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
j := &jws{privKey: privKey}
clientChallenge := challenge{Type: HTTP01, Token: "http2"}
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}}
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
t.Errorf("Solve error: got %v, want error", err)
} else if want, want18 := "invalid port 123456", "123456: invalid port"; !strings.HasSuffix(err.Error(), want) && !strings.HasSuffix(err.Error(), want18) {
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
}
}

View file

@ -1,100 +0,0 @@
package acme
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestHTTPHeadUserAgent(t *testing.T) {
var ua, method string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ua = r.Header.Get("User-Agent")
method = r.Method
}))
defer ts.Close()
_, err := httpHead(ts.URL)
if err != nil {
t.Fatal(err)
}
if method != "HEAD" {
t.Errorf("Expected method to be HEAD, got %s", method)
}
if !strings.Contains(ua, ourUserAgent) {
t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
}
}
func TestHTTPGetUserAgent(t *testing.T) {
var ua, method string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ua = r.Header.Get("User-Agent")
method = r.Method
}))
defer ts.Close()
res, err := httpGet(ts.URL)
if err != nil {
t.Fatal(err)
}
res.Body.Close()
if method != "GET" {
t.Errorf("Expected method to be GET, got %s", method)
}
if !strings.Contains(ua, ourUserAgent) {
t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
}
}
func TestHTTPPostUserAgent(t *testing.T) {
var ua, method string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ua = r.Header.Get("User-Agent")
method = r.Method
}))
defer ts.Close()
res, err := httpPost(ts.URL, "text/plain", strings.NewReader("falalalala"))
if err != nil {
t.Fatal(err)
}
res.Body.Close()
if method != "POST" {
t.Errorf("Expected method to be POST, got %s", method)
}
if !strings.Contains(ua, ourUserAgent) {
t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
}
}
func TestUserAgent(t *testing.T) {
ua := userAgent()
if !strings.Contains(ua, defaultGoUserAgent) {
t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua)
}
if !strings.Contains(ua, ourUserAgent) {
t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua)
}
if strings.HasSuffix(ua, " ") {
t.Errorf("UA should not have trailing spaces; got '%s'", ua)
}
// customize the UA by appending a value
UserAgent = "MyApp/1.2.3"
ua = userAgent()
if !strings.Contains(ua, defaultGoUserAgent) {
t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua)
}
if !strings.Contains(ua, ourUserAgent) {
t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua)
}
if !strings.Contains(ua, UserAgent) {
t.Errorf("Expected custom UA to contain %s, got '%s'", UserAgent, ua)
}
}

View file

@ -1,131 +0,0 @@
package acme
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"fmt"
"net/http"
"sync"
"gopkg.in/square/go-jose.v1"
)
type jws struct {
directoryURL string
privKey crypto.PrivateKey
nonces nonceManager
}
func keyAsJWK(key interface{}) *jose.JsonWebKey {
switch k := key.(type) {
case *ecdsa.PublicKey:
return &jose.JsonWebKey{Key: k, Algorithm: "EC"}
case *rsa.PublicKey:
return &jose.JsonWebKey{Key: k, Algorithm: "RSA"}
default:
return nil
}
}
// Posts a JWS signed message to the specified URL.
// It does NOT close the response body, so the caller must
// do that if no error was returned.
func (j *jws) post(url string, content []byte) (*http.Response, error) {
signedContent, err := j.signContent(content)
if err != nil {
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
}
resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize())))
if err != nil {
return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error())
}
nonce, nonceErr := getNonceFromResponse(resp)
if nonceErr == nil {
j.nonces.Push(nonce)
}
return resp, nil
}
func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
var alg jose.SignatureAlgorithm
switch k := j.privKey.(type) {
case *rsa.PrivateKey:
alg = jose.RS256
case *ecdsa.PrivateKey:
if k.Curve == elliptic.P256() {
alg = jose.ES256
} else if k.Curve == elliptic.P384() {
alg = jose.ES384
}
}
signer, err := jose.NewSigner(alg, j.privKey)
if err != nil {
return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error())
}
signer.SetNonceSource(j)
signed, err := signer.Sign(content)
if err != nil {
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
}
return signed, nil
}
func (j *jws) Nonce() (string, error) {
if nonce, ok := j.nonces.Pop(); ok {
return nonce, nil
}
return getNonce(j.directoryURL)
}
type nonceManager struct {
nonces []string
sync.Mutex
}
func (n *nonceManager) Pop() (string, bool) {
n.Lock()
defer n.Unlock()
if len(n.nonces) == 0 {
return "", false
}
nonce := n.nonces[len(n.nonces)-1]
n.nonces = n.nonces[:len(n.nonces)-1]
return nonce, true
}
func (n *nonceManager) Push(nonce string) {
n.Lock()
defer n.Unlock()
n.nonces = append(n.nonces, nonce)
}
func getNonce(url string) (string, error) {
resp, err := httpHead(url)
if err != nil {
return "", fmt.Errorf("Failed to get nonce from HTTP HEAD -> %s", err.Error())
}
return getNonceFromResponse(resp)
}
func getNonceFromResponse(resp *http.Response) (string, error) {
nonce := resp.Header.Get("Replay-Nonce")
if nonce == "" {
return "", fmt.Errorf("Server did not respond with a proper nonce header.")
}
return nonce, nil
}

View file

@ -1,115 +0,0 @@
package acme
import (
"time"
"gopkg.in/square/go-jose.v1"
)
type directory struct {
NewAuthzURL string `json:"new-authz"`
NewCertURL string `json:"new-cert"`
NewRegURL string `json:"new-reg"`
RevokeCertURL string `json:"revoke-cert"`
}
type registrationMessage struct {
Resource string `json:"resource"`
Contact []string `json:"contact"`
Delete bool `json:"delete,omitempty"`
}
// Registration is returned by the ACME server after the registration
// The client implementation should save this registration somewhere.
type Registration struct {
Resource string `json:"resource,omitempty"`
ID int `json:"id"`
Key jose.JsonWebKey `json:"key"`
Contact []string `json:"contact"`
Agreement string `json:"agreement,omitempty"`
Authorizations string `json:"authorizations,omitempty"`
Certificates string `json:"certificates,omitempty"`
}
// RegistrationResource represents all important informations about a registration
// of which the client needs to keep track itself.
type RegistrationResource struct {
Body Registration `json:"body,omitempty"`
URI string `json:"uri,omitempty"`
NewAuthzURL string `json:"new_authzr_uri,omitempty"`
TosURL string `json:"terms_of_service,omitempty"`
}
type authorizationResource struct {
Body authorization
Domain string
NewCertURL string
AuthURL string
}
type authorization struct {
Resource string `json:"resource,omitempty"`
Identifier identifier `json:"identifier"`
Status string `json:"status,omitempty"`
Expires time.Time `json:"expires,omitempty"`
Challenges []challenge `json:"challenges,omitempty"`
Combinations [][]int `json:"combinations,omitempty"`
}
type identifier struct {
Type string `json:"type"`
Value string `json:"value"`
}
type validationRecord struct {
URI string `json:"url,omitempty"`
Hostname string `json:"hostname,omitempty"`
Port string `json:"port,omitempty"`
ResolvedAddresses []string `json:"addressesResolved,omitempty"`
UsedAddress string `json:"addressUsed,omitempty"`
}
type challenge struct {
Resource string `json:"resource,omitempty"`
Type Challenge `json:"type,omitempty"`
Status string `json:"status,omitempty"`
URI string `json:"uri,omitempty"`
Token string `json:"token,omitempty"`
KeyAuthorization string `json:"keyAuthorization,omitempty"`
TLS bool `json:"tls,omitempty"`
Iterations int `json:"n,omitempty"`
Error RemoteError `json:"error,omitempty"`
ValidationRecords []validationRecord `json:"validationRecord,omitempty"`
}
type csrMessage struct {
Resource string `json:"resource,omitempty"`
Csr string `json:"csr"`
Authorizations []string `json:"authorizations"`
}
type revokeCertMessage struct {
Resource string `json:"resource"`
Certificate string `json:"certificate"`
}
type deactivateAuthMessage struct {
Resource string `json:"resource,omitempty"`
Status string `jsom:"status"`
}
// CertificateResource represents a CA issued certificate.
// PrivateKey, Certificate and IssuerCertificate are all
// already PEM encoded and can be directly written to disk.
// Certificate may be a certificate bundle, depending on the
// options supplied to create it.
type CertificateResource struct {
Domain string `json:"domain"`
CertURL string `json:"certUrl"`
CertStableURL string `json:"certStableUrl"`
AccountRef string `json:"accountRef,omitempty"`
PrivateKey []byte `json:"-"`
Certificate []byte `json:"-"`
IssuerCertificate []byte `json:"-"`
CSR []byte `json:"-"`
}

View file

@ -1 +0,0 @@
package acme

View file

@ -1,67 +0,0 @@
package acme
import (
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"log"
)
type tlsSNIChallenge struct {
jws *jws
validate validateFunc
provider ChallengeProvider
}
func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
// FIXME: https://github.com/ietf-wg-acme/acme/pull/22
// Currently we implement this challenge to track boulder, not the current spec!
logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
if err != nil {
return err
}
err = t.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
}
defer func() {
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Printf("[%s] error cleaning up: %v", domain, err)
}
}()
return t.validate(t.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}
// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge
func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, string, error) {
// generate a new RSA key for the certificates
tempPrivKey, err := generatePrivateKey(RSA2048)
if err != nil {
return tls.Certificate{}, "", err
}
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
rsaPrivPEM := pemEncode(rsaPrivKey)
zBytes := sha256.Sum256([]byte(keyAuth))
z := hex.EncodeToString(zBytes[:sha256.Size])
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
if err != nil {
return tls.Certificate{}, "", err
}
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
if err != nil {
return tls.Certificate{}, "", err
}
return certificate, domain, nil
}

View file

@ -1,62 +0,0 @@
package acme
import (
"crypto/tls"
"fmt"
"net"
"net/http"
)
// TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge
// It may be instantiated without using the NewTLSProviderServer function if
// you want only to use the default values.
type TLSProviderServer struct {
iface string
port string
done chan bool
listener net.Listener
}
// NewTLSProviderServer creates a new TLSProviderServer on the selected interface and port.
// Setting iface and / or port to an empty string will make the server fall back to
// the "any" interface and port 443 respectively.
func NewTLSProviderServer(iface, port string) *TLSProviderServer {
return &TLSProviderServer{iface: iface, port: port}
}
// Present makes the keyAuth available as a cert
func (s *TLSProviderServer) Present(domain, token, keyAuth string) error {
if s.port == "" {
s.port = "443"
}
cert, _, err := TLSSNI01ChallengeCert(keyAuth)
if err != nil {
return err
}
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{cert}
s.listener, err = tls.Listen("tcp", net.JoinHostPort(s.iface, s.port), tlsConf)
if err != nil {
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
}
s.done = make(chan bool)
go func() {
http.Serve(s.listener, nil)
s.done <- true
}()
return nil
}
// CleanUp closes the HTTP server.
func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error {
if s.listener == nil {
return nil
}
s.listener.Close()
<-s.done
return nil
}

View file

@ -1,65 +0,0 @@
package acme
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"strings"
"testing"
)
func TestTLSSNIChallenge(t *testing.T) {
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
j := &jws{privKey: privKey}
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"}
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{
InsecureSkipVerify: true,
})
if err != nil {
t.Errorf("Expected to connect to challenge server without an error. %s", err.Error())
}
// Expect the server to only return one certificate
connState := conn.ConnectionState()
if count := len(connState.PeerCertificates); count != 1 {
t.Errorf("Expected the challenge server to return exactly one certificate but got %d", count)
}
remoteCert := connState.PeerCertificates[0]
if count := len(remoteCert.DNSNames); count != 1 {
t.Errorf("Expected the challenge certificate to have exactly one DNSNames entry but had %d", count)
}
zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))
z := hex.EncodeToString(zBytes[:sha256.Size])
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
if remoteCert.DNSNames[0] != domain {
t.Errorf("Expected the challenge certificate DNSName to match %s but was %s", domain, remoteCert.DNSNames[0])
}
return nil
}
solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &TLSProviderServer{port: "23457"}}
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
t.Errorf("Solve error: got %v, want nil", err)
}
}
func TestTLSSNIChallengeInvalidPort(t *testing.T) {
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
j := &jws{privKey: privKey}
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"}
solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}}
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
t.Errorf("Solve error: got %v, want error", err)
} else if want, want18 := "invalid port 123456", "123456: invalid port"; !strings.HasSuffix(err.Error(), want) && !strings.HasSuffix(err.Error(), want18) {
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
}
}

View file

@ -1,29 +0,0 @@
package acme
import (
"fmt"
"time"
)
// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'.
func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error {
var lastErr string
timeup := time.After(timeout)
for {
select {
case <-timeup:
return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr)
default:
}
stop, err := f()
if stop {
return nil
}
if err != nil {
lastErr = err.Error()
}
time.Sleep(interval)
}
}

10
buildx.Dockerfile Normal file
View file

@ -0,0 +1,10 @@
# syntax=docker/dockerfile:1.4
FROM alpine:3
RUN apk --no-cache --no-progress add git ca-certificates tzdata \
&& rm -rf /var/cache/apk/*
COPY lego /
ENTRYPOINT ["/lego"]
EXPOSE 80

320
certcrypto/crypto.go Normal file
View file

@ -0,0 +1,320 @@
package certcrypto
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"strings"
"time"
"golang.org/x/crypto/ocsp"
)
// Constants for all key types we support.
const (
EC256 = KeyType("P256")
EC384 = KeyType("P384")
RSA2048 = KeyType("2048")
RSA3072 = KeyType("3072")
RSA4096 = KeyType("4096")
RSA8192 = KeyType("8192")
)
const (
// OCSPGood means that the certificate is valid.
OCSPGood = ocsp.Good
// OCSPRevoked means that the certificate has been deliberately revoked.
OCSPRevoked = ocsp.Revoked
// OCSPUnknown means that the OCSP responder doesn't know about the certificate.
OCSPUnknown = ocsp.Unknown
// OCSPServerFailed means that the OCSP responder failed to process the request.
OCSPServerFailed = ocsp.ServerFailed
)
// Constants for OCSP must staple.
var (
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05}
)
// KeyType represents the key algo as well as the key size or curve to use.
type KeyType string
type DERCertificateBytes []byte
// ParsePEMBundle parses a certificate bundle from top to bottom and returns
// a slice of x509 certificates. This function will error if no certificates are found.
func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
var certificates []*x509.Certificate
var certDERBlock *pem.Block
for {
certDERBlock, bundle = pem.Decode(bundle)
if certDERBlock == nil {
break
}
if certDERBlock.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(certDERBlock.Bytes)
if err != nil {
return nil, err
}
certificates = append(certificates, cert)
}
}
if len(certificates) == 0 {
return nil, errors.New("no certificates were found while parsing the bundle")
}
return certificates, nil
}
// ParsePEMPrivateKey parses a private key from key, which is a PEM block.
// Borrowed from Go standard library, to handle various private key and PEM block types.
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308
// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238)
func ParsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
keyBlockDER, _ := pem.Decode(key)
if keyBlockDER == nil {
return nil, fmt.Errorf("invalid PEM block")
}
if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") {
return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type)
}
if key, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
return key, nil
default:
return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key)
}
}
if key, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil {
return key, nil
}
return nil, errors.New("failed to parse private key")
}
func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
switch keyType {
case EC256:
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case EC384:
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case RSA2048:
return rsa.GenerateKey(rand.Reader, 2048)
case RSA3072:
return rsa.GenerateKey(rand.Reader, 3072)
case RSA4096:
return rsa.GenerateKey(rand.Reader, 4096)
case RSA8192:
return rsa.GenerateKey(rand.Reader, 8192)
}
return nil, fmt.Errorf("invalid KeyType: %s", keyType)
}
func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
var dnsNames []string
var ipAddresses []net.IP
for _, altname := range san {
if ip := net.ParseIP(altname); ip != nil {
ipAddresses = append(ipAddresses, ip)
} else {
dnsNames = append(dnsNames, altname)
}
}
template := x509.CertificateRequest{
Subject: pkix.Name{CommonName: domain},
DNSNames: dnsNames,
IPAddresses: ipAddresses,
}
if mustStaple {
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
Id: tlsFeatureExtensionOID,
Value: ocspMustStapleFeature,
})
}
return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
}
func PEMEncode(data interface{}) []byte {
return pem.EncodeToMemory(PEMBlock(data))
}
func PEMBlock(data interface{}) *pem.Block {
var pemBlock *pem.Block
switch key := data.(type) {
case *ecdsa.PrivateKey:
keyBytes, _ := x509.MarshalECPrivateKey(key)
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
case *rsa.PrivateKey:
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
case *x509.CertificateRequest:
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
case DERCertificateBytes:
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(DERCertificateBytes))}
}
return pemBlock
}
func pemDecode(data []byte) (*pem.Block, error) {
pemBlock, _ := pem.Decode(data)
if pemBlock == nil {
return nil, errors.New("PEM decode did not yield a valid block. Is the certificate in the right format?")
}
return pemBlock, nil
}
func PemDecodeTox509CSR(data []byte) (*x509.CertificateRequest, error) {
pemBlock, err := pemDecode(data)
if pemBlock == nil {
return nil, err
}
if pemBlock.Type != "CERTIFICATE REQUEST" && pemBlock.Type != "NEW CERTIFICATE REQUEST" {
return nil, errors.New("PEM block is not a certificate request")
}
return x509.ParseCertificateRequest(pemBlock.Bytes)
}
// ParsePEMCertificate returns Certificate from a PEM encoded certificate.
// The certificate has to be PEM encoded. Any other encodings like DER will fail.
func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) {
pemBlock, err := pemDecode(cert)
if pemBlock == nil {
return nil, err
}
// from a DER encoded certificate
return x509.ParseCertificate(pemBlock.Bytes)
}
func ExtractDomains(cert *x509.Certificate) []string {
var domains []string
if cert.Subject.CommonName != "" {
domains = append(domains, cert.Subject.CommonName)
}
// Check for SAN certificate
for _, sanDomain := range cert.DNSNames {
if sanDomain == cert.Subject.CommonName {
continue
}
domains = append(domains, sanDomain)
}
commonNameIP := net.ParseIP(cert.Subject.CommonName)
for _, sanIP := range cert.IPAddresses {
if !commonNameIP.Equal(sanIP) {
domains = append(domains, sanIP.String())
}
}
return domains
}
func ExtractDomainsCSR(csr *x509.CertificateRequest) []string {
var domains []string
if csr.Subject.CommonName != "" {
domains = append(domains, csr.Subject.CommonName)
}
// loop over the SubjectAltName DNS names
for _, sanName := range csr.DNSNames {
if containsSAN(domains, sanName) {
// Duplicate; skip this name
continue
}
// Name is unique
domains = append(domains, sanName)
}
cnip := net.ParseIP(csr.Subject.CommonName)
for _, sanIP := range csr.IPAddresses {
if !cnip.Equal(sanIP) {
domains = append(domains, sanIP.String())
}
}
return domains
}
func containsSAN(domains []string, sanName string) bool {
for _, existingName := range domains {
if existingName == sanName {
return true
}
}
return false
}
func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {
derBytes, err := generateDerCert(privateKey, time.Time{}, domain, extensions)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
}
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
}
if expiration.IsZero() {
expiration = time.Now().AddDate(1, 0, 0)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "ACME Challenge TEMP",
},
NotBefore: time.Now(),
NotAfter: expiration,
KeyUsage: x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
ExtraExtensions: extensions,
}
// handling SAN filling as type suspected
if ip := net.ParseIP(domain); ip != nil {
template.IPAddresses = []net.IP{ip}
} else {
template.DNSNames = []string{domain}
}
return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
}

177
certcrypto/crypto_test.go Normal file
View file

@ -0,0 +1,177 @@
package certcrypto
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"encoding/pem"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGeneratePrivateKey(t *testing.T) {
key, err := GeneratePrivateKey(RSA2048)
require.NoError(t, err, "Error generating private key")
assert.NotNil(t, key)
}
func TestGenerateCSR(t *testing.T) {
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Error generating private key")
type expected struct {
len int
error bool
}
testCases := []struct {
desc string
privateKey crypto.PrivateKey
domain string
san []string
mustStaple bool
expected expected
}{
{
desc: "without SAN",
privateKey: privateKey,
domain: "lego.acme",
mustStaple: true,
expected: expected{len: 245},
},
{
desc: "without SAN",
privateKey: privateKey,
domain: "lego.acme",
san: []string{},
mustStaple: true,
expected: expected{len: 245},
},
{
desc: "with SAN",
privateKey: privateKey,
domain: "lego.acme",
san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
mustStaple: true,
expected: expected{len: 296},
},
{
desc: "no domain",
privateKey: privateKey,
domain: "",
mustStaple: true,
expected: expected{len: 225},
},
{
desc: "no domain with SAN",
privateKey: privateKey,
domain: "",
san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"},
mustStaple: true,
expected: expected{len: 276},
},
{
desc: "private key nil",
privateKey: nil,
domain: "fizz.buzz",
mustStaple: true,
expected: expected{error: true},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
csr, err := GenerateCSR(test.privateKey, test.domain, test.san, test.mustStaple)
if test.expected.error {
require.Error(t, err)
} else {
require.NoError(t, err, "Error generating CSR")
assert.NotEmpty(t, csr)
assert.Len(t, csr, test.expected.len)
}
})
}
}
func TestPEMEncode(t *testing.T) {
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)
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) {
privateKey, err := GeneratePrivateKey(RSA2048)
require.NoError(t, err, "Error generating private key")
expiration := time.Now().Add(365).Round(time.Second)
certBytes, err := generateDerCert(privateKey.(*rsa.PrivateKey), expiration, "test.com", nil)
require.NoError(t, err, "Error generating cert")
buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
// Some random string should return an error.
cert, err := ParsePEMCertificate(buf.Bytes())
require.Errorf(t, err, "returned %v", cert)
// A DER encoded certificate should return an error.
_, err = ParsePEMCertificate(certBytes)
require.Error(t, err, "Expected to return an error for DER certificates")
// A PEM encoded certificate should work ok.
pemCert := PEMEncode(DERCertificateBytes(certBytes))
cert, err = ParsePEMCertificate(pemCert)
require.NoError(t, err)
assert.Equal(t, expiration.UTC(), cert.NotAfter)
}
func TestParsePEMPrivateKey(t *testing.T) {
privateKey, err := GeneratePrivateKey(RSA2048)
require.NoError(t, err, "Error generating private key")
pemPrivateKey := PEMEncode(privateKey)
// Decoding a key should work and create an identical key to the original
decoded, err := ParsePEMPrivateKey(pemPrivateKey)
require.NoError(t, err)
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"}))
require.Errorf(t, err, "Expected to return an error for non-private key input")
// Decoding a PEM block that doesn't actually contain a key should error
_, err = ParsePEMPrivateKey(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY"}))
require.Errorf(t, err, "Expected to return an error for empty input")
// Decoding non-PEM input should return an error
_, 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

@ -0,0 +1,81 @@
package certificate
import (
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/log"
)
const (
// overallRequestLimit is the overall number of request per second
// limited on the "new-reg", "new-authz" and "new-cert" endpoints.
// From the documentation the limitation is 20 requests per second,
// but using 20 as value doesn't work but 18 do.
overallRequestLimit = 18
)
func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) {
resc, errc := make(chan acme.Authorization), make(chan domainError)
delay := time.Second / overallRequestLimit
for _, authzURL := range order.Authorizations {
time.Sleep(delay)
go func(authzURL string) {
authz, err := c.core.Authorizations.Get(authzURL)
if err != nil {
errc <- domainError{Domain: authz.Identifier.Value, Error: err}
return
}
resc <- authz
}(authzURL)
}
var responses []acme.Authorization
failures := make(obtainError)
for i := 0; i < len(order.Authorizations); i++ {
select {
case res := <-resc:
responses = append(responses, res)
case err := <-errc:
failures[err.Domain] = err.Error
}
}
for i, auth := range order.Authorizations {
log.Infof("[%s] AuthURL: %s", order.Identifiers[i].Value, auth)
}
close(resc)
close(errc)
// be careful to not return an empty failures map;
// even if empty, they become non-nil error values
if len(failures) > 0 {
return responses, failures
}
return responses, nil
}
func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force bool) {
for _, authzURL := range order.Authorizations {
auth, err := c.core.Authorizations.Get(authzURL)
if err != nil {
log.Infof("Unable to get the authorization for: %s", authzURL)
continue
}
if auth.Status == acme.StatusValid && !force {
log.Infof("Skipping deactivating of valid auth: %s", authzURL)
continue
}
log.Infof("Deactivating auth: %s", authzURL)
if c.core.Authorizations.Deactivate(authzURL) != nil {
log.Infof("Unable to deactivate the authorization: %s", authzURL)
}
}
}

662
certificate/certificates.go Normal file
View file

@ -0,0 +1,662 @@
package certificate
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/wait"
"golang.org/x/crypto/ocsp"
"golang.org/x/net/idna"
)
// maxBodySize is the maximum size of body that we will read.
const maxBodySize = 1024 * 1024
// Resource represents a CA issued certificate.
// PrivateKey, Certificate and IssuerCertificate are all
// already PEM encoded and can be directly written to disk.
// Certificate may be a certificate bundle,
// depending on the options supplied to create it.
type Resource struct {
Domain string `json:"domain"`
CertURL string `json:"certUrl"`
CertStableURL string `json:"certStableUrl"`
PrivateKey []byte `json:"-"`
Certificate []byte `json:"-"`
IssuerCertificate []byte `json:"-"`
CSR []byte `json:"-"`
}
// ObtainRequest The request to obtain certificate.
//
// The first domain in domains is used for the CommonName field of the certificate,
// all other domains are added using the Subject Alternate Names extension.
//
// A new private key is generated for every invocation of the function Obtain.
// If you do not want that you can supply your own private key in the privateKey parameter.
// If this parameter is non-nil it will be used instead of generating a new one.
//
// If `Bundle` is true, the `[]byte` contains both the issuer certificate and your issued certificate as a bundle.
//
// 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
NotBefore time.Time
NotAfter time.Time
Bundle bool
PreferredChain string
AlwaysDeactivateAuthorizations bool
}
// ObtainForCSRRequest The request to obtain a certificate matching the CSR passed into it.
//
// If `Bundle` is true, the `[]byte` contains both the issuer certificate and your issued certificate as a bundle.
//
// 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 ObtainForCSRRequest struct {
CSR *x509.CertificateRequest
NotBefore time.Time
NotAfter time.Time
Bundle bool
PreferredChain string
AlwaysDeactivateAuthorizations bool
}
type resolver interface {
Solve(authorizations []acme.Authorization) error
}
type CertifierOptions struct {
KeyType certcrypto.KeyType
Timeout time.Duration
}
// Certifier A service to obtain/renew/revoke certificates.
type Certifier struct {
core *api.Core
resolver resolver
options CertifierOptions
}
// NewCertifier creates a Certifier.
func NewCertifier(core *api.Core, resolver resolver, options CertifierOptions) *Certifier {
return &Certifier{
core: core,
resolver: resolver,
options: options,
}
}
// Obtain tries to obtain a single certificate using all domains passed into it.
//
// This function will never return a partial certificate.
// If one domain in the list fails, the whole certificate will fail.
func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
if len(request.Domains) == 0 {
return nil, errors.New("no domains to obtain a certificate for")
}
domains := sanitizeDomain(request.Domains)
if request.Bundle {
log.Infof("[%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
} else {
log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
}
orderOpts := &api.OrderOptions{
NotBefore: request.NotBefore,
NotAfter: request.NotAfter,
}
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
if err != nil {
return nil, err
}
authz, err := c.getAuthorizations(order)
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations)
return nil, err
}
err = c.resolver.Solve(authz)
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations)
return nil, err
}
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := make(obtainError)
cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain)
if err != nil {
for _, auth := range authz {
failures[challenge.GetTargetedDomain(auth)] = err
}
}
if request.AlwaysDeactivateAuthorizations {
c.deactivateAuthorizations(order, true)
}
// Do not return an empty failures map, because
// it would still be a non-nil error value
if len(failures) > 0 {
return cert, failures
}
return cert, nil
}
// ObtainForCSR tries to obtain a certificate matching the CSR passed into it.
//
// The domains are inferred from the CommonName and SubjectAltNames, if any.
// The private key for this CSR is not required.
//
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
//
// This function will never return a partial certificate.
// If one domain in the list fails, the whole certificate will fail.
func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error) {
if request.CSR == nil {
return nil, errors.New("cannot obtain resource for CSR: CSR is missing")
}
// figure out what domains it concerns
// start with the common name
domains := certcrypto.ExtractDomainsCSR(request.CSR)
if request.Bundle {
log.Infof("[%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
} else {
log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
}
orderOpts := &api.OrderOptions{
NotBefore: request.NotBefore,
NotAfter: request.NotAfter,
}
order, err := c.core.Orders.NewWithOptions(domains, orderOpts)
if err != nil {
return nil, err
}
authz, err := c.getAuthorizations(order)
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations)
return nil, err
}
err = c.resolver.Solve(authz)
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations)
return nil, err
}
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := make(obtainError)
cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, nil, request.PreferredChain)
if err != nil {
for _, auth := range authz {
failures[challenge.GetTargetedDomain(auth)] = err
}
}
if request.AlwaysDeactivateAuthorizations {
c.deactivateAuthorizations(order, true)
}
if cert != nil {
// Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = certcrypto.PEMEncode(request.CSR)
}
// Do not return an empty failures map,
// because it would still be a non-nil error value
if len(failures) > 0 {
return cert, failures
}
return cert, nil
}
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
}
}
// Determine certificate name(s) based on the authorization resources
commonName := domains[0]
// RFC8555 Section 7.4 "Applying for Certificate Issuance"
// https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4
// says:
// Clients SHOULD NOT make any assumptions about the sort order of
// "identifiers" or "authorizations" elements in the returned order
// object.
san := []string{commonName}
for _, auth := range order.Identifiers {
if auth.Value != commonName {
san = append(san, auth.Value)
}
}
// 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, bundle, csr, certcrypto.PEMEncode(privateKey), preferredChain)
}
func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) {
respOrder, err := c.core.Orders.UpdateForCSR(order.Finalize, csr)
if err != nil {
return nil, err
}
commonName := domains[0]
certRes := &Resource{
Domain: commonName,
CertURL: respOrder.Certificate,
PrivateKey: privateKeyPem,
}
if respOrder.Status == acme.StatusValid {
// if the certificate is available right away, short cut!
ok, errR := c.checkResponse(respOrder, certRes, bundle, preferredChain)
if errR != nil {
return nil, errR
}
if ok {
return certRes, nil
}
}
timeout := c.options.Timeout
if c.options.Timeout <= 0 {
timeout = 30 * time.Second
}
err = wait.For("certificate", timeout, timeout/60, func() (bool, error) {
ord, errW := c.core.Orders.Get(order.Location)
if errW != nil {
return false, errW
}
done, errW := c.checkResponse(ord, certRes, bundle, preferredChain)
if errW != nil {
return false, errW
}
return done, nil
})
return certRes, err
}
// checkResponse checks to see if the certificate is ready and a link is contained in the response.
//
// If so, loads it into certRes and returns true.
// If the cert is not yet ready, it returns false.
//
// The certRes input should already have the Domain (common name) field populated.
//
// If bundle is true, the certificate will be bundled with the issuer's cert.
func (c *Certifier) checkResponse(order acme.ExtendedOrder, certRes *Resource, bundle bool, preferredChain string) (bool, error) {
valid, err := checkOrderStatus(order)
if err != nil || !valid {
return valid, err
}
certs, err := c.core.Certificates.GetAll(order.Certificate, bundle)
if err != nil {
return false, err
}
// Set the default certificate
certRes.IssuerCertificate = certs[order.Certificate].Issuer
certRes.Certificate = certs[order.Certificate].Cert
certRes.CertURL = order.Certificate
certRes.CertStableURL = order.Certificate
if preferredChain == "" {
log.Infof("[%s] Server responded with a certificate.", certRes.Domain)
return true, nil
}
for link, cert := range certs {
ok, err := hasPreferredChain(cert.Issuer, preferredChain)
if err != nil {
return false, err
}
if ok {
log.Infof("[%s] Server responded with a certificate for the preferred certificate chains %q.", certRes.Domain, preferredChain)
certRes.IssuerCertificate = cert.Issuer
certRes.Certificate = cert.Cert
certRes.CertURL = link
certRes.CertStableURL = link
return true, nil
}
}
log.Infof("lego has been configured to prefer certificate chains with issuer %q, but no chain from the CA matched this issuer. Using the default certificate chain instead.", preferredChain)
return true, nil
}
// Revoke takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
func (c *Certifier) Revoke(cert []byte) error {
return c.RevokeWithReason(cert, nil)
}
// RevokeWithReason takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
func (c *Certifier) RevokeWithReason(cert []byte, reason *uint) error {
certificates, err := certcrypto.ParsePEMBundle(cert)
if err != nil {
return err
}
x509Cert := certificates[0]
if x509Cert.IsCA {
return errors.New("certificate bundle starts with a CA certificate")
}
revokeMsg := acme.RevokeCertMessage{
Certificate: base64.RawURLEncoding.EncodeToString(x509Cert.Raw),
Reason: reason,
}
return c.core.Certificates.Revoke(revokeMsg)
}
// RenewOptions options used by Certifier.RenewWithOptions.
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
AlwaysDeactivateAuthorizations bool
// Not supported for CSR request.
MustStaple bool
}
// Renew takes a Resource and tries to renew the certificate.
//
// If the renewal process succeeds, the new certificate will be returned in a new CertResource.
// Please be aware that this function will return a new certificate in ANY case that is not an error.
// If the server does not provide us with a new cert on a GET request to the CertURL
// this function will start a new-cert flow where a new certificate gets generated.
//
// 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{
Bundle: bundle,
PreferredChain: preferredChain,
MustStaple: mustStaple,
})
}
// RenewWithOptions takes a Resource and tries to renew the certificate.
//
// If the renewal process succeeds, the new certificate will be returned in a new CertResource.
// Please be aware that this function will return a new certificate in ANY case that is not an error.
// If the server does not provide us with a new cert on a GET request to the CertURL
// this function will start a new-cert flow where a new certificate gets generated.
//
// 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.
func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*Resource, error) {
// Input certificate is PEM encoded.
// Decode it here as we may need the decoded cert later on in the renewal process.
// The input may be a bundle or a single certificate.
certificates, err := certcrypto.ParsePEMBundle(certRes.Certificate)
if err != nil {
return nil, err
}
x509Cert := certificates[0]
if x509Cert.IsCA {
return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", certRes.Domain)
}
// This is just meant to be informal for the user.
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
log.Infof("[%s] acme: Trying renewal with %d hours remaining", certRes.Domain, int(timeLeft.Hours()))
// We always need to request a new certificate to renew.
// Start by checking to see if the certificate was based off a CSR,
// and use that if it's defined.
if len(certRes.CSR) > 0 {
csr, errP := certcrypto.PemDecodeTox509CSR(certRes.CSR)
if errP != nil {
return nil, errP
}
request := ObtainForCSRRequest{CSR: csr}
if options != nil {
request.NotBefore = options.NotBefore
request.NotAfter = options.NotAfter
request.Bundle = options.Bundle
request.PreferredChain = options.PreferredChain
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
}
return c.ObtainForCSR(request)
}
var privateKey crypto.PrivateKey
if certRes.PrivateKey != nil {
privateKey, err = certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)
if err != nil {
return nil, err
}
}
request := ObtainRequest{
Domains: certcrypto.ExtractDomains(x509Cert),
PrivateKey: privateKey,
}
if options != nil {
request.MustStaple = options.MustStaple
request.NotBefore = options.NotBefore
request.NotAfter = options.NotAfter
request.Bundle = options.Bundle
request.PreferredChain = options.PreferredChain
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
}
return c.Obtain(request)
}
// GetOCSP takes a PEM encoded cert or cert bundle returning the raw OCSP response,
// the parsed response, and an error, if any.
//
// The returned []byte can be passed directly into the OCSPStaple property of a tls.Certificate.
// If the bundle only contains the issued certificate,
// this function will try to get the issuer certificate from the IssuingCertificateURL in the certificate.
//
// If the []byte and/or ocsp.Response return values are nil, the OCSP status may be assumed OCSPUnknown.
func (c *Certifier) GetOCSP(bundle []byte) ([]byte, *ocsp.Response, error) {
certificates, err := certcrypto.ParsePEMBundle(bundle)
if err != nil {
return nil, nil, err
}
// We expect the certificate slice to be ordered downwards the chain.
// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
// which should always be the first two certificates.
// If there's no OCSP server listed in the leaf cert, there's nothing to do.
// And if we have only one certificate so far, we need to get the issuer cert.
issuedCert := certificates[0]
if len(issuedCert.OCSPServer) == 0 {
return nil, nil, errors.New("no OCSP server specified in cert")
}
if len(certificates) == 1 {
// TODO: build fallback. If this fails, check the remaining array entries.
if len(issuedCert.IssuingCertificateURL) == 0 {
return nil, nil, errors.New("no issuing certificate URL")
}
resp, errC := c.core.HTTPClient.Get(issuedCert.IssuingCertificateURL[0])
if errC != nil {
return nil, nil, errC
}
defer resp.Body.Close()
issuerBytes, errC := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if errC != nil {
return nil, nil, errC
}
issuerCert, errC := x509.ParseCertificate(issuerBytes)
if errC != nil {
return nil, nil, errC
}
// Insert it into the slice on position 0
// We want it ordered right SRV CRT -> CA
certificates = append(certificates, issuerCert)
}
issuerCert := certificates[1]
// Finally kick off the OCSP request.
ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
if err != nil {
return nil, nil, err
}
resp, err := c.core.HTTPClient.Post(issuedCert.OCSPServer[0], "application/ocsp-request", bytes.NewReader(ocspReq))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
ocspResBytes, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if err != nil {
return nil, nil, err
}
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
if err != nil {
return nil, nil, err
}
return ocspResBytes, ocspRes, nil
}
// Get attempts to fetch the certificate at the supplied URL.
// The URL is the same as what would normally be supplied at the Resource's CertURL.
//
// The returned Resource will not have the PrivateKey and CSR fields populated as these will not be available.
//
// If bundle is true, the Certificate field in the returned Resource includes the issuer certificate.
func (c *Certifier) Get(url string, bundle bool) (*Resource, error) {
cert, issuer, err := c.core.Certificates.Get(url, bundle)
if err != nil {
return nil, err
}
// Parse the returned cert bundle so that we can grab the domain from the common name.
x509Certs, err := certcrypto.ParsePEMBundle(cert)
if err != nil {
return nil, err
}
return &Resource{
Domain: x509Certs[0].Subject.CommonName,
Certificate: cert,
IssuerCertificate: issuer,
CertURL: url,
CertStableURL: url,
}, nil
}
func hasPreferredChain(issuer []byte, preferredChain string) (bool, error) {
certs, err := certcrypto.ParsePEMBundle(issuer)
if err != nil {
return false, err
}
topCert := certs[len(certs)-1]
if topCert.Issuer.CommonName == preferredChain {
return true, nil
}
return false, nil
}
func checkOrderStatus(order acme.ExtendedOrder) (bool, error) {
switch order.Status {
case acme.StatusValid:
return true, nil
case acme.StatusInvalid:
return false, order.Error
default:
return false, nil
}
}
// https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4
// The domain name MUST be encoded in the form in which it would appear in a certificate.
// That is, it MUST be encoded according to the rules in Section 7 of [RFC5280].
//
// 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 {
log.Infof("skip domain %q: unable to sanitize (punnycode): %v", domain, err)
} else {
sanitizedDomains = append(sanitizedDomains, sanitizedDomain)
}
}
return sanitizedDomains
}

View file

@ -0,0 +1,398 @@
package certificate
import (
"crypto/rand"
"crypto/rsa"
"encoding/pem"
"fmt"
"net/http"
"testing"
"github.com/go-acme/lego/v4/acme"
"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/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const certResponseNoBundleMock = `-----BEGIN CERTIFICATE-----
MIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD
Ex1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa
Fw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag
bxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5
y3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy
144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3
BJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE
zcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO
BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG
A1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD
ggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4
jXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9
IDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE
HBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd
TqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri
OPPkKtAKAbQkKbUIfsHpBZjKZMU=
-----END CERTIFICATE-----
`
const certResponseMock = `-----BEGIN CERTIFICATE-----
MIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD
Ex1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa
Fw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag
bxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5
y3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy
144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3
BJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE
zcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO
BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG
A1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD
ggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4
jXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9
IDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE
HBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd
TqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri
OPPkKtAKAbQkKbUIfsHpBZjKZMU=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw
NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl
NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT
SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh
0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen
SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx
HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt
D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu
mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD
AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm
iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd
QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ
wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv
rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2
7R4IbHGnj0BJA2vMYC4hSw==
-----END CERTIFICATE-----
`
const issuerMock = `-----BEGIN CERTIFICATE-----
MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw
NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl
NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT
SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh
0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen
SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx
HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt
D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu
mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD
AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm
iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd
QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ
wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv
rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2
7R4IbHGnj0BJA2vMYC4hSw==
-----END CERTIFICATE-----
`
const certResponseMock2 = `
-----BEGIN CERTIFICATE-----
MIIFUzCCBDugAwIBAgISA/z9btaZCSo/qlVwmJrHpoyPMA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA3MjUwNjUxNDRaFw0y
MDEwMjMwNjUxNDRaMBgxFjAUBgNVBAMTDW5hdHVyZS5nbG9iYWwwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN/PF8lWub3i+lO3CLl/HJAM86pQH9hWej
Whci1PPNzKyEByJq2psNLCO1W1mXK3ClWSyifptCf7+AAFAOoBojPMwjaKMziw1M
BxAQiX8MzZLv4Hr4Uk08cQX31QHiEpOv4pMHqB0UpodTYY10dZnDdyJHaGKzxfJh
nQPYIVto+UegcVu9iZIDow7ugoT2Gh8nB8jOAc4wtBgmylgeAFmYR6QZ4PYSYFh0
DLZGGB1WuU/4YC5OciwTDv5EiqP3KM3NdkmGhPY0A3jcTrjN+HhcE4pYBtG1wHi8
PEuqqKyCLa3AjHq4WrZyCCkCMXPbIDS1Qt7botDmUZr/26xJZnl5AgMBAAGjggJj
MIICXzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFFm72Cv7LnjVhcLqUujrykUr70lF
MB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMw
YTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9y
ZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9y
Zy8wGAYDVR0RBBEwD4INbmF0dXJlLmdsb2JhbDBMBgNVHSAERTBDMAgGBmeBDAEC
ATA3BgsrBgEEAYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNl
bmNyeXB0Lm9yZzCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB3ALIeBcyLos2KIE6H
ZvkruYolIGdr2vpw57JJUy3vi5BeAAABc4T006IAAAQDAEgwRgIhAPEEvCEMkekD
8XLDaxHPnJ85UZL72JqGgNK+7I/NdFNuAiEA5D78b4V1YsD8wvWz/sk6Ks8VgjED
eKGl/TyXwKEpzEIAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZEwAA
AXOE9NPrAAAEAwBHMEUCIAu4YFfGZIN/P+0eRG0krSddHKCSf6rqr6aVqUWkJY3F
AiEAz0HkTe0alED1gW9nEAJ1qqK1MLMjRM8SsUv9Is86+CwwDQYJKoZIhvcNAQEL
BQADggEBAGriSVi9YuBnm50w84gjlinmeGdvxgugblIoEqKoXd3d5/zx0DvW9Tm6
YGfXsvAJUSCag7dZ/s/PEu23jKNdFoaBmDaUHHKnUwbWWF7/ptYZ+YuDVGOJo8PL
CULNfUMon20rPU9smzW4BFDBZ6KmX/r4Q8cQ7FLOqKdcng0yMcqIfq4cBxEvd0uQ
pHR3AwCjAIGpV6Q9WHHiHx+SEd/Xc18Z5pXa9m3Rz4i6Mfv+AYLtnsZDxcH81cVM
7rYp80vhXM9tFd4wyrqLuaVZgYD1ylxTYpTI7sijIq4Sl984f3IPA/olN+zK6E8d
EbiufIcKeju/aSellDzzBabEo80YT4o=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----
`
const issuerMock2 = `-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE-----
`
func Test_checkResponse(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(t)
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(certResponseMock))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
order := acme.ExtendedOrder{
Order: acme.Order{
Status: acme.StatusValid,
Certificate: apiURL + "/certificate",
},
}
certRes := &Resource{}
valid, err := certifier.checkResponse(order, certRes, true, "")
require.NoError(t, err)
assert.True(t, valid)
assert.NotNil(t, certRes)
assert.Equal(t, "", certRes.Domain)
assert.Contains(t, certRes.CertStableURL, "/certificate")
assert.Contains(t, certRes.CertURL, "/certificate")
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_checkResponse_issuerRelUp(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(t)
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`)
_, err := w.Write([]byte(certResponseMock))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
mux.HandleFunc("/issuer", func(w http.ResponseWriter, _ *http.Request) {
p, _ := pem.Decode([]byte(issuerMock))
_, err := w.Write(p.Bytes)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
order := acme.ExtendedOrder{
Order: acme.Order{
Status: acme.StatusValid,
Certificate: apiURL + "/certificate",
},
}
certRes := &Resource{}
valid, err := certifier.checkResponse(order, certRes, true, "")
require.NoError(t, err)
assert.True(t, valid)
assert.NotNil(t, certRes)
assert.Equal(t, "", certRes.Domain)
assert.Contains(t, certRes.CertStableURL, "/certificate")
assert.Contains(t, certRes.CertURL, "/certificate")
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_checkResponse_no_bundle(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(t)
mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(certResponseMock))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
order := acme.ExtendedOrder{
Order: acme.Order{
Status: acme.StatusValid,
Certificate: apiURL + "/certificate",
},
}
certRes := &Resource{}
valid, err := certifier.checkResponse(order, certRes, false, "")
require.NoError(t, err)
assert.True(t, valid)
assert.NotNil(t, certRes)
assert.Equal(t, "", certRes.Domain)
assert.Contains(t, certRes.CertStableURL, "/certificate")
assert.Contains(t, certRes.CertURL, "/certificate")
assert.Nil(t, certRes.CSR)
assert.Nil(t, certRes.PrivateKey)
assert.Equal(t, certResponseNoBundleMock, string(certRes.Certificate), "Certificate")
assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate")
}
func Test_checkResponse_alternate(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(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(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
order := acme.ExtendedOrder{
Order: acme.Order{
Status: acme.StatusValid,
Certificate: apiURL + "/certificate",
},
}
certRes := &Resource{
Domain: "example.com",
}
valid, err := certifier.checkResponse(order, certRes, true, "DST Root CA X3")
require.NoError(t, err)
assert.True(t, valid)
assert.NotNil(t, certRes)
assert.Equal(t, "example.com", certRes.Domain)
assert.Contains(t, certRes.CertStableURL, "/certificate/1")
assert.Contains(t, certRes.CertURL, "/certificate/1")
assert.Nil(t, certRes.CSR)
assert.Nil(t, certRes.PrivateKey)
assert.Equal(t, certResponseMock2, string(certRes.Certificate), "Certificate")
assert.Equal(t, issuerMock2, string(certRes.IssuerCertificate), "IssuerCertificate")
}
func Test_Get(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(t)
mux.HandleFunc("/acme/cert/test-cert", func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(certResponseMock))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
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, 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")
}
type resolverMock struct {
error error
}
func (r *resolverMock) Solve(_ []acme.Authorization) error {
return r.error
}

30
certificate/errors.go Normal file
View file

@ -0,0 +1,30 @@
package certificate
import (
"bytes"
"fmt"
"sort"
)
// obtainError is returned when there are specific errors available per domain.
type obtainError map[string]error
func (e obtainError) Error() string {
buffer := bytes.NewBufferString("error: one or more domains had a problem:\n")
var domains []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()
}
type domainError struct {
Domain string
Error error
}

204
certificate/renewal.go Normal file
View file

@ -0,0 +1,204 @@
package certificate
import (
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"math/rand"
"strings"
"time"
"github.com/go-acme/lego/v4/acme"
)
// RenewalInfoRequest contains the necessary renewal information.
type RenewalInfoRequest struct {
Cert *x509.Certificate
Issuer *x509.Certificate
// HashName must be the string representation of a crypto.Hash constant in the golang.org/x/crypto package (e.g. "SHA-256").
// The correct value depends on the algorithm expected by the ACME server's ARI implementation.
HashName string
}
// RenewalInfoResponse is a wrapper around acme.RenewalInfoResponse that provides a method for determining when to renew a certificate.
type RenewalInfoResponse struct {
acme.RenewalInfoResponse
}
// ShouldRenewAt determines the optimal renewal time based on the current time (UTC),renewal window suggest by ARI, and the client's willingness to sleep.
// It returns a pointer to a time.Time value indicating when the renewal should be attempted or nil if deferred until the next normal wake time.
// This method implements the RECOMMENDED algorithm described in draft-ietf-acme-ari.
//
// - (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()
start := r.SuggestedWindow.Start.UTC()
end := r.SuggestedWindow.End.UTC()
// Select a uniform random time within the suggested window.
window := end.Sub(start)
randomDuration := time.Duration(rand.Int63n(int64(window)))
rt := start.Add(randomDuration)
// If the selected time is in the past, attempt renewal immediately.
if rt.Before(now) {
return &now
}
// Otherwise, if the client can schedule itself to attempt renewal at exactly the selected time, do so.
willingToSleepUntil := now.Add(willingToSleep)
if willingToSleepUntil.After(rt) || willingToSleepUntil.Equal(rt) {
return &rt
}
// TODO: Otherwise, if the selected time is before the next time that the client would wake up normally, attempt renewal immediately.
// Otherwise, sleep until the next normal wake time, re-check ARI, and return to Step 1.
return nil
}
// GetRenewalInfo sends a request to the ACME server's renewalInfo endpoint to obtain a suggested renewal window.
// The caller MUST provide the certificate and issuer certificate for the certificate they wish to renew.
// The caller should attempt to renew the certificate at the time indicated by the ShouldRenewAt method of the returned RenewalInfoResponse object.
//
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
//
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse, error) {
certID, err := makeCertID(req.Cert, req.Issuer, req.HashName)
if err != nil {
return nil, fmt.Errorf("error making certID: %w", err)
}
resp, err := c.core.Certificates.GetRenewalInfo(certID)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var info RenewalInfoResponse
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
return &info, nil
}
// UpdateRenewalInfo sends an update to the ACME server's renewal info endpoint to indicate that the client has successfully replaced a certificate.
// A certificate is considered replaced when its revocation would not disrupt any ongoing services,
// for instance because it has been renewed and the new certificate is in use, or because it is no longer in use.
//
// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.
// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.
//
// https://datatracker.ietf.org/doc/draft-ietf-acme-ari
func (c *Certifier) UpdateRenewalInfo(req RenewalInfoRequest) error {
certID, err := makeCertID(req.Cert, req.Issuer, req.HashName)
if err != nil {
return fmt.Errorf("error making certID: %w", err)
}
_, err = c.core.Certificates.UpdateRenewalInfo(acme.RenewalInfoUpdateRequest{
CertID: certID,
Replaced: true,
})
if err != nil {
return err
}
return nil
}
// makeCertID returns a base64url-encoded string that uniquely identifies a certificate to endpoints
// that implement the draft-ietf-acme-ari specification: https://datatracker.ietf.org/doc/draft-ietf-acme-ari.
// hashName must be the string representation of a crypto.Hash constant in the golang.org/x/crypto package.
// Supported hash functions are SHA-1, SHA-256, SHA-384, and SHA-512.
func makeCertID(leaf, issuer *x509.Certificate, hashName string) (string, error) {
if leaf == nil {
return "", fmt.Errorf("leaf certificate is nil")
}
if issuer == nil {
return "", fmt.Errorf("issuer certificate is nil")
}
var hashFunc crypto.Hash
var oid asn1.ObjectIdentifier
switch hashName {
// The following correlation of hashFunc to OID is copied from a private mapping in golang.org/x/crypto/ocsp:
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.8.0:ocsp/ocsp.go;l=156
case crypto.SHA1.String():
hashFunc = crypto.SHA1
oid = asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26})
case crypto.SHA256.String():
hashFunc = crypto.SHA256
oid = asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 1})
case crypto.SHA384.String():
hashFunc = crypto.SHA384
oid = asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 2})
case crypto.SHA512.String():
hashFunc = crypto.SHA512
oid = asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 3})
default:
return "", fmt.Errorf("hashName %q is not supported by this package", hashName)
}
if !hashFunc.Available() {
// This should never happen.
return "", fmt.Errorf("hash function %q is not available on your platform", hashFunc)
}
var spki struct {
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}
_, err := asn1.Unmarshal(issuer.RawSubjectPublicKeyInfo, &spki)
if err != nil {
return "", err
}
h := hashFunc.New()
h.Write(spki.PublicKey.RightAlign())
issuerKeyHash := h.Sum(nil)
h.Reset()
h.Write(issuer.RawSubject)
issuerNameHash := h.Sum(nil)
type certID struct {
HashAlgorithm pkix.AlgorithmIdentifier
IssuerNameHash []byte
IssuerKeyHash []byte
SerialNumber *big.Int
}
// DER-encode the CertID ASN.1 sequence [RFC6960].
certIDBytes, err := asn1.Marshal(certID{
HashAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: oid,
},
IssuerNameHash: issuerNameHash,
IssuerKeyHash: issuerKeyHash,
SerialNumber: leaf.SerialNumber,
})
if err != nil {
return "", err
}
// base64url-encode [RFC4648] the bytes of the DER-encoded CertID ASN.1 sequence [RFC6960].
encodedBytes := base64.URLEncoding.EncodeToString(certIDBytes)
// Any trailing '=' characters MUST be stripped.
return strings.TrimRight(encodedBytes, "="), nil
}

356
certificate/renewal_test.go Normal file
View file

@ -0,0 +1,356 @@
package certificate
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/go-acme/lego/v4/acme"
"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-jose/go-jose/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
ariLeafPEM = `-----BEGIN CERTIFICATE-----
MIIDMDCCAhigAwIBAgIIPqNFaGVEHxwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVbWluaWNhIHJvb3QgY2EgM2ExMzU2MB4XDTIyMDMxNzE3NTEwOVoXDTI0MDQx
NjE3NTEwOVowFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCgm9K/c+il2Pf0f8qhgxn9SKqXq88cOm9ov9AVRbPA
OWAAewqX2yUAwI4LZBGEgzGzTATkiXfoJ3cN3k39cH6tBbb3iSPuEn7OZpIk9D+e
3Q9/hX+N/jlWkaTB/FNA+7aE5IVWhmdczYilXa10V9r+RcvACJt0gsipBZVJ4jfJ
HnWJJGRZzzxqG/xkQmpXxZO7nOPFc8SxYKWdfcgp+rjR2ogYhSz7BfKoVakGPbpX
vZOuT9z4kkHra/WjwlkQhtHoTXdAxH3qC2UjMzO57Tx+otj0CxAv9O7CTJXISywB
vEVcmTSZkHS3eZtvvIwPx7I30ITRkYk/tLl1MbyB3SiZAgMBAAGjeDB2MA4GA1Ud
DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T
AQH/BAIwADAfBgNVHSMEGDAWgBQ4zzDRUaXHVKqlSTWkULGU4zGZpTAWBgNVHREE
DzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAx0aYvmCk7JYGNEXe
+hrOfKawkHYzWvA92cI/Oi6h+oSdHZ2UKzwFNf37cVKZ37FCrrv5pFP/xhhHvrNV
EnOx4IaF7OrnaTu5miZiUWuvRQP7ZGmGNFYbLTEF6/dj+WqyYdVaWzxRqHFu1ptC
TXysJCeyiGnR+KOOjOOQ9ZlO5JUK3OE4hagPLfaIpDDy6RXQt3ss0iNLuB1+IOtp
1URpvffLZQ8xPsEgOZyPWOcabTwJrtqBwily+lwPFn2mChUx846LwQfxtsXU/lJg
HX2RteNJx7YYNeX3Uf960mgo5an6vE8QNAsIoNHYrGyEmXDhTRe9mCHyiW2S7fZq
o9q12g==
-----END CERTIFICATE-----`
ariIssuerPEM = `-----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIIOhNWtJ7Igr0wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVbWluaWNhIHJvb3QgY2EgM2ExMzU2MCAXDTIyMDMxNzE3NTEwOVoYDzIxMjIw
MzE3MTc1MTA5WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAzYTEzNTYwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDc3P6cxcCZ7FQOQrYuigReSa8T
IOPNKmlmX9OrTkPwjThiMNEETYKO1ea99yXPK36LUHC6OLmZ9jVQW2Ny1qwQCOy6
TrquhnwKgtkBMDAZBLySSEXYdKL3r0jA4sflW130/OLwhstU/yv0J8+pj7eSVOR3
zJBnYd1AqnXHRSwQm299KXgqema7uwsa8cgjrXsBzAhrwrvYlVhpWFSv3lQRDFQg
c5Z/ZDV9i26qiaJsCCmdisJZWN7N2luUgxdRqzZ4Cr2Xoilg3T+hkb2y/d6ttsPA
kaSA+pq3q6Qa7/qfGdT5WuUkcHpvKNRWqnwT9rCYlmG00r3hGgc42D/z1VvfAgMB
AAGjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr
BgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ4zzDRUaXHVKql
STWkULGU4zGZpTAfBgNVHSMEGDAWgBQ4zzDRUaXHVKqlSTWkULGU4zGZpTANBgkq
hkiG9w0BAQsFAAOCAQEArbDHhEjGedjb/YjU80aFTPWOMRjgyfQaPPgyxwX6Dsid
1i2H1x4ud4ntz3sTZZxdQIrOqtlIWTWVCjpStwGxaC+38SdreiTTwy/nikXGa/6W
ZyQRppR3agh/pl5LHVO6GsJz3YHa7wQhEhj3xsRwa9VrRXgHbLGbPOFVRTHPjaPg
Gtsv2PN3f67DsPHF47ASqyOIRpLZPQmZIw6D3isJwfl+8CzvlB1veO0Q3uh08IJc
fspYQXvFBzYa64uKxNAJMi4Pby8cf4r36Wnb7cL4ho3fOHgAltxdW8jgibRzqZpQ
QKyxn2jX7kxeUDt0hFDJE8lOrhP73m66eBNzxe//FQ==
-----END CERTIFICATE-----`
ariLeafCertID = "MFswCwYJYIZIAWUDBAIBBCCeWLRusNLb--vmWOkxm34qDjTMWkc3utIhOMoMwKDqbgQg2iiKWySZrD-6c88HMZ6vhIHZPamChLlzGHeZ7pTS8jYCCD6jRWhlRB8c"
)
func Test_makeCertID(t *testing.T) {
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
require.NoError(t, err)
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
require.NoError(t, err)
actual, err := makeCertID(leaf, issuer, crypto.SHA256.String())
require.NoError(t, err)
assert.Equal(t, ariLeafCertID, actual)
}
func TestCertifier_GetRenewalInfo(t *testing.T) {
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
require.NoError(t, err)
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
require.NoError(t, err)
// Test with a fake API.
mux, apiURL := tester.SetupFakeAPI(t)
mux.HandleFunc("/renewalInfo/"+ariLeafCertID, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, wErr := w.Write([]byte(`{
"suggestedWindow": {
"start": "2020-03-17T17:51:09Z",
"end": "2020-03-17T18:21:09Z"
},
"explanationUrl": "https://aricapable.ca/docs/renewal-advice/"
}
}`))
require.NoError(t, wErr)
})
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
ri, err := certifier.GetRenewalInfo(RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()})
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/docs/renewal-advice/", ri.ExplanationURL)
}
func TestCertifier_GetRenewalInfo_errors(t *testing.T) {
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
require.NoError(t, err)
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
require.NoError(t, err)
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
testCases := []struct {
desc string
httpClient *http.Client
request RenewalInfoRequest
handler http.HandlerFunc
}{
{
desc: "API timeout",
httpClient: &http.Client{Timeout: 500 * time.Millisecond}, // HTTP client that times out after 500ms.
request: RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()},
handler: func(w http.ResponseWriter, r *http.Request) {
// API that takes 2ms to respond.
time.Sleep(2 * time.Millisecond)
},
},
{
desc: "API error",
httpClient: http.DefaultClient,
request: RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()},
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)
},
},
{
desc: "Issuer certificate is nil",
httpClient: http.DefaultClient,
request: RenewalInfoRequest{leaf, nil, crypto.SHA256.String()},
handler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
mux, apiURL := tester.SetupFakeAPI(t)
mux.HandleFunc("/renewalInfo/"+ariLeafCertID, test.handler)
core, err := api.New(test.httpClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
response, err := certifier.GetRenewalInfo(test.request)
require.Error(t, err)
assert.Nil(t, response)
})
}
}
func TestCertifier_UpdateRenewalInfo(t *testing.T) {
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
require.NoError(t, err)
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
require.NoError(t, err)
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
// Test with a fake API.
mux, apiURL := tester.SetupFakeAPI(t)
mux.HandleFunc("/renewalInfo", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
body, rsbErr := readSignedBody(r, key)
if rsbErr != nil {
http.Error(w, rsbErr.Error(), http.StatusBadRequest)
return
}
var req acme.RenewalInfoUpdateRequest
err = json.Unmarshal(body, &req)
assert.NoError(t, err)
assert.True(t, req.Replaced)
assert.Equal(t, ariLeafCertID, req.CertID)
w.WriteHeader(http.StatusOK)
})
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
err = certifier.UpdateRenewalInfo(RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()})
require.NoError(t, err)
}
func TestCertifier_UpdateRenewalInfo_errors(t *testing.T) {
leaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))
require.NoError(t, err)
issuer, err := certcrypto.ParsePEMCertificate([]byte(ariIssuerPEM))
require.NoError(t, err)
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, "Could not generate test key")
testCases := []struct {
desc string
request RenewalInfoRequest
}{
{
desc: "API error",
request: RenewalInfoRequest{leaf, issuer, crypto.SHA256.String()},
},
{
desc: "Certificate is nil",
request: RenewalInfoRequest{nil, issuer, crypto.SHA256.String()},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
mux, apiURL := tester.SetupFakeAPI(t)
// Always returns an error.
mux.HandleFunc("/renewalInfo", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
})
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
require.NoError(t, err)
certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})
err = certifier.UpdateRenewalInfo(test.request)
require.Error(t, err)
})
}
}
func TestRenewalInfoResponse_ShouldRenew(t *testing.T) {
now := time.Now().UTC()
t.Run("Window is in the past", func(t *testing.T) {
ri := RenewalInfoResponse{
acme.RenewalInfoResponse{
SuggestedWindow: acme.Window{
Start: now.Add(-2 * time.Hour),
End: now.Add(-1 * time.Hour),
},
ExplanationURL: "",
},
}
rt := ri.ShouldRenewAt(now, 0)
require.NotNil(t, rt)
assert.Equal(t, now, *rt)
})
t.Run("Window is in the future", func(t *testing.T) {
ri := RenewalInfoResponse{
acme.RenewalInfoResponse{
SuggestedWindow: acme.Window{
Start: now.Add(1 * time.Hour),
End: now.Add(2 * time.Hour),
},
ExplanationURL: "",
},
}
rt := ri.ShouldRenewAt(now, 0)
assert.Nil(t, rt)
})
t.Run("Window is in the future, but caller is willing to sleep", func(t *testing.T) {
ri := RenewalInfoResponse{
acme.RenewalInfoResponse{
SuggestedWindow: acme.Window{
Start: now.Add(1 * time.Hour),
End: now.Add(2 * time.Hour),
},
ExplanationURL: "",
},
}
rt := ri.ShouldRenewAt(now, 2*time.Hour)
require.NotNil(t, rt)
assert.True(t, rt.Before(now.Add(2*time.Hour)))
})
t.Run("Window is in the future, but caller isn't willing to sleep long enough", func(t *testing.T) {
ri := RenewalInfoResponse{
acme.RenewalInfoResponse{
SuggestedWindow: acme.Window{
Start: now.Add(1 * time.Hour),
End: now.Add(2 * time.Hour),
},
ExplanationURL: "",
},
}
rt := ri.ShouldRenewAt(now, 59*time.Minute)
assert.Nil(t, rt)
})
}
func readSignedBody(r *http.Request, privateKey *rsa.PrivateKey) ([]byte, error) {
reqBody, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
jws, err := jose.ParseSigned(string(reqBody))
if err != nil {
return nil, err
}
body, err := jws.Verify(&jose.JSONWebKey{
Key: privateKey.Public(),
Algorithm: "RSA",
})
if err != nil {
return nil, err
}
return body, nil
}

44
challenge/challenges.go Normal file
View file

@ -0,0 +1,44 @@
package challenge
import (
"fmt"
"github.com/go-acme/lego/v4/acme"
)
// Type is a string that identifies a particular challenge type and version of ACME challenge.
type Type string
const (
// HTTP01 is the "http-01" ACME challenge https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3
// Note: ChallengePath returns the URL path to fulfill this challenge.
HTTP01 = Type("http-01")
// DNS01 is the "dns-01" ACME challenge https://www.rfc-editor.org/rfc/rfc8555.html#section-8.4
// Note: GetRecord returns a DNS record which will fulfill this challenge.
DNS01 = Type("dns-01")
// TLSALPN01 is the "tls-alpn-01" ACME challenge https://www.rfc-editor.org/rfc/rfc8737.html
TLSALPN01 = Type("tls-alpn-01")
)
func (t Type) String() string {
return string(t)
}
func FindChallenge(chlgType Type, authz acme.Authorization) (acme.Challenge, error) {
for _, chlg := range authz.Challenges {
if chlg.Type == string(chlgType) {
return chlg, nil
}
}
return acme.Challenge{}, fmt.Errorf("[%s] acme: unable to find challenge %s", GetTargetedDomain(authz), chlgType)
}
func GetTargetedDomain(authz acme.Authorization) string {
if authz.Wildcard {
return "*." + authz.Identifier.Value
}
return authz.Identifier.Value
}

16
challenge/dns01/cname.go Normal file
View file

@ -0,0 +1,16 @@
package dns01
import "github.com/miekg/dns"
// Update FQDN with CNAME if any.
func updateDomainWithCName(r *dns.Msg, fqdn string) string {
for _, rr := range r.Answer {
if cn, ok := rr.(*dns.CNAME); ok {
if cn.Hdr.Name == fqdn {
return cn.Target
}
}
}
return fqdn
}

View file

@ -0,0 +1,238 @@
package dns01
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
"strconv"
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/wait"
"github.com/miekg/dns"
)
const (
// DefaultPropagationTimeout default propagation timeout.
DefaultPropagationTimeout = 60 * time.Second
// DefaultPollingInterval default polling interval.
DefaultPollingInterval = 2 * time.Second
// DefaultTTL default TTL.
DefaultTTL = 120
)
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
type ChallengeOption func(*Challenge) error
// CondOption Conditional challenge option.
func CondOption(condition bool, opt ChallengeOption) ChallengeOption {
if !condition {
// NoOp options
return func(*Challenge) error {
return nil
}
}
return opt
}
// Challenge implements the dns-01 challenge.
type Challenge struct {
core *api.Core
validate ValidateFunc
provider challenge.Provider
preCheck preCheck
dnsTimeout time.Duration
}
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {
chlg := &Challenge{
core: core,
validate: validate,
provider: provider,
preCheck: newPreCheck(),
dnsTimeout: 10 * time.Second,
}
for _, opt := range opts {
err := opt(chlg)
if err != nil {
log.Infof("challenge option error: %v", err)
}
}
return chlg
}
// PreSolve just submits the txt record to the dns provider.
// It does not validate record propagation, or do anything at all with the acme server.
func (c *Challenge) PreSolve(authz acme.Authorization) error {
domain := challenge.GetTargetedDomain(authz)
log.Infof("[%s] acme: Preparing to solve DNS-01", domain)
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
if err != nil {
return err
}
if c.provider == nil {
return fmt.Errorf("[%s] acme: no DNS Provider configured", domain)
}
// Generate the Key Authorization for the challenge
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err)
}
return nil
}
func (c *Challenge) Solve(authz acme.Authorization) error {
domain := challenge.GetTargetedDomain(authz)
log.Infof("[%s] acme: Trying to solve DNS-01", domain)
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
if err != nil {
return err
}
// Generate the Key Authorization for the challenge
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
info := GetChallengeInfo(authz.Identifier.Value, keyAuth)
var timeout, interval time.Duration
switch provider := c.provider.(type) {
case challenge.ProviderTimeout:
timeout, interval = provider.Timeout()
default:
timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
}
log.Infof("[%s] acme: Checking DNS record propagation using %+v", domain, recursiveNameservers)
time.Sleep(interval)
err = wait.For("propagation", timeout, interval, func() (bool, error) {
stop, errP := c.preCheck.call(domain, info.EffectiveFQDN, info.Value)
if !stop || errP != nil {
log.Infof("[%s] acme: Waiting for DNS record propagation.", domain)
}
return stop, errP
})
if err != nil {
return err
}
chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng)
}
// CleanUp cleans the challenge.
func (c *Challenge) CleanUp(authz acme.Authorization) error {
log.Infof("[%s] acme: Cleaning DNS-01 challenge", challenge.GetTargetedDomain(authz))
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
if err != nil {
return err
}
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
return c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
}
func (c *Challenge) Sequential() (bool, time.Duration) {
if p, ok := c.provider.(sequential); ok {
return ok, p.Sequential()
}
return false, 0
}
type sequential interface {
Sequential() time.Duration
}
// 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)
return info.EffectiveFQDN, info.Value
}
// ChallengeInfo contains the information use to create the TXT record.
type ChallengeInfo struct {
// FQDN is the full-qualified challenge domain (i.e. `_acme-challenge.[domain].`)
FQDN string
// EffectiveFQDN contains the resulting FQDN after the CNAMEs resolutions.
EffectiveFQDN string
// Value contains the value for the TXT record.
Value string
}
// GetChallengeInfo returns information used to create a DNS record which will fulfill the `dns-01` challenge.
func GetChallengeInfo(domain, keyAuth string) ChallengeInfo {
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
// base64URL encoding without padding
value := base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
ok, _ := strconv.ParseBool(os.Getenv("LEGO_DISABLE_CNAME_SUPPORT"))
return ChallengeInfo{
Value: value,
FQDN: getChallengeFQDN(domain, false),
EffectiveFQDN: getChallengeFQDN(domain, !ok),
}
}
func getChallengeFQDN(domain string, followCNAME bool) string {
fqdn := fmt.Sprintf("_acme-challenge.%s.", domain)
if !followCNAME {
return fqdn
}
// recursion counter so it doesn't spin out of control
for limit := 0; limit < 50; limit++ {
// Keep following CNAMEs
r, err := dnsQuery(fqdn, dns.TypeCNAME, recursiveNameservers, true)
if err != nil || r.Rcode != dns.RcodeSuccess {
// No more CNAME records to follow, exit
break
}
// Check if the domain has CNAME then use that
cname := updateDomainWithCName(r, fqdn)
if cname == fqdn {
break
}
log.Infof("Found CNAME entry for %q: %q", fqdn, cname)
fqdn = cname
}
return fqdn
}

View file

@ -0,0 +1,59 @@
package dns01
import (
"bufio"
"fmt"
"os"
"time"
)
const (
dnsTemplate = `%s %d IN TXT %q`
)
// DNSProviderManual is an implementation of the ChallengeProvider interface.
type DNSProviderManual struct{}
// NewDNSProviderManual returns a DNSProviderManual instance.
func NewDNSProviderManual() (*DNSProviderManual, error) {
return &DNSProviderManual{}, nil
}
// Present prints instructions for manually creating the TXT record.
func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
info := GetChallengeInfo(domain, keyAuth)
authZone, err := FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return err
}
fmt.Printf("lego: Please create the following TXT record in your %s zone:\n", authZone)
fmt.Printf(dnsTemplate+"\n", info.EffectiveFQDN, DefaultTTL, info.Value)
fmt.Printf("lego: Press 'Enter' when you are done\n")
_, err = bufio.NewReader(os.Stdin).ReadBytes('\n')
return err
}
// CleanUp prints instructions for manually removing the TXT record.
func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
info := GetChallengeInfo(domain, keyAuth)
authZone, err := FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return err
}
fmt.Printf("lego: You can now remove this TXT record from your %s zone:\n", authZone)
fmt.Printf(dnsTemplate+"\n", info.EffectiveFQDN, DefaultTTL, "...")
return nil
}
// Sequential All DNS challenges for this provider will be resolved sequentially.
// Returns the interval between each iteration.
func (d *DNSProviderManual) Sequential() time.Duration {
return DefaultPropagationTimeout
}

View file

@ -0,0 +1,60 @@
package dns01
import (
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDNSProviderManual(t *testing.T) {
backupStdin := os.Stdin
defer func() { os.Stdin = backupStdin }()
testCases := []struct {
desc string
input string
expectError bool
}{
{
desc: "Press enter",
input: "ok\n",
},
{
desc: "Missing enter",
input: "ok",
expectError: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
file, err := os.CreateTemp("", "lego_test")
assert.NoError(t, err)
defer func() { _ = os.Remove(file.Name()) }()
_, err = file.WriteString(test.input)
assert.NoError(t, err)
_, err = file.Seek(0, io.SeekStart)
assert.NoError(t, err)
os.Stdin = file
manualProvider, err := NewDNSProviderManual()
require.NoError(t, err)
err = manualProvider.Present("example.com", "", "")
if test.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
err = manualProvider.CleanUp("example.com", "", "")
require.NoError(t, err)
}
})
}
}

View file

@ -0,0 +1,283 @@
package dns01
import (
"crypto/rand"
"crypto/rsa"
"errors"
"net/http"
"testing"
"time"
"github.com/go-acme/lego/v4/acme"
"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/stretchr/testify/require"
)
type providerMock struct {
present, cleanUp error
}
func (p *providerMock) Present(domain, token, keyAuth string) error { return p.present }
func (p *providerMock) CleanUp(domain, token, keyAuth string) error { return p.cleanUp }
type providerTimeoutMock struct {
present, cleanUp error
timeout, interval time.Duration
}
func (p *providerTimeoutMock) Present(domain, token, keyAuth string) error { return p.present }
func (p *providerTimeoutMock) CleanUp(domain, token, keyAuth string) error { return p.cleanUp }
func (p *providerTimeoutMock) Timeout() (time.Duration, time.Duration) { return p.timeout, p.interval }
func TestChallenge_PreSolve(t *testing.T) {
_, apiURL := tester.SetupFakeAPI(t)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
testCases := []struct {
desc string
validate ValidateFunc
preCheck WrapPreCheckFunc
provider challenge.Provider
expectError bool
}{
{
desc: "success",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{},
},
{
desc: "validate fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New("OOPS") },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{
present: nil,
cleanUp: nil,
},
},
{
desc: "preCheck fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return false, errors.New("OOPS") },
provider: &providerTimeoutMock{
timeout: 2 * time.Second,
interval: 500 * time.Millisecond,
},
},
{
desc: "present fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{
present: errors.New("OOPS"),
},
expectError: true,
},
{
desc: "cleanUp fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{
cleanUp: errors.New("OOPS"),
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
chlg := NewChallenge(core, test.validate, test.provider, WrapPreCheck(test.preCheck))
authz := acme.Authorization{
Identifier: acme.Identifier{
Value: "example.com",
},
Challenges: []acme.Challenge{
{Type: challenge.DNS01.String()},
},
}
err = chlg.PreSolve(authz)
if test.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestChallenge_Solve(t *testing.T) {
_, apiURL := tester.SetupFakeAPI(t)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
testCases := []struct {
desc string
validate ValidateFunc
preCheck WrapPreCheckFunc
provider challenge.Provider
expectError bool
}{
{
desc: "success",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{},
},
{
desc: "validate fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New("OOPS") },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{
present: nil,
cleanUp: nil,
},
expectError: true,
},
{
desc: "preCheck fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return false, errors.New("OOPS") },
provider: &providerTimeoutMock{
timeout: 2 * time.Second,
interval: 500 * time.Millisecond,
},
expectError: true,
},
{
desc: "present fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{
present: errors.New("OOPS"),
},
},
{
desc: "cleanUp fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{
cleanUp: errors.New("OOPS"),
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
var options []ChallengeOption
if test.preCheck != nil {
options = append(options, WrapPreCheck(test.preCheck))
}
chlg := NewChallenge(core, test.validate, test.provider, options...)
authz := acme.Authorization{
Identifier: acme.Identifier{
Value: "example.com",
},
Challenges: []acme.Challenge{
{Type: challenge.DNS01.String()},
},
}
err = chlg.Solve(authz)
if test.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestChallenge_CleanUp(t *testing.T) {
_, apiURL := tester.SetupFakeAPI(t)
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err)
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
testCases := []struct {
desc string
validate ValidateFunc
preCheck WrapPreCheckFunc
provider challenge.Provider
expectError bool
}{
{
desc: "success",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{},
},
{
desc: "validate fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New("OOPS") },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{
present: nil,
cleanUp: nil,
},
},
{
desc: "preCheck fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return false, errors.New("OOPS") },
provider: &providerTimeoutMock{
timeout: 2 * time.Second,
interval: 500 * time.Millisecond,
},
},
{
desc: "present fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{
present: errors.New("OOPS"),
},
},
{
desc: "cleanUp fail",
validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },
preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },
provider: &providerMock{
cleanUp: errors.New("OOPS"),
},
expectError: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
chlg := NewChallenge(core, test.validate, test.provider, WrapPreCheck(test.preCheck))
authz := acme.Authorization{
Identifier: acme.Identifier{
Value: "example.com",
},
Challenges: []acme.Challenge{
{Type: challenge.DNS01.String()},
},
}
err = chlg.CleanUp(authz)
if test.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

24
challenge/dns01/domain.go Normal file
View file

@ -0,0 +1,24 @@
package dns01
import (
"fmt"
"strings"
"github.com/miekg/dns"
)
// ExtractSubDomain extracts the subdomain part from a domain and a zone.
func ExtractSubDomain(domain, zone string) (string, error) {
canonDomain := dns.Fqdn(domain)
canonZone := dns.Fqdn(zone)
if canonDomain == canonZone {
return "", fmt.Errorf("no subdomain because the domain and the zone are identical: %s", canonDomain)
}
if !dns.IsSubDomain(canonZone, canonDomain) {
return "", fmt.Errorf("%s is not a subdomain of %s", canonDomain, canonZone)
}
return strings.TrimSuffix(canonDomain, "."+canonZone), nil
}

View file

@ -0,0 +1,104 @@
package dns01
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExtractSubDomain(t *testing.T) {
testCases := []struct {
desc string
domain string
zone string
expected string
}{
{
desc: "no FQDN",
domain: "_acme-challenge.example.com",
zone: "example.com",
expected: "_acme-challenge",
},
{
desc: "no FQDN zone",
domain: "_acme-challenge.example.com.",
zone: "example.com",
expected: "_acme-challenge",
},
{
desc: "no FQDN domain",
domain: "_acme-challenge.example.com",
zone: "example.com.",
expected: "_acme-challenge",
},
{
desc: "FQDN",
domain: "_acme-challenge.example.com.",
zone: "example.com.",
expected: "_acme-challenge",
},
{
desc: "multi-level subdomain",
domain: "_acme-challenge.one.example.com.",
zone: "example.com.",
expected: "_acme-challenge.one",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
subDomain, err := ExtractSubDomain(test.domain, test.zone)
require.NoError(t, err)
assert.Equal(t, test.expected, subDomain)
})
}
}
func TestExtractSubDomain_errors(t *testing.T) {
testCases := []struct {
desc string
domain string
zone string
}{
{
desc: "same domain",
domain: "example.com",
zone: "example.com",
},
{
desc: "same domain, no FQDN zone",
domain: "example.com.",
zone: "example.com",
},
{
desc: "same domain, no FQDN domain",
domain: "example.com",
zone: "example.com.",
},
{
desc: "same domain, FQDN",
domain: "example.com.",
zone: "example.com.",
},
{
desc: "zone and domain are unrelated",
domain: "_acme-challenge.example.com",
zone: "example.org",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
_, err := ExtractSubDomain(test.domain, test.zone)
require.Error(t, err)
})
}
}

19
challenge/dns01/fqdn.go Normal file
View file

@ -0,0 +1,19 @@
package dns01
// ToFqdn converts the name into a fqdn appending a trailing dot.
func ToFqdn(name string) string {
n := len(name)
if n == 0 || name[n-1] == '.' {
return name
}
return name + "."
}
// UnFqdn converts the fqdn into a name removing the trailing dot.
func UnFqdn(name string) string {
n := len(name)
if n != 0 && name[n-1] == '.' {
return name[:n-1]
}
return name
}

View file

@ -0,0 +1,66 @@
package dns01
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestToFqdn(t *testing.T) {
testCases := []struct {
desc string
domain string
expected string
}{
{
desc: "simple",
domain: "foo.bar.com",
expected: "foo.bar.com.",
},
{
desc: "already FQDN",
domain: "foo.bar.com.",
expected: "foo.bar.com.",
},
}
for _, test := range testCases {
test := test
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
fqdn string
expected string
}{
{
desc: "simple",
fqdn: "foo.bar.com.",
expected: "foo.bar.com",
},
{
desc: "already domain",
fqdn: "foo.bar.com",
expected: "foo.bar.com",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
domain := UnFqdn(test.fqdn)
assert.Equal(t, test.expected, domain)
})
}
}

View file

@ -0,0 +1,281 @@
package dns01
import (
"errors"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/miekg/dns"
)
const defaultResolvConf = "/etc/resolv.conf"
var (
fqdnSoaCache = map[string]*soaCacheEntry{}
muFqdnSoaCache sync.Mutex
)
var defaultNameservers = []string{
"google-public-dns-a.google.com:53",
"google-public-dns-b.google.com:53",
}
// recursiveNameservers are used to pre-check DNS propagation.
var recursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
// soaCacheEntry holds a cached SOA record (only selected fields).
type soaCacheEntry struct {
zone string // zone apex (a domain name)
primaryNs string // primary nameserver for the zone apex
expires time.Time // time when this cache entry should be evicted
}
func newSoaCacheEntry(soa *dns.SOA) *soaCacheEntry {
return &soaCacheEntry{
zone: soa.Hdr.Name,
primaryNs: soa.Ns,
expires: time.Now().Add(time.Duration(soa.Refresh) * time.Second),
}
}
// isExpired checks whether a cache entry should be considered expired.
func (cache *soaCacheEntry) isExpired() bool {
return time.Now().After(cache.expires)
}
// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
func ClearFqdnCache() {
muFqdnSoaCache.Lock()
fqdnSoaCache = map[string]*soaCacheEntry{}
muFqdnSoaCache.Unlock()
}
func AddDNSTimeout(timeout time.Duration) ChallengeOption {
return func(_ *Challenge) error {
dnsTimeout = timeout
return nil
}
}
func AddRecursiveNameservers(nameservers []string) ChallengeOption {
return func(_ *Challenge) error {
recursiveNameservers = ParseNameservers(nameservers)
return nil
}
}
// getNameservers attempts to get systems nameservers before falling back to the defaults.
func getNameservers(path string, defaults []string) []string {
config, err := dns.ClientConfigFromFile(path)
if err != nil || len(config.Servers) == 0 {
return defaults
}
return ParseNameservers(config.Servers)
}
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 {
resolvers = append(resolvers, net.JoinHostPort(resolver, "53"))
} else {
resolvers = append(resolvers, resolver)
}
}
return resolvers
}
// lookupNameservers returns the authoritative nameservers for the given fqdn.
func lookupNameservers(fqdn string) ([]string, error) {
var authoritativeNss []string
zone, err := FindZoneByFqdn(fqdn)
if err != nil {
return nil, fmt.Errorf("could not determine the zone: %w", err)
}
r, err := dnsQuery(zone, dns.TypeNS, recursiveNameservers, true)
if err != nil {
return nil, err
}
for _, rr := range r.Answer {
if ns, ok := rr.(*dns.NS); ok {
authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))
}
}
if len(authoritativeNss) > 0 {
return authoritativeNss, nil
}
return nil, errors.New("could not determine authoritative nameservers")
}
// FindPrimaryNsByFqdn determines the primary nameserver of the zone apex for the given fqdn
// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.
func FindPrimaryNsByFqdn(fqdn string) (string, error) {
return FindPrimaryNsByFqdnCustom(fqdn, recursiveNameservers)
}
// FindPrimaryNsByFqdnCustom determines the primary nameserver of the zone apex for the given fqdn
// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.
func FindPrimaryNsByFqdnCustom(fqdn string, nameservers []string) (string, error) {
soa, err := lookupSoaByFqdn(fqdn, nameservers)
if err != nil {
return "", err
}
return soa.primaryNs, nil
}
// FindZoneByFqdn determines the zone apex for the given fqdn
// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.
func FindZoneByFqdn(fqdn string) (string, error) {
return FindZoneByFqdnCustom(fqdn, recursiveNameservers)
}
// FindZoneByFqdnCustom determines the zone apex for the given fqdn
// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.
func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) {
soa, err := lookupSoaByFqdn(fqdn, nameservers)
if err != nil {
return "", err
}
return soa.zone, nil
}
func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
muFqdnSoaCache.Lock()
defer muFqdnSoaCache.Unlock()
// Do we have it cached and is it still fresh?
if ent := fqdnSoaCache[fqdn]; ent != nil && !ent.isExpired() {
return ent, nil
}
ent, err := fetchSoaByFqdn(fqdn, nameservers)
if err != nil {
return nil, err
}
fqdnSoaCache[fqdn] = ent
return ent, nil
}
func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {
var err error
var in *dns.Msg
labelIndexes := dns.Split(fqdn)
for _, index := range labelIndexes {
domain := fqdn[index:]
in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
if err != nil {
continue
}
if in == nil {
continue
}
switch in.Rcode {
case dns.RcodeSuccess:
// Check if we got a SOA RR in the answer section
if len(in.Answer) == 0 {
continue
}
// CNAME records cannot/should not exist at the root of a zone.
// So we skip a domain when a CNAME is found.
if dnsMsgContainsCNAME(in) {
continue
}
for _, ans := range in.Answer {
if soa, ok := ans.(*dns.SOA); ok {
return newSoaCacheEntry(soa), nil
}
}
case dns.RcodeNameError:
// NXDOMAIN
default:
// Any response code other than NOERROR and NXDOMAIN is treated as error
return nil, fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain)
}
}
return nil, fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err))
}
// dnsMsgContainsCNAME checks for a CNAME answer in msg.
func dnsMsgContainsCNAME(msg *dns.Msg) bool {
for _, ans := range msg.Answer {
if _, ok := ans.(*dns.CNAME); ok {
return true
}
}
return false
}
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) {
m := createDNSMsg(fqdn, rtype, recursive)
var in *dns.Msg
var err error
for _, ns := range nameservers {
in, err = sendDNSQuery(m, ns)
if err == nil && len(in.Answer) > 0 {
break
}
}
return in, err
}
func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
m := new(dns.Msg)
m.SetQuestion(fqdn, rtype)
m.SetEdns0(4096, false)
if !recursive {
m.RecursionDesired = false
}
return m
}
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
in, _, err := udp.Exchange(m, ns)
if in != nil && in.Truncated {
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
// If the TCP request succeeds, the err will reset to nil
in, _, err = tcp.Exchange(m, ns)
}
return in, err
}
func formatDNSError(msg *dns.Msg, err error) string {
var parts []string
if msg != nil {
parts = append(parts, dns.RcodeToString[msg.Rcode])
}
if err != nil {
parts = append(parts, err.Error())
}
if len(parts) > 0 {
return ": " + strings.Join(parts, " ")
}
return ""
}

View file

@ -0,0 +1,199 @@
package dns01
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLookupNameserversOK(t *testing.T) {
testCases := []struct {
fqdn string
nss []string
}{
{
fqdn: "en.wikipedia.org.",
nss: []string{"ns0.wikimedia.org.", "ns1.wikimedia.org.", "ns2.wikimedia.org."},
},
{
fqdn: "www.google.com.",
nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
},
{
fqdn: "physics.georgetown.edu.",
nss: []string{"ns4.georgetown.edu.", "ns5.georgetown.edu.", "ns6.georgetown.edu."},
},
}
for _, test := range testCases {
test := test
t.Run(test.fqdn, func(t *testing.T) {
t.Parallel()
nss, err := lookupNameservers(test.fqdn)
require.NoError(t, err)
sort.Strings(nss)
sort.Strings(test.nss)
assert.EqualValues(t, test.nss, nss)
})
}
}
func TestLookupNameserversErr(t *testing.T) {
testCases := []struct {
desc string
fqdn string
error string
}{
{
desc: "invalid tld",
fqdn: "_null.n0n0.",
error: "could not determine the zone",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
_, err := lookupNameservers(test.fqdn)
require.Error(t, err)
assert.Contains(t, err.Error(), test.error)
})
}
}
var findXByFqdnTestCases = []struct {
desc string
fqdn string
zone string
primaryNs string
nameservers []string
expectedError string
}{
{
desc: "domain is a CNAME",
fqdn: "mail.google.com.",
zone: "google.com.",
primaryNs: "ns1.google.com.",
nameservers: recursiveNameservers,
},
{
desc: "domain is a non-existent subdomain",
fqdn: "foo.google.com.",
zone: "google.com.",
primaryNs: "ns1.google.com.",
nameservers: recursiveNameservers,
},
{
desc: "domain is a eTLD",
fqdn: "example.com.ac.",
zone: "ac.",
primaryNs: "a0.nic.ac.",
nameservers: recursiveNameservers,
},
{
desc: "domain is a cross-zone CNAME",
fqdn: "cross-zone-example.assets.sh.",
zone: "assets.sh.",
primaryNs: "gina.ns.cloudflare.com.",
nameservers: recursiveNameservers,
},
{
desc: "NXDOMAIN",
fqdn: "test.lego.zz.",
zone: "lego.zz.",
nameservers: []string{"8.8.8.8:53"},
expectedError: "could not find the start of authority for test.lego.zz.: 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"},
expectedError: "could not find the start of authority for mail.google.com.: read udp",
},
{
desc: "no nameservers",
fqdn: "test.ldez.com.",
zone: "ldez.com.",
nameservers: []string{},
expectedError: "could not find the start of authority for test.ldez.com.",
},
}
func TestFindZoneByFqdnCustom(t *testing.T) {
for _, test := range findXByFqdnTestCases {
t.Run(test.desc, func(t *testing.T) {
ClearFqdnCache()
zone, err := FindZoneByFqdnCustom(test.fqdn, test.nameservers)
if test.expectedError != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), test.expectedError)
} else {
require.NoError(t, err)
assert.Equal(t, test.zone, zone)
}
})
}
}
func TestFindPrimaryNsByFqdnCustom(t *testing.T) {
for _, test := range findXByFqdnTestCases {
t.Run(test.desc, func(t *testing.T) {
ClearFqdnCache()
ns, err := FindPrimaryNsByFqdnCustom(test.fqdn, test.nameservers)
if test.expectedError != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), test.expectedError)
} else {
require.NoError(t, err)
assert.Equal(t, test.primaryNs, ns)
}
})
}
}
func TestResolveConfServers(t *testing.T) {
testCases := []struct {
fixture string
expected []string
defaults []string
}{
{
fixture: "fixtures/resolv.conf.1",
defaults: []string{"127.0.0.1:53"},
expected: []string{"10.200.3.249:53", "10.200.3.250:5353", "[2001:4860:4860::8844]:53", "[10.0.0.1]:5353"},
},
{
fixture: "fixtures/resolv.conf.nonexistant",
defaults: []string{"127.0.0.1:53"},
expected: []string{"127.0.0.1:53"},
},
}
for _, test := range testCases {
t.Run(test.fixture, func(t *testing.T) {
result := getNameservers(test.fixture, test.defaults)
sort.Strings(result)
sort.Strings(test.expected)
assert.Equal(t, test.expected, result)
})
}
}

View file

@ -0,0 +1,8 @@
//go:build !windows
package dns01
import "time"
// dnsTimeout is used to override the default DNS timeout of 10 seconds.
var dnsTimeout = 10 * time.Second

View file

@ -0,0 +1,8 @@
//go:build windows
package dns01
import "time"
// dnsTimeout is used to override the default DNS timeout of 20 seconds.
var dnsTimeout = 20 * time.Second

110
challenge/dns01/precheck.go Normal file
View file

@ -0,0 +1,110 @@
package dns01
import (
"fmt"
"net"
"strings"
"github.com/miekg/dns"
)
// PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready.
type PreCheckFunc func(fqdn, value string) (bool, error)
// WrapPreCheckFunc wraps a PreCheckFunc in order to do extra operations before or after
// the main check, put it in a loop, etc.
type WrapPreCheckFunc func(domain, fqdn, value string, check PreCheckFunc) (bool, error)
// WrapPreCheck Allow to define checks before notifying ACME that the DNS challenge is ready.
func WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption {
return func(chlg *Challenge) error {
chlg.preCheck.checkFunc = wrap
return nil
}
}
func DisableCompletePropagationRequirement() ChallengeOption {
return func(chlg *Challenge) error {
chlg.preCheck.requireCompletePropagation = false
return nil
}
}
type preCheck struct {
// checks DNS propagation before notifying ACME that the DNS challenge is ready.
checkFunc WrapPreCheckFunc
// require the TXT record to be propagated to all authoritative name servers
requireCompletePropagation bool
}
func newPreCheck() preCheck {
return preCheck{
requireCompletePropagation: true,
}
}
func (p preCheck) call(domain, fqdn, value string) (bool, error) {
if p.checkFunc == nil {
return p.checkDNSPropagation(fqdn, value)
}
return p.checkFunc(domain, fqdn, value, p.checkDNSPropagation)
}
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) {
// Initial attempt to resolve at the recursive NS
r, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameservers, true)
if err != nil {
return false, err
}
if !p.requireCompletePropagation {
return true, nil
}
if r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn)
}
authoritativeNss, err := lookupNameservers(fqdn)
if err != nil {
return false, err
}
return checkAuthoritativeNss(fqdn, value, authoritativeNss)
}
// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
for _, ns := range nameservers {
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false)
if err != nil {
return false, err
}
if r.Rcode != dns.RcodeSuccess {
return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
}
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
break
}
}
}
if !found {
return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s, value: %s]: %s", ns, fqdn, value, strings.Join(records, " ,"))
}
}
return true, nil
}

View file

@ -0,0 +1,117 @@
package dns01
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCheckDNSPropagation(t *testing.T) {
testCases := []struct {
desc string
fqdn string
value string
expectError bool
}{
{
desc: "success",
fqdn: "postman-echo.com.",
value: "postman-domain-verification=c85de626cb79d941310696e06558e2e790223802f3697dfbdcaf65510152d52c",
},
{
desc: "no TXT record",
fqdn: "acme-staging.api.letsencrypt.org.",
value: "fe01=",
expectError: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
ClearFqdnCache()
check := newPreCheck()
ok, err := check.checkDNSPropagation(test.fqdn, test.value)
if test.expectError {
assert.Errorf(t, err, "PreCheckDNS must failed for %s", test.fqdn)
assert.False(t, ok, "PreCheckDNS must failed for %s", test.fqdn)
} else {
assert.NoErrorf(t, err, "PreCheckDNS failed for %s", test.fqdn)
assert.True(t, ok, "PreCheckDNS failed for %s", test.fqdn)
}
})
}
}
func TestCheckAuthoritativeNss(t *testing.T) {
testCases := []struct {
desc string
fqdn, value string
ns []string
expected bool
}{
{
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",
fqdn: "ns1.google.com.",
ns: []string{"ns2.google.com."},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
ClearFqdnCache()
ok, _ := checkAuthoritativeNss(test.fqdn, test.value, test.ns)
assert.Equal(t, test.expected, ok, test.fqdn)
})
}
}
func TestCheckAuthoritativeNssErr(t *testing.T) {
testCases := []struct {
desc string
fqdn, value string
ns []string
error string
}{
{
desc: "TXT RR /w unexpected value",
fqdn: "8.8.8.8.asn.routeviews.org.",
value: "fe01=",
ns: []string{"asnums.routeviews.org."},
error: "did not return the expected TXT record",
},
{
desc: "No TXT RR",
fqdn: "ns1.google.com.",
value: "fe01=",
ns: []string{"ns2.google.com."},
error: "did not return the expected TXT record",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
ClearFqdnCache()
_, err := checkAuthoritativeNss(test.fqdn, test.value, test.ns)
require.Error(t, err)
assert.Contains(t, err.Error(), test.error)
})
}
}

View file

@ -0,0 +1,187 @@
package http01
import (
"fmt"
"net/http"
"strings"
)
// A domainMatcher tries to match a domain (the one we're requesting a certificate for)
// in the HTTP request coming from the ACME validation servers.
// This step is part of DNS rebind attack prevention,
// where the webserver matches incoming requests to a list of domain the server acts authoritative for.
//
// The most simple check involves finding the domain in the HTTP Host header;
// this is what hostMatcher does.
// Use it, when the http01.ProviderServer is directly reachable from the internet,
// or when it operates behind a transparent proxy.
//
// In many (reverse) proxy setups, Apache and NGINX traditionally move the Host header to a new header named X-Forwarded-Host.
// Use arbitraryMatcher("X-Forwarded-Host") in this case,
// or the appropriate header name for other proxy servers.
//
// RFC7239 has standardized the different forwarding headers into a single header named Forwarded.
// The header value has a different format, so you should use forwardedMatcher
// when the http01.ProviderServer operates behind a RFC7239 compatible proxy.
// https://www.rfc-editor.org/rfc/rfc7239.html
//
// Note: RFC7239 also reminds us, "that an HTTP list [...] may be split over multiple header fields" (section 7.1),
// meaning that
//
// X-Header: a
// X-Header: b
//
// is equal to
//
// X-Header: a, b
//
// All matcher implementations (explicitly not excluding arbitraryMatcher!)
// have in common that they only match against the first value in such lists.
type domainMatcher interface {
// matches checks whether the request is valid for the given domain.
matches(request *http.Request, domain string) bool
// name returns the header name used in the check.
// This is primarily used to create meaningful error messages.
name() string
}
// hostMatcher checks whether (*net/http).Request.Host starts with a domain name.
type hostMatcher struct{}
func (m *hostMatcher) name() string {
return "Host"
}
func (m *hostMatcher) matches(r *http.Request, domain string) bool {
return strings.HasPrefix(r.Host, domain)
}
// hostMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name.
type arbitraryMatcher string
func (m arbitraryMatcher) name() string {
return string(m)
}
func (m arbitraryMatcher) matches(r *http.Request, domain string) bool {
return strings.HasPrefix(r.Header.Get(m.name()), domain)
}
// forwardedMatcher checks whether the Forwarded header contains a "host" element starting with a domain name.
// See https://www.rfc-editor.org/rfc/rfc7239.html for details.
type forwardedMatcher struct{}
func (m *forwardedMatcher) name() string {
return "Forwarded"
}
func (m *forwardedMatcher) matches(r *http.Request, domain string) bool {
fwds, err := parseForwardedHeader(r.Header.Get(m.name()))
if err != nil {
return false
}
if len(fwds) == 0 {
return false
}
host := fwds[0]["host"]
return strings.HasPrefix(host, domain)
}
// parsing requires some form of state machine.
func parseForwardedHeader(s string) (elements []map[string]string, err error) {
cur := make(map[string]string)
key := ""
val := ""
inquote := false
pos := 0
l := len(s)
for i := 0; i < l; i++ {
r := rune(s[i])
if inquote {
if r == '"' {
cur[key] = s[pos:i]
key = ""
pos = i
inquote = false
}
continue
}
switch {
case r == '"': // start of quoted-string
if key == "" {
return nil, fmt.Errorf("unexpected quoted string as pos %d", i)
}
inquote = true
pos = i + 1
case r == ';': // end of forwarded-pair
cur[key] = s[pos:i]
key = ""
i = skipWS(s, i)
pos = i + 1
case r == '=': // end of token
key = strings.ToLower(strings.TrimFunc(s[pos:i], isWS))
i = skipWS(s, i)
pos = i + 1
case r == ',': // end of forwarded-element
if key != "" {
if val == "" {
val = s[pos:i]
}
cur[key] = val
}
elements = append(elements, cur)
cur = make(map[string]string)
key = ""
val = ""
i = skipWS(s, i)
pos = i + 1
case tchar(r) || isWS(r): // valid token character or whitespace
continue
default:
return nil, fmt.Errorf("invalid token character at pos %d: %c", i, r)
}
}
if inquote {
return nil, fmt.Errorf("unterminated quoted-string at pos %d", len(s))
}
if key != "" {
if pos < len(s) {
val = s[pos:]
}
cur[key] = val
}
if len(cur) > 0 {
elements = append(elements, cur)
}
return elements, nil
}
func tchar(r rune) bool {
return strings.ContainsRune("!#$%&'*+-.^_`|~", r) ||
'0' <= r && r <= '9' ||
'a' <= r && r <= 'z' ||
'A' <= r && r <= 'Z'
}
func skipWS(s string, i int) int {
for isWS(rune(s[i+1])) {
i++
}
return i
}
func isWS(r rune) bool {
return strings.ContainsRune(" \t\v\r\n", r)
}

View file

@ -0,0 +1,86 @@
package http01
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseForwardedHeader(t *testing.T) {
testCases := []struct {
name string
input string
want []map[string]string
err string
}{
{
name: "empty input",
input: "",
want: nil,
},
{
name: "simple case",
input: `for=1.2.3.4;host=example.com; by=127.0.0.1`,
want: []map[string]string{
{"for": "1.2.3.4", "host": "example.com", "by": "127.0.0.1"},
},
},
{
name: "quoted-string",
input: `foo="bar"`,
want: []map[string]string{
{"foo": "bar"},
},
},
{
name: "multiple entries",
input: `a=1, b=2; c=3, d=4`,
want: []map[string]string{
{"a": "1"},
{"b": "2", "c": "3"},
{"d": "4"},
},
},
{
name: "whitespace",
input: " a = 1,\tb\n=\r\n2,c=\" untrimmed \"",
want: []map[string]string{
{"a": "1"},
{"b": "2"},
{"c": " untrimmed "},
},
},
{
name: "unterminated quote",
input: `x="y`,
err: "unterminated quoted-string",
},
{
name: "unexpected quote",
input: `"x=y"`,
err: "unexpected quote",
},
{
name: "invalid token",
input: `a=b, ipv6=[fe80::1], x=y`,
err: "invalid token character at pos 10: [",
},
}
for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
actual, err := parseForwardedHeader(test.input)
if test.err == "" {
require.NoError(t, err)
assert.EqualValues(t, test.want, actual)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), test.err)
}
})
}
}

View file

@ -0,0 +1,65 @@
package http01
import (
"fmt"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/log"
)
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
// ChallengePath returns the URL path for the `http-01` challenge.
func ChallengePath(token string) string {
return "/.well-known/acme-challenge/" + token
}
type Challenge struct {
core *api.Core
validate ValidateFunc
provider challenge.Provider
}
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
return &Challenge{
core: core,
validate: validate,
provider: provider,
}
}
func (c *Challenge) SetProvider(provider challenge.Provider) {
c.provider = provider
}
func (c *Challenge) Solve(authz acme.Authorization) error {
domain := challenge.GetTargetedDomain(authz)
log.Infof("[%s] acme: Trying to solve HTTP-01", domain)
chlng, err := challenge.FindChallenge(challenge.HTTP01, authz)
if err != nil {
return err
}
// Generate the Key Authorization for the challenge
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)
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 {
log.Warnf("[%s] acme: cleaning up failed: %v", domain, err)
}
}()
chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng)
}

View file

@ -0,0 +1,137 @@
package http01
import (
"fmt"
"io/fs"
"net"
"net/http"
"net/textproto"
"os"
"strings"
"github.com/go-acme/lego/v4/log"
)
// ProviderServer implements ChallengeProvider for `http-01` challenge.
// It may be instantiated without using the NewProviderServer function if
// you want only to use the default values.
type ProviderServer struct {
address string
network string // must be valid argument to net.Listen
socketMode fs.FileMode
matcher domainMatcher
done chan bool
listener net.Listener
}
// NewProviderServer creates a new ProviderServer on the selected interface and port.
// Setting iface and / or port to an empty string will make the server fall back to
// the "any" interface and port 80 respectively.
func NewProviderServer(iface, port string) *ProviderServer {
if port == "" {
port = "80"
}
return &ProviderServer{network: "tcp", address: net.JoinHostPort(iface, port), matcher: &hostMatcher{}}
}
func NewUnixProviderServer(socketPath string, mode fs.FileMode) *ProviderServer {
return &ProviderServer{network: "unix", address: socketPath, socketMode: mode, matcher: &hostMatcher{}}
}
// 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)
}
if s.network == "unix" {
if err = os.Chmod(s.address, s.socketMode); err != nil {
return fmt.Errorf("chmod %s: %w", s.address, err)
}
}
s.done = make(chan bool)
go s.serve(domain, token, keyAuth)
return nil
}
func (s *ProviderServer) GetAddress() string {
return s.address
}
// CleanUp closes the HTTP server and removes the token from `ChallengePath(token)`.
func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {
if s.listener == nil {
return nil
}
s.listener.Close()
<-s.done
return nil
}
// SetProxyHeader changes the validation of incoming requests.
// By default, s matches the "Host" header value to the domain name.
//
// When the server runs behind a proxy server, this is not the correct place to look at;
// Apache and NGINX have traditionally moved the original Host header into a new header named "X-Forwarded-Host".
// Other webservers might use different names;
// and RFC7239 has standardized a new header named "Forwarded" (with slightly different semantics).
//
// The exact behavior depends on the value of headerName:
// - "" (the empty string) and "Host" will restore the default and only check the Host header
// - "Forwarded" will look for a Forwarded header, and inspect it according to https://www.rfc-editor.org/rfc/rfc7239.html
// - any other value will check the header value with the same name.
func (s *ProviderServer) SetProxyHeader(headerName string) {
switch h := textproto.CanonicalMIMEHeaderKey(headerName); h {
case "", "Host":
s.matcher = &hostMatcher{}
case "Forwarded":
s.matcher = &forwardedMatcher{}
default:
s.matcher = arbitraryMatcher(h)
}
}
func (s *ProviderServer) serve(domain, token, keyAuth string) {
path := ChallengePath(token)
// The incoming request will be validated to prevent DNS rebind attacks.
// We only respond with the keyAuth, when we're receiving a GET requests with
// the "Host" header matching the domain (the latter is configurable though SetProxyHeader).
mux := http.NewServeMux()
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && s.matcher.matches(r, domain) {
w.Header().Set("Content-Type", "text/plain")
_, err := w.Write([]byte(keyAuth))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Infof("[%s] Served key authentication", domain)
} else {
log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure you are passing the %s header properly.", r.Host, r.Method, s.matcher.name())
_, err := w.Write([]byte("TEST"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
})
httpServer := &http.Server{Handler: mux}
// Once httpServer is shut down
// we don't want any lingering connections, so disable KeepAlives.
httpServer.SetKeepAlivesEnabled(false)
err := httpServer.Serve(s.listener)
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
log.Println(err)
}
s.done <- true
}

View file

@ -0,0 +1,438 @@
package http01
import (
"context"
"crypto/rand"
"crypto/rsa"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"net/textproto"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/go-acme/lego/v4/acme"
"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/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProviderServer_GetAddress(t *testing.T) {
dir := t.TempDir()
t.Cleanup(func() { _ = os.RemoveAll(dir) })
sock := filepath.Join(dir, "var", "run", "test")
testCases := []struct {
desc string
server *ProviderServer
expected string
}{
{
desc: "TCP default address",
server: NewProviderServer("", ""),
expected: ":80",
},
{
desc: "TCP with explicit port",
server: NewProviderServer("", "8080"),
expected: ":8080",
},
{
desc: "TCP with host and port",
server: NewProviderServer("localhost", "8080"),
expected: "localhost:8080",
},
{
desc: "UDS socket",
server: NewUnixProviderServer(sock, fs.ModeSocket|0o666),
expected: sock,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
address := test.server.GetAddress()
assert.Equal(t, test.expected, address)
})
}
}
func TestChallenge(t *testing.T) {
_, apiURL := tester.SetupFakeAPI(t)
providerServer := NewProviderServer("", "23457")
validate := func(_ *api.Core, _ string, chlng acme.Challenge) error {
uri := "http://localhost" + providerServer.GetAddress() + ChallengePath(chlng.Token)
resp, err := http.DefaultClient.Get(uri)
if err != nil {
return err
}
defer resp.Body.Close()
if want := "text/plain"; resp.Header.Get("Content-Type") != want {
t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
bodyStr := string(body)
if bodyStr != chlng.KeyAuthorization {
t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization)
}
return nil
}
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(core, validate, providerServer)
authz := acme.Authorization{
Identifier: acme.Identifier{
Value: "localhost:23457",
},
Challenges: []acme.Challenge{
{Type: challenge.HTTP01.String(), Token: "http1"},
},
}
err = solver.Solve(authz)
require.NoError(t, err)
}
func TestChallengeUnix(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("only for UNIX systems")
}
_, apiURL := tester.SetupFakeAPI(t)
dir := t.TempDir()
t.Cleanup(func() { _ = os.RemoveAll(dir) })
socket := filepath.Join(dir, "lego-challenge-test.sock")
providerServer := NewUnixProviderServer(socket, fs.ModeSocket|0o666)
validate := func(_ *api.Core, _ string, chlng acme.Challenge) error {
// any uri will do, as we hijack the dial
uri := "http://localhost" + ChallengePath(chlng.Token)
client := &http.Client{Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial("unix", socket)
},
}}
resp, err := client.Get(uri)
if err != nil {
return err
}
defer resp.Body.Close()
if want := "text/plain"; resp.Header.Get("Content-Type") != want {
t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
bodyStr := string(body)
if bodyStr != chlng.KeyAuthorization {
t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization)
}
return nil
}
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(core, validate, providerServer)
authz := acme.Authorization{
Identifier: acme.Identifier{
Value: "localhost",
},
Challenges: []acme.Challenge{
{Type: challenge.HTTP01.String(), Token: "http1"},
},
}
err = solver.Solve(authz)
require.NoError(t, err)
}
func TestChallengeInvalidPort(t *testing.T) {
_, apiURL := tester.SetupFakeAPI(t)
privateKey, err := rsa.GenerateKey(rand.Reader, 128)
require.NoError(t, err, "Could not generate test key")
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 }
solver := NewChallenge(core, validate, NewProviderServer("", "123456"))
authz := acme.Authorization{
Identifier: acme.Identifier{
Value: "localhost:123456",
},
Challenges: []acme.Challenge{
{Type: challenge.HTTP01.String(), Token: "http2"},
},
}
err = solver.Solve(authz)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid port")
assert.Contains(t, err.Error(), "123456")
}
type testProxyHeader struct {
name string
values []string
}
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 != "" {
r.Header[h.name] = h.values
}
}
func TestChallengeWithProxy(t *testing.T) {
h := func(name string, values ...string) *testProxyHeader {
name = textproto.CanonicalMIMEHeaderKey(name)
return &testProxyHeader{name, values}
}
const (
ok = "localhost:23457"
nook = "example.com"
)
testCases := []struct {
name string
header *testProxyHeader
extra *testProxyHeader
isErr bool
}{
// tests for hostMatcher
{
name: "no proxy",
},
{
name: "empty string",
header: h(""),
},
{
name: "empty Host",
header: h("host"),
},
{
name: "matching Host",
header: h("host", ok),
},
{
name: "Host mismatch",
header: h("host", nook),
isErr: true,
},
{
name: "Host mismatch (ignoring forwarding header)",
header: h("host", nook),
extra: h("X-Forwarded-Host", ok),
isErr: true,
},
// test for arbitraryMatcher
{
name: "matching X-Forwarded-Host",
header: h("X-Forwarded-Host", ok),
},
{
name: "matching X-Forwarded-Host (multiple fields)",
header: h("X-Forwarded-Host", ok, nook),
},
{
name: "matching X-Forwarded-Host (chain value)",
header: h("X-Forwarded-Host", ok+", "+nook),
},
{
name: "X-Forwarded-Host mismatch",
header: h("X-Forwarded-Host", nook),
extra: h("host", ok),
isErr: true,
},
{
name: "X-Forwarded-Host mismatch (multiple fields)",
header: h("X-Forwarded-Host", nook, ok),
isErr: true,
},
{
name: "matching X-Something-Else",
header: h("X-Something-Else", ok),
},
{
name: "matching X-Something-Else (multiple fields)",
header: h("X-Something-Else", ok, nook),
},
{
name: "matching X-Something-Else (chain value)",
header: h("X-Something-Else", ok+", "+nook),
},
{
name: "X-Something-Else mismatch",
header: h("X-Something-Else", nook),
isErr: true,
},
{
name: "X-Something-Else mismatch (multiple fields)",
header: h("X-Something-Else", nook, ok),
isErr: true,
},
{
name: "X-Something-Else mismatch (chain value)",
header: h("X-Something-Else", nook+", "+ok),
isErr: true,
},
// tests for forwardedHeader
{
name: "matching Forwarded",
header: h("Forwarded", fmt.Sprintf("host=%q;foo=bar", ok)),
},
{
name: "matching Forwarded (multiple fields)",
header: h("Forwarded", fmt.Sprintf("host=%q", ok), "host="+nook),
},
{
name: "matching Forwarded (chain value)",
header: h("Forwarded", fmt.Sprintf("host=%q, host=%s", ok, nook)),
},
{
name: "Forwarded mismatch",
header: h("Forwarded", "host="+nook),
isErr: true,
},
{
name: "Forwarded mismatch (missing information)",
header: h("Forwarded", "for=127.0.0.1"),
isErr: true,
},
{
name: "Forwarded mismatch (multiple fields)",
header: h("Forwarded", "host="+nook, fmt.Sprintf("host=%q", ok)),
isErr: true,
},
{
name: "Forwarded mismatch (chain value)",
header: h("Forwarded", fmt.Sprintf("host=%s, host=%q", nook, ok)),
isErr: true,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
testServeWithProxy(t, test.header, test.extra, test.isErr)
})
}
}
func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectError bool) {
t.Helper()
_, apiURL := tester.SetupFakeAPI(t)
providerServer := NewProviderServer("localhost", "23457")
if header != nil {
providerServer.SetProxyHeader(header.name)
}
validate := func(_ *api.Core, _ string, chlng acme.Challenge) error {
uri := "http://" + providerServer.GetAddress() + ChallengePath(chlng.Token)
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil {
return err
}
header.update(req)
extra.update(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if want := "text/plain"; resp.Header.Get("Content-Type") != want {
return fmt.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
bodyStr := string(body)
if bodyStr != chlng.KeyAuthorization {
return fmt.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization)
}
return nil
}
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(core, validate, providerServer)
authz := acme.Authorization{
Identifier: acme.Identifier{
Value: "localhost:23457",
},
Challenges: []acme.Challenge{
{Type: challenge.HTTP01.String(), Token: "http1"},
},
}
err = solver.Solve(authz)
if expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}

View file

@ -1,28 +1,28 @@
package acme
package challenge
import "time"
// ChallengeProvider enables implementing a custom challenge
// Provider enables implementing a custom challenge
// provider. Present presents the solution to a challenge available to
// be solved. CleanUp will be called by the challenge if Present ends
// in a non-error state.
type ChallengeProvider interface {
type Provider interface {
Present(domain, token, keyAuth string) error
CleanUp(domain, token, keyAuth string) error
}
// ChallengeProviderTimeout allows for implementing a
// ChallengeProvider where an unusually long timeout is required when
// ProviderTimeout allows for implementing a
// Provider where an unusually long timeout is required when
// waiting for an ACME challenge to be satisfied, such as when
// checking for DNS record progagation. If an implementor of a
// ChallengeProvider provides a Timeout method, then the return values
// checking for DNS record propagation. If an implementor of a
// Provider provides a Timeout method, then the return values
// of the Timeout method will be used when appropriate by the acme
// package. The interval value is the time between checks.
//
// The default values used for timeout and interval are 60 seconds and
// 2 seconds respectively. These are used when no Timeout method is
// defined for the ChallengeProvider.
type ChallengeProviderTimeout interface {
ChallengeProvider
// defined for the Provider.
type ProviderTimeout interface {
Provider
Timeout() (timeout, interval time.Duration)
}

View file

@ -0,0 +1,25 @@
package resolver
import (
"bytes"
"fmt"
"sort"
)
// obtainError is returned when there are specific errors available per domain.
type obtainError map[string]error
func (e obtainError) Error() string {
buffer := bytes.NewBufferString("error: one or more domains had a problem:\n")
var domains []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()
}

View file

@ -0,0 +1,173 @@
package resolver
import (
"fmt"
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/log"
)
// Interface for all challenge solvers to implement.
type solver interface {
Solve(authorization acme.Authorization) error
}
// Interface for challenges like dns, where we can set a record in advance for ALL challenges.
// This saves quite a bit of time vs creating the records and solving them serially.
type preSolver interface {
PreSolve(authorization acme.Authorization) error
}
// Interface for challenges like dns, where we can solve all the challenges before to delete them.
type cleanup interface {
CleanUp(authorization acme.Authorization) error
}
type sequential interface {
Sequential() (bool, time.Duration)
}
// an authz with the solver we have chosen and the index of the challenge associated with it.
type selectedAuthSolver struct {
authz acme.Authorization
solver solver
}
type Prober struct {
solverManager *SolverManager
}
func NewProber(solverManager *SolverManager) *Prober {
return &Prober{
solverManager: solverManager,
}
}
// Solve Looks through the challenge combinations to find a solvable match.
// Then solves the challenges in series and returns.
func (p *Prober) Solve(authorizations []acme.Authorization) error {
failures := make(obtainError)
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 {
// Boulder might recycle recent validated authz (see issue #267)
log.Infof("[%s] acme: authorization already valid; skipping challenge", domain)
continue
}
if solvr := p.solverManager.chooseSolver(authz); solvr != nil {
authSolver := &selectedAuthSolver{authz: authz, solver: solvr}
switch s := solvr.(type) {
case sequential:
if ok, _ := s.Sequential(); ok {
authSolversSequential = append(authSolversSequential, authSolver)
} else {
authSolvers = append(authSolvers, authSolver)
}
default:
authSolvers = append(authSolvers, authSolver)
}
} else {
failures[domain] = fmt.Errorf("[%s] acme: could not determine solvers", domain)
}
}
parallelSolve(authSolvers, failures)
sequentialSolve(authSolversSequential, failures)
// Be careful not to return an empty failures map,
// for even an empty obtainError is a non-nil error value
if len(failures) > 0 {
return failures
}
return nil
}
func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
for i, authSolver := range authSolvers {
// Submit the challenge
domain := challenge.GetTargetedDomain(authSolver.authz)
if solvr, ok := authSolver.solver.(preSolver); ok {
err := solvr.PreSolve(authSolver.authz)
if err != nil {
failures[domain] = err
cleanUp(authSolver.solver, authSolver.authz)
continue
}
}
// Solve challenge
err := authSolver.solver.Solve(authSolver.authz)
if err != nil {
failures[domain] = err
cleanUp(authSolver.solver, authSolver.authz)
continue
}
// Clean challenge
cleanUp(authSolver.solver, authSolver.authz)
if 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) {
// For all valid preSolvers, first submit the challenges so they have max time to propagate
for _, authSolver := range authSolvers {
authz := authSolver.authz
if solvr, ok := authSolver.solver.(preSolver); ok {
err := solvr.PreSolve(authz)
if err != nil {
failures[challenge.GetTargetedDomain(authz)] = err
}
}
}
defer func() {
// Clean all created TXT records
for _, authSolver := range authSolvers {
cleanUp(authSolver.solver, authSolver.authz)
}
}()
// 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
continue
}
err := authSolver.solver.Solve(authz)
if err != nil {
failures[domain] = err
}
}
}
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

@ -0,0 +1,44 @@
package resolver
import (
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/challenge"
)
type preSolverMock struct {
preSolve map[string]error
solve map[string]error
cleanUp map[string]error
}
func (s *preSolverMock) PreSolve(authorization acme.Authorization) error {
return s.preSolve[authorization.Identifier.Value]
}
func (s *preSolverMock) Solve(authorization acme.Authorization) error {
return s.solve[authorization.Identifier.Value]
}
func (s *preSolverMock) CleanUp(authorization acme.Authorization) error {
return s.cleanUp[authorization.Identifier.Value]
}
func createStubAuthorizationHTTP01(domain, status string) acme.Authorization {
return acme.Authorization{
Status: status,
Expires: time.Now(),
Identifier: acme.Identifier{
Type: challenge.HTTP01.String(),
Value: domain,
},
Challenges: []acme.Challenge{
{
Type: challenge.HTTP01.String(),
Validated: time.Now(),
Error: nil,
},
},
}
}

View file

@ -0,0 +1,118 @@
package resolver
import (
"errors"
"testing"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/challenge"
"github.com/stretchr/testify/require"
)
func TestProber_Solve(t *testing.T) {
testCases := []struct {
desc string
solvers map[challenge.Type]solver
authz []acme.Authorization
expectedError string
}{
{
desc: "success",
solvers: map[challenge.Type]solver{
challenge.HTTP01: &preSolverMock{
preSolve: map[string]error{},
solve: map[string]error{},
cleanUp: map[string]error{},
},
},
authz: []acme.Authorization{
createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing),
},
},
{
desc: "already valid",
solvers: map[challenge.Type]solver{
challenge.HTTP01: &preSolverMock{
preSolve: map[string]error{},
solve: map[string]error{},
cleanUp: map[string]error{},
},
},
authz: []acme.Authorization{
createStubAuthorizationHTTP01("acme.wtf", acme.StatusValid),
createStubAuthorizationHTTP01("lego.wtf", acme.StatusValid),
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusValid),
},
},
{
desc: "when preSolve fail, auth is flagged as error and skipped",
solvers: map[challenge.Type]solver{
challenge.HTTP01: &preSolverMock{
preSolve: map[string]error{
"acme.wtf": errors.New("preSolve error acme.wtf"),
},
solve: map[string]error{
"acme.wtf": errors.New("solve error acme.wtf"),
},
cleanUp: map[string]error{
"acme.wtf": errors.New("clean error acme.wtf"),
},
},
},
authz: []acme.Authorization{
createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing),
},
expectedError: `error: one or more domains had a problem:
[acme.wtf] preSolve error acme.wtf
`,
},
{
desc: "errors at different stages",
solvers: map[challenge.Type]solver{
challenge.HTTP01: &preSolverMock{
preSolve: map[string]error{
"acme.wtf": errors.New("preSolve error acme.wtf"),
},
solve: map[string]error{
"acme.wtf": errors.New("solve error acme.wtf"),
"lego.wtf": errors.New("solve error lego.wtf"),
},
cleanUp: map[string]error{
"mydomain.wtf": errors.New("clean error mydomain.wtf"),
},
},
},
authz: []acme.Authorization{
createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing),
createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing),
},
expectedError: `error: one or more domains had a problem:
[acme.wtf] preSolve error acme.wtf
[lego.wtf] solve error lego.wtf
`,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
prober := &Prober{
solverManager: &SolverManager{solvers: test.solvers},
}
err := prober.Solve(test.authz)
if test.expectedError != "" {
require.EqualError(t, err, test.expectedError)
} else {
require.NoError(t, err)
}
})
}
}

View file

@ -0,0 +1,164 @@
package resolver
import (
"errors"
"fmt"
"sort"
"strconv"
"time"
"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"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/go-acme/lego/v4/log"
)
type byType []acme.Challenge
func (a byType) Len() int { return len(a) }
func (a byType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byType) Less(i, j int) bool { return a[i].Type > a[j].Type }
type SolverManager struct {
core *api.Core
solvers map[challenge.Type]solver
}
func NewSolversManager(core *api.Core) *SolverManager {
return &SolverManager{
solvers: map[challenge.Type]solver{},
core: core,
}
}
// SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge.
func (c *SolverManager) SetHTTP01Provider(p challenge.Provider) error {
c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p)
return nil
}
// SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge.
func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider) error {
c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p)
return nil
}
// SetDNS01Provider specifies a custom provider p that can solve the given DNS-01 challenge.
func (c *SolverManager) SetDNS01Provider(p challenge.Provider, opts ...dns01.ChallengeOption) error {
c.solvers[challenge.DNS01] = dns01.NewChallenge(c.core, validate, p, opts...)
return nil
}
// Remove removes a challenge type from the available solvers.
func (c *SolverManager) Remove(chlgType challenge.Type) {
delete(c.solvers, chlgType)
}
// Checks all challenges from the server in order and returns the first matching solver.
func (c *SolverManager) chooseSolver(authz acme.Authorization) solver {
// Allow to have a deterministic challenge order
sort.Sort(byType(authz.Challenges))
domain := challenge.GetTargetedDomain(authz)
for _, chlg := range authz.Challenges {
if solvr, ok := c.solvers[challenge.Type(chlg.Type)]; ok {
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)
}
return nil
}
func validate(core *api.Core, domain string, chlg acme.Challenge) error {
chlng, err := core.Challenges.New(chlg.URL)
if err != nil {
return fmt.Errorf("failed to initiate challenge: %w", err)
}
valid, err := checkChallengeStatus(chlng)
if err != nil {
return err
}
if valid {
log.Infof("[%s] The server validated our request", domain)
return nil
}
ra, err := strconv.Atoi(chlng.RetryAfter)
if err != nil {
// The ACME server MUST return a Retry-After.
// 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
ra = 5
}
initialInterval := time.Duration(ra) * time.Second
bo := backoff.NewExponentialBackOff()
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.
operation := func() error {
authz, err := core.Authorizations.Get(chlng.AuthorizationURL)
if err != nil {
return backoff.Permanent(err)
}
valid, err := checkAuthorizationStatus(authz)
if err != nil {
return backoff.Permanent(err)
}
if valid {
log.Infof("[%s] The server validated our request", domain)
return nil
}
return errors.New("the server didn't respond to our request")
}
return backoff.Retry(operation, bo)
}
func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
switch chlng.Status {
case acme.StatusValid:
return true, nil
case acme.StatusPending, acme.StatusProcessing:
return false, nil
case acme.StatusInvalid:
return false, chlng.Error
default:
return false, errors.New("the server returned an unexpected state")
}
}
func checkAuthorizationStatus(authz acme.Authorization) (bool, error) {
switch authz.Status {
case acme.StatusValid:
return true, nil
case acme.StatusPending, acme.StatusProcessing:
return false, nil
case acme.StatusDeactivated, acme.StatusExpired, acme.StatusRevoked:
return false, fmt.Errorf("the authorization state %s", authz.Status)
case acme.StatusInvalid:
for _, chlg := range authz.Challenges {
if chlg.Status == acme.StatusInvalid && chlg.Error != nil {
return false, chlg.Error
}
}
return false, fmt.Errorf("the authorization state %s", authz.Status)
default:
return false, errors.New("the server returned an unexpected state")
}
}

View file

@ -0,0 +1,178 @@
package resolver
import (
"crypto/rand"
"crypto/rsa"
"fmt"
"io"
"net/http"
"sort"
"testing"
"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-jose/go-jose/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestByType(t *testing.T) {
challenges := []acme.Challenge{
{Type: "dns-01"}, {Type: "tlsalpn-01"}, {Type: "http-01"},
}
sort.Sort(byType(challenges))
expected := []acme.Challenge{
{Type: "tlsalpn-01"}, {Type: "http-01"}, {Type: "dns-01"},
}
assert.Equal(t, expected, challenges)
}
func TestValidate(t *testing.T) {
mux, apiURL := tester.SetupFakeAPI(t)
var statuses []string
privateKey, _ := rsa.GenerateKey(rand.Reader, 512)
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
}
if err := validateNoBody(privateKey, r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Link", "<"+apiURL+`/my-authz>; rel="up"`)
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{}
}
err := tester.WriteJSONResponse(w, chlg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
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
}
st := statuses[0]
statuses = statuses[1:]
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 {
name string
statuses []string
want string
}{
{
name: "POST-unexpected",
statuses: []string{"weird"},
want: "unexpected",
},
{
name: "POST-valid",
statuses: []string{acme.StatusValid},
},
{
name: "POST-invalid",
statuses: []string{acme.StatusInvalid},
want: "error",
},
{
name: "POST-pending-unexpected",
statuses: []string{acme.StatusPending, "weird"},
want: "unexpected",
},
{
name: "POST-pending-valid",
statuses: []string{acme.StatusPending, acme.StatusValid},
},
{
name: "POST-pending-invalid",
statuses: []string{acme.StatusPending, acme.StatusInvalid},
want: "error",
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
statuses = test.statuses
err := validate(core, "example.com", acme.Challenge{Type: "http-01", Token: "token", URL: apiURL + "/chlg"})
if test.want == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), test.want)
}
})
}
}
// 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.
// We use this to verify challenge POSTs to the ts below do not send a JWS body.
func validateNoBody(privateKey *rsa.PrivateKey, r *http.Request) error {
reqBody, err := io.ReadAll(r.Body)
if err != nil {
return err
}
jws, err := jose.ParseSigned(string(reqBody))
if err != nil {
return err
}
body, err := jws.Verify(&jose.JSONWebKey{
Key: privateKey.Public(),
Algorithm: "RSA",
})
if err != nil {
return err
}
if bodyStr := string(body); bodyStr != "{}" && bodyStr != "" {
return fmt.Errorf(`expected JWS POST body "{}" or "", got %q`, bodyStr)
}
return nil
}

View file

@ -0,0 +1,129 @@
package tlsalpn01
import (
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/log"
)
// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension.
// Reference: https://www.rfc-editor.org/rfc/rfc8737.html#section-6.1
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 Challenge struct {
core *api.Core
validate ValidateFunc
provider challenge.Provider
}
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
return &Challenge{
core: core,
validate: validate,
provider: provider,
}
}
func (c *Challenge) SetProvider(provider challenge.Provider) {
c.provider = provider
}
// Solve manages the provider to validate and solve the challenge.
func (c *Challenge) Solve(authz acme.Authorization) error {
domain := authz.Identifier.Value
log.Infof("[%s] acme: Trying to solve TLS-ALPN-01", challenge.GetTargetedDomain(authz))
chlng, err := challenge.FindChallenge(challenge.TLSALPN01, authz)
if err != nil {
return err
}
// Generate the Key Authorization for the challenge
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
err = c.provider.Present(domain, chlng.Token, keyAuth)
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 {
log.Warnf("[%s] acme: cleaning up failed: %v", challenge.GetTargetedDomain(authz), err)
}
}()
chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng)
}
// ChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension
// and domain name for the `tls-alpn-01` challenge.
func ChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) {
// Compute the SHA-256 digest of the key authorization.
zBytes := sha256.Sum256([]byte(keyAuth))
value, err := asn1.Marshal(zBytes[:sha256.Size])
if err != nil {
return nil, nil, err
}
// Add the keyAuth digest as the acmeValidation-v1 extension
// (marked as critical such that it won't be used by non-ACME software).
// Reference: https://www.rfc-editor.org/rfc/rfc8737.html#section-3
extensions := []pkix.Extension{
{
Id: idPeAcmeIdentifierV1,
Critical: true,
Value: value,
},
}
// Generate a new RSA key for the certificates.
tempPrivateKey, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048)
if err != nil {
return nil, nil, err
}
rsaPrivateKey := tempPrivateKey.(*rsa.PrivateKey)
// Generate the PEM certificate using the provided private key, domain, and extra extensions.
tempCertPEM, err := certcrypto.GeneratePemCert(rsaPrivateKey, domain, extensions)
if err != nil {
return nil, nil, err
}
// Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair.
rsaPrivatePEM := certcrypto.PEMEncode(rsaPrivateKey)
return tempCertPEM, rsaPrivatePEM, nil
}
// ChallengeCert returns a certificate with the acmeValidation-v1 extension
// and domain name for the `tls-alpn-01` challenge.
func ChallengeCert(domain, keyAuth string) (*tls.Certificate, error) {
tempCertPEM, rsaPrivatePEM, err := ChallengeBlocks(domain, keyAuth)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(tempCertPEM, rsaPrivatePEM)
if err != nil {
return nil, err
}
return &cert, nil
}

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