--- Begin Message ---
- To: Debian Bug Tracking System <submit@bugs.debian.org>
- Subject: unblock: acmetool/0.0.59-1
- From: Peter Colberg <peter@colberg.org>
- Date: Fri, 26 May 2017 22:10:57 -0400
- Message-id: <20170527021057.5fbgbhvfgmhqbvrl@alcyone>
Package: release.debian.org
Severity: normal
User: release.debian.org@packages.debian.org
Usertags: unblock
Dear Debian Release Team,
Please accept my apology for the belated request:
unblock acmetool/0.0.59-1
Could you please unblock a new upstream bugfix release of acmetool, a
client for the Let’s Encrypt TLS certificate authority? This version
was uploaded to Debian unstable back in February, shortly after the
beginning of the full freeze [0].
The release comprises the following bug and usability fixes:
* Validate hostnames in 'acmetool want' [1]
* Allow environment variables to be passed to challenge hooks [2]
* Allow acmeapi to obtain new nonces if nonce pool is depleted [3]
* Don't attempt fdb permission tests on non-cgo builds [4]
* Add read/write timeouts to redirector server [5]
* Allow hidden files within the state directory [6]
Regards,
Peter
[0] https://tracker.debian.org/news/839171
[1] https://github.com/hlandau/acme/commit/96126c04eb76c1921127731ea3ae562a67459b2d
[2] https://github.com/hlandau/acme/commit/c8f5d91e3b1d5fab90fda1298a65f5f283555097
[3] https://github.com/hlandau/acme/commit/a087733bf7567b224b8d192e2747f794fc93a27c
[4] https://github.com/hlandau/acme/commit/ca02f4791ab63b92907c2dfcf7d1f9a1f62b7b87
[5] https://github.com/hlandau/acme/commit/b9637d98466b45de1b7fc848474d1fc10ef60667
[6] https://github.com/hlandau/acme/commit/677aa28007341961102375d45857e26fac149e80
diff -Nru acmetool-0.0.58/.travis/after_success acmetool-0.0.59/.travis/after_success
--- acmetool-0.0.58/.travis/after_success 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/.travis/after_success 2017-02-17 06:26:01.000000000 -0500
@@ -32,20 +32,25 @@
# Prepare Ubuntu PPA signing key.
echo Preparing Ubuntu PPA signing key...
-cd "$ACME_DIR/.travis"
-wget -c "https://www.devever.net/~hl/f/gnupg-ppa-data.tar.gz.enc"
-openssl enc -d -aes-128-cbc -md sha256 -salt -pass env:PPA_ENCRYPTION_PASS -in "gnupg-ppa-data.tar.gz.enc" -out "gnupg-ppa-data.tar.gz"
-tar xvf gnupg-ppa-data.tar.gz
-shred -u gnupg-ppa-data.tar.*
-cd "$ACME_DIR"
+wget -qO ppa-private.asc.enc "https://www.devever.net/~hl/f/ppa-private-${PPA_ENCRYPTION_ID}.asc.enc"
+export PPA_ENCRYPTION_ID=
+openssl enc -d -aes-128-cbc -md sha256 -salt -pass env:PPA_ENCRYPTION_PASS -in "ppa-private.asc.enc" -out "ppa-private.asc"
+export PPA_ENCRYPTION_PASS=
+shred -u ppa-private.asc.enc
export GNUPGHOME="$ACME_DIR/.travis/.gnupg"
+mkdir -p "$GNUPGHOME"
+gpg --batch --import < ppa-private.asc
+shred -u ppa-private.asc
+cat <<END | gpg --batch --import-ownertrust
+046B4FF0F9FD04C1F4662DE951107171B1D4C4C5:6:
+END
# Upload Ubuntu PPA package.
cat <<'END' > "$HOME/.devscripts"
-DEBSIGN_KEYID="Hugo Landau (2016 PPA Signing) <hlandau@devever.net>"
+DEBSIGN_KEYID="Hugo Landau (2017 PPA Signing) <hlandau@devever.net>"
END
-UBUNTU_RELEASES="xenial precise trusty vivid wily"
+UBUNTU_RELEASES="precise trusty xenial yakkety zesty vivid"
for distro_name in $UBUNTU_RELEASES; do
echo Creating Debian source environment for ${distro_name}...
$GOPATH/src/github.com/$TRAVIS_REPO_SLUG/.travis/make_debian_env "$GOPATH/releasing/dbuilds/$distro_name" "$GOPATH/releasing/dist/" "$TRAVIS_TAG" "$distro_name"
@@ -90,7 +95,7 @@
cat <<END > /tmp/rpm-metadata
{
"project_id": $COPR_PROJECT_ID,
- "chroots": ["fedora-23-i386", "fedora-23-x86_64", "epel-7-x86_64", "fedora-24-i386", "fedora-24-x86_64"]
+ "chroots": ["fedora-23-i386", "fedora-23-x86_64", "epel-7-x86_64", "fedora-24-i386", "fedora-24-x86_64", "fedora-25-i386", "fedora-25-x86_64", "fedora-26-i386", "fedora-26-x86_64"]
}
END
else
diff -Nru acmetool-0.0.58/.travis/boulder.patch acmetool-0.0.59/.travis/boulder.patch
--- acmetool-0.0.58/.travis/boulder.patch 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/.travis/boulder.patch 2017-02-17 06:26:01.000000000 -0500
@@ -11,7 +11,7 @@
# If we reach here, a child died early. Log what died:
diff --git a/test/config-next/va.json b/test/config-next/va.json
-index c237d7f..1336bb5 100644
+index 374ff68..4e701da 100644
--- a/test/config-next/va.json
+++ b/test/config-next/va.json
@@ -4,7 +4,7 @@
@@ -23,35 +23,42 @@
"httpsPort": 5001,
"tlsPort": 5001
},
-@@ -56,4 +56,4 @@
- "dnsTimeout": "10s",
- "dnsAllowLoopbackAddresses": true
- }
--}
-\ No newline at end of file
-+}
diff --git a/test/config/ca.json b/test/config/ca.json
-index a4d71c8..9057f6f 100644
+index eb6a2c1..7c6c0e3 100644
--- a/test/config/ca.json
+++ b/test/config/ca.json
-@@ -5,10 +5,10 @@
+@@ -5,11 +5,11 @@
"ecdsaProfile": "ecdsaEE",
- "debugAddr": "localhost:8001",
+ "debugAddr": ":8001",
"Issuers": [{
- "ConfigFile": "test/test-ca.key-pkcs11.json",
+ "File": "test/test-ca.key",
- "CertFile": "test/test-ca2.pem"
+ "CertFile": "test/test-ca2.pem",
+ "NumSessions": 2
}, {
- "ConfigFile": "test/test-ca.key-pkcs11.json",
+ "File": "test/test-ca.key",
- "CertFile": "test/test-ca.pem"
+ "CertFile": "test/test-ca.pem",
+ "NumSessions": 2
}],
- "expiry": "2160h",
+diff --git a/test/config/ra.json b/test/config/ra.json
+index a5cbe39..95e03b3 100644
+--- a/test/config/ra.json
++++ b/test/config/ra.json
+@@ -21,7 +21,7 @@
+ },
+ "SA": {
+ "server": "SA.server",
+- "rpcTimeout": "15s"
++ "rpcTimeout": "60s"
+ },
+ "CA": {
+ "server": "CA.server",
diff --git a/test/config/va.json b/test/config/va.json
-index 75ff959..371edf3 100644
+index 8d0fcef..4da51fc 100644
--- a/test/config/va.json
+++ b/test/config/va.json
-@@ -3,7 +3,7 @@
+@@ -4,7 +4,7 @@
"userAgent": "boulder",
"debugAddr": "localhost:8004",
"portConfig": {
@@ -60,13 +67,6 @@
"httpsPort": 5001,
"tlsPort": 5001
},
-@@ -37,4 +37,4 @@
- "dnsTimeout": "10s",
- "dnsAllowLoopbackAddresses": true
- }
--}
-\ No newline at end of file
-+}
diff --git a/test/hostname-policy.json b/test/hostname-policy.json
index 6397ee9..15ad50c 100644
--- a/test/hostname-policy.json
diff -Nru acmetool-0.0.58/.travis/check-copr-token acmetool-0.0.59/.travis/check-copr-token
--- acmetool-0.0.58/.travis/check-copr-token 1969-12-31 19:00:00.000000000 -0500
+++ acmetool-0.0.59/.travis/check-copr-token 2017-02-17 06:26:01.000000000 -0500
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -e
+TRAVIS_FILE="$(dirname "$0")/../.travis.yml"
+[ -e "$TRAVIS_FILE" ] || exit 1
+
+EXPIRY="$(grep 'COPR_LOGIN_TOKEN expires=' "$TRAVIS_FILE" | sed 's/^.*COPR_LOGIN_TOKEN expires=\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\)/\1/g')"
+
+EXPIRY_S="$(date -d "$EXPIRY" +%s)"
+NOW_S="$(date +%s)"
+
+if [ "$NOW_S" -ge "$EXPIRY_S" ]; then
+ echo >&2 "Outdated copr token. Renew it and update expiry date in .travis.yml."
+ exit 1
+fi
diff -Nru acmetool-0.0.58/.travis/script acmetool-0.0.59/.travis/script
--- acmetool-0.0.58/.travis/script 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/.travis/script 2017-02-17 06:26:01.000000000 -0500
@@ -33,7 +33,7 @@
# Start boulder.
export OBJDIR="$GOPATH/src/github.com/letsencrypt/boulder/bin"
-./start.py &> boulder.log &
+{ ./start.py &> boulder.log || cat boulder.log ; } &
START_PID=$$
# Wait for boulder to come up.
diff -Nru acmetool-0.0.58/.travis.yml acmetool-0.0.59/.travis.yml
--- acmetool-0.0.58/.travis.yml 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/.travis.yml 2017-02-17 06:26:01.000000000 -0500
@@ -51,10 +51,12 @@
global:
# GITHUB_TOKEN for automatic releases
- secure: "OA/Trkip03Ee3145oxrbHv3oM7dFpoX2h3y65CzyecQ2v8X4/l5pOwyMiJei5i20zm+QrK0iP9JttbDR9hY71d1DoxMXRGW0YHGFEutUQLZFpkPHLv7klSq8RjRGzpusSaxAtpEF27ZS+7NU42awYynWDzVsK4cglH9CimrS1glr2lKA5bXucqFROlqbI5GzXEdZJXhdGlKZWQWo83Hwe8JTwvIN8xRn5xZ33yxeMDl6SgQ3UhEs6zmsAQphGZ1pNcQaPjtyFtwEBeVQCsYW0loo8gUyjsfippSfGciu+g1J6sGVBj3HxGWWKmMa7lMaCEpL5CUKVcT2WH+LefYLHX5ZkyK8EQwt8QzrO1+X268+SulbWu2rf9SFQlLgoazIa8N8qfd8wVlo6Z3Jiy5YNHhHImMRYtgh5q3lo/5COUrPSgPBx4+VdciuMLxVYw96lTrPcMd4/J2gVYAf7f3AXeOpi/zF0T1WyD/64X0xKquYrbBzGbrEH4EM68vXQBiK5Q2sAEwhMUZNhgAqlKRzpqQoe/Cdx/Stm6cuFt6r87TbJfYiHGCZehveASWwH/Nk1HogOXjv/iVikxOqUiuqy0Q7GLPuFdcAGuLjqxS3wmdN1pBEGVqtSKA/3xrJptKlniz6+1hWr+H1ttTRTgok6ViX/POf+CW11VsfVo7qjyc="
+ # PPA_ENCRYPTION_ID
+ - secure: "oYuMlIP0jJZpvw1V6HKcieHW/HcYX2X+5znZ7lLcroyz3uW8ZtdRo0mDBFmSJuxpxWA/6uNdB/ReV5hhSBGM+XsIB04FAhgp6dOOT9Z7ncE92d4SBkofYh0Le7gX/2DbtsDXBWJt8RLrCbnh/b7Nu51XXELu4vFPrp9RB28iYiCZqJxnEFf/4XMoWsfV/qUL7xaa54KC3Fhmyx5TpTtneJemhkPHc91z2SFv/v//QON6h/HZla5jgu0Ncxm6sCzGvLI6Rp4UGT1x0jifzqJ4WwCOvLCdHwy2KOq0hJFrRybfgWgo8o36CT7uTmisanWNvI/kQMZr/WqvRP7+OXBrA9dnGX6TUpHW+nigq+AopIjAWkshKUZDL53oMl3zWUdryD36fjxSYnxHo4I/6ocoZFRCh/hSClLwNvDyjsugqQhBY6gUSlFItHyubdFV8L5r1ehhwafE6Mz9OqqVZhW3LAlUOhvKruv8WA7gGKYc2IwRNRCql/Glun7OZk2JB2SuwJnNCn63HqAAs1QMWHaHrFCeGLj8GqZM0P2dNXYfS2M/g1691l/IYtQLwNFCLmzBEdkNF2uytoqq+VGwZSx6waxCybWwI9selPjvFrWB9dk3WVjiDmg2g1qZshr0jPLaCBC5imw0oSobjV0lJefANeTsmrX6PAZlTbLZhjvclIg="
# PPA_ENCRYPTION_PASS
- - secure: "u9L0PymBiOKz1ylJIaUPzEicW55UZNoXCr8Kd8e0tRG1ABm1GQHC2BUM6AhhHiw33QE8uwe2qf2f5fBupoUsMRnoTh/EZDs8P1Iieg/3vcMZZLI77fQHpc4BcPbVGhHg+3vdR6jg4zRLNW7YLkPAgF9qj7Ezm2b+4MAp+A+9OChWpy1tdck9hftfhJ1ItkFDBufiqTLJEcwME8VgvKVz1zdKaNk3yX7wW4GDvxhuq4ZN6lyfOS6n1VIFWqXKuDWpVemM6ksEAWbdGh/9e9OYd/YxqDTZJT5+MTAUfAy+B00rB2BtR5+zZr9qPgvo5uSLAORrkr2lWRjHBTN1M8s682bry0zViUfMVKfPCGM2UUdGxtc1XQFDUNTi3+iWqQ6jHoeR+CyUxlD4O3F1NU9sHD4Z4mKfUkPfZkD9sy7+i3MojdCQlU9XmTTaxr4J68OwosOIWHUVtG9bNkyq1QhBlXgZOzwJLI47WJQfMoCctu6qG6uFyQ1RRVwZi7R5l5Fj1CvupBsC/BHxegt6+h6sD2gVASxH3oLKP41N8xZSVynd1EJhdPLRoZoGymEAAuplEnUu37BBJfTHxmtA8pu62TNgDjL36F5w+w/HH/lQRpeUUeyA96LSlNc/+gk4b6d5325pd0KlojHjDbU1JE6QYN6T7Xk7sQ0FS6Gxpmy/f4o="
- # COPR_LOGIN_TOKEN
- - secure: "LICnvsATVBSRC5AzjSy7Wszw01cm15R4VckS+NN7yxAQcyjYhHaQGbvLkymCc08psMq+KNDzeU+ZrKGwWZBjerlQqH39g6ookSRVwUCdXRw7w5K2SJSvlUlTEW9kQYdCKqLFpkRd/4wW6XPUuSSYbQIkOyYOcNnf7h8usVzn3CQjjBnkQFjiqtf4GfNFdDChT8Hi8uQfN9KNRDyKxBzvA6f3b9VtjbctbCAUY7/1x/8YZxBkiTTsFe2H8zP8agqOxFO+8gJc+lffrOJXytqcoRC0Kd1jmwHm8aot/PvSkpDmWhaJqKaFrC7lVX7V7LLaNFkI+7Tsw5RHsF+0S+bNVM24YR+YVLJdwjBTdkp+PyHv2wvFAjcc589ujdjz/sdtzVeCeL878Ger76PHs2X25LnYAkjgHqi/YtqLAGzRhqiS8MAmGopv6ju3eyE0sylIAmIVXsf6GP2paw5KELXlVe9AtdyiB/xh+y3yzElxjoRX37rjPFd5ErInYki9rbdGkgRf3fySJsbHp3RKHR+x7TPO8zw8kmrnj7HD9+5l24lD6Zngoxr0rPYo6jastE729BIC4dUEWiw39HBLsUczL4/vatL12P4kdpBUQE1lp4BOKow3z20Rd69ujZOmsiNznX5aEJjcWcesdlbU1XsKknu1d640WysovU1lbKI85Js="
+ - secure: "Edr/h71sDFi2aXxICO3Ij5twLl/83HEwTgWfQ6/dJ7BcavjONTDyzB8cNQ0dGjlljujtbyyoD0+89Wu5pVotkv49JUZpQoWOJdn/9kyxFi9u61cpABSZvU/Sr1pWkOkDra7oAxgcJTAwNg5j1OVJ3+wfxJGGRVTotqPXc+hpIKx6z7jKR22D0Adz4uu1hWzRMdw8Qp8opqBJG2YHwvIF51U/Ztz4FcNwq1LJ1kdZ5YJYvU4SG6zm9+Q2XdjNQivLPuMdNL+s5Ik6J8Iiftu/OvxsSdfPClxyg0r8VCnoM8vpPAJc0BAOo6FBwUFLHfhFkUHUuLtZR/gyh5zkTd7fhRvdM/Sc94Dd9r2PeN8Jh5sTpn5a8/Qyhq/JItjcuRBB0Ysl4cZR81eIvPMeW4R3cnZ5mTA3rOpYjswiWAxBvJ6ZCOmGbtDG3lTkMUZ8Po6DmTqXMRRfWa/Nsuju5360UC65Q7mmHZx+hOTgeDw1LlMEhcG+ac2QH/FbnVM/SnRsYw+y5QORWJlFMcqPCwsGEVD2FxkuxX/tOtbIdyyBvQNEbdx+3/NpmwmUnQgH0v4i0o6rlQ65ETw6CdMNt9P+RuhRvrisbDvm/lwwfPT2IJenElB6Xu3Xz/i2WbAty92XJYfxpiIz1Rpivfu89OsyqKsMKzmhOqSfq6W2QxPuW8k="
+ # COPR_LOGIN_TOKEN expires=2017-08-16
+ - secure: "pjZpulkzB+g5p4lRzNUPybIt5IgWSJAidubbyiHypzoUI5voVnVXl1upv3nbDg2RTPFNvIKblB9H5i0kF2p5Dd5iPo/xA1QwrhgKjnhHOzYCIYwgHj5pXk+ZGVx0RLoLOePWGqeVomsjR6p5rqrG1jOPhUhoiu7q5scDTUUnBYJw3bZNmN0qiARxk89htzbsVMBYRQXdMt6Y2mbrQig09rCAw2GosAHnG0hr5kBlEv6tXhHxR1vuCUwLkzZQZJq0c5E1pDgFBqeB1/Yyzq8VtnnBR97cVvLT+SaMiwsRasx7rjAR4aUeTM6AIE3ALRPJcrg+85RThwyhOVW4yJWSWBfkWEqVrTpMOifLZ9ZaxpdKIcywBLYfYxaAJ8zjdD5N/4grLK6pl0dapapQ1n0XRufKGwpD9rBYZ61E8yAgfCZERCmq0MfpBYOY/x/Jg8m37nZRDrU6C31nOE47MJ+w4qo031igJ7YuKjcK38e5tEZWjFmP9+41vkYIfzI537VcwyLg4NouvJPgxYIkBoqJ5pa7khsRdaATP4DL2cqVcHiYHZdyUodqa0Ik9+jNdvRrOZn7aYcMbCIwzSgijesH0ItmS6AsFYzts5bwPJqlsQR5vQhn68CaA7qTZ0kSLIOfjCITxOKBut4YO8kkZrrSzspLx79nj9CMu6xkun/2iZs="
branches:
only:
diff -Nru acmetool-0.0.58/_doc/SCHEMA.md acmetool-0.0.59/_doc/SCHEMA.md
--- acmetool-0.0.58/_doc/SCHEMA.md 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/_doc/SCHEMA.md 2017-02-17 06:26:01.000000000 -0500
@@ -271,6 +271,14 @@
- 402
- 4402
+ # Defaults to true. If false, will not perform self-test but will assume
+ # challenge can be completed. Rarely needed.
+ http-self-test: true
+
+ # Optionally set environment variables to be passed to hooks.
+ env:
+ FOO: BAR
+
### accounts
An ACME State Directory MUST contain a subdirectory "accounts" which contains
diff -Nru acmetool-0.0.58/_doc/dns.hook acmetool-0.0.59/_doc/dns.hook
--- acmetool-0.0.58/_doc/dns.hook 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/_doc/dns.hook 2017-02-17 06:26:01.000000000 -0500
@@ -36,7 +36,8 @@
echo "$0: couldn't get apex for $name" >&2
return 1
fi
- if [ "`dig +noall +answer SOA "${name}." |grep SOA|wc -l`" == "1" ]; then
+ local ans="`dig +noall +answer SOA "${name}."`"
+ if [ "`echo "$ans" | grep SOA | wc -l`" == "1" -a "`echo "$ans" | grep CNAME | wc -l`" == "0" ]; then
APEX="$name"
return
fi
@@ -61,7 +62,7 @@
updns() {
local op="$1"
(
- declare -f nsupdate_cmds >/dev/null && nsupdate_cmds
+ declare -f nsupdate_cmds >/dev/null && nsupdate_cmds "$APEX"
[ -n "$TKIP_KEY" ] && echo key "$TKIP_KEY_NAME" "$TKIP_KEY"
echo $op "_acme-challenge.${CH_HOSTNAME}." 60 IN TXT "\"${CH_TXT_VALUE}\""
echo send
diff -Nru acmetool-0.0.58/_doc/response-file.yaml acmetool-0.0.59/_doc/response-file.yaml
--- acmetool-0.0.58/_doc/response-file.yaml 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/_doc/response-file.yaml 2017-02-17 06:26:01.000000000 -0500
@@ -7,7 +7,7 @@
# For dialogs not requiring a response, but merely acknowledgement, specify true.
# This file is YAML. Note that JSON is a subset of YAML.
"acme-enter-email": "hostmaster@example.com"
-"acme-agreement:https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf": true
+"acme-agreement:https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf": true
"acmetool-quickstart-choose-server": https://acme-staging.api.letsencrypt.org/directory
"acmetool-quickstart-choose-method": redirector
# This is only used if "acmetool-quickstart-choose-method" is "webroot".
diff -Nru acmetool-0.0.58/_doc/tinydns.hook acmetool-0.0.59/_doc/tinydns.hook
--- acmetool-0.0.58/_doc/tinydns.hook 1969-12-31 19:00:00.000000000 -0500
+++ acmetool-0.0.59/_doc/tinydns.hook 2017-02-17 06:26:01.000000000 -0500
@@ -0,0 +1,220 @@
+#!/bin/sh
+set -e
+# This is a DNS hook that updated the tinydns (djbdns/dbndns) database. For a
+# small period (default 90 secs), waits for dns propagation. On fail, reverts.
+# Uses dig for resolution.
+#
+# Tries to figure out your tinydns root directory (overwrite if necessary).
+# When the root directory contains a Makefile, invokes make(1), else
+# tinydns-data(8). That way, you can notify downstream DNS server, eg with
+# http://tindyns.org/dnsnotify
+#
+# Copy, move, or link this script to $ACME_HOOKS_DIR/tinydns
+#
+# You can test this script with
+# ./tinydns.hook challenge-dns-start example.com "" "deadbeef"
+# ./tinydns.hook challenge-dns-stop example.com "" "deadbeef"
+#
+# This script reads /etc/default/acme-tinydns and /etc/conf.d/acme-tinydns
+# You can override the following variables there:
+#
+# DNS_SYNC_MAX Maximum time in seconds to wait for DNS propagation
+# (default 90)
+# SERVICE_ROOT Directory that contains daemontools(8) services
+# (default one of /service /etc/service /etc/sv)
+# SERVICE Directory with the tinydns(8) service for
+# daemontools(8) (default ${SERVICE_ROOT}/tinydns)
+# SERVICE_ENV Directory with the envdir(8) environment for the
+# tinydns(8) service, if used. (default ${SERVICE}/en)
+# ROOT Directory containing tinydns(8)'s data, especially
+# the `data` file. (default: when ${SERVICE_ENV}/ROOT
+# is a file, its contents, otherwise ${SERVICE}/root)
+#
+EXIT_UNKNOWN_EVENT="42"
+DATA_MARKER_START='# -- ACMETOOL TINYDNS HOOK START --'
+DATA_MARKER_STOP='# -- ACMETOOL TINYDNS HOOK STOP --'
+
+# return 1 or 0 whether the given command exists
+have_command() { command -v "${1}" 2>&1 >/dev/null; }
+
+# strips everything before second-level-domain. TDLs not supported.
+get_domain() {
+ echo "${1}" | sed -e 's/^\([^.]\{1,\}\.\)\{0,\}\([^.]\{1,\}\.[^.]\{1,\}\.\{0,1\}\)$/\2/'
+}
+
+# get primary dns server, prefer the one we are provisioning
+get_ns() {
+ if [ -e "${SERVICE_ENV}/IP" ]; then
+ cat "${SERVICE_ENV}/IP"
+ else
+ DOMAIN="$(get_domain "${1}")"
+ dig +short SOA "${DOMAIN}" | cut -d' ' -f1
+ fi
+}
+
+get_all_ns() {
+ DOMAIN="$(get_domain "${1}")"
+ dig +short NS "${DOMAIN}"
+}
+
+# parse dnsq/dnsqr/tinydns-get output (we care for 1st field of data)
+#answer: example.com ttl RECORD data
+parse_dnsq() { grep '^answer: ' | cut -d' ' -f5; }
+
+# parse DNS TXT record that still contains length prepended (as dnsq)
+parse_dnstxt() { sed -e 's/^\(\\[[:digit:]]\{3\}\)\|.//'; }
+
+# parse DNS TXT record still with quotes (as dig)
+parse_digtxt() { TXT="${1#\"}"; echo "${TXT%\"}"; }
+
+# Get content of given TXT record via DNS (opt from SERVER)
+get_txt() {
+ TXT_HOST="${1}"
+ SERVER="${2}"
+ if [ -z "${SERVER}" ]; then
+ parse_digtxt "$(dig +short TXT "${TXT_HOST}")"
+ else
+ parse_digtxt "$(dig +short "@${SERVER}" TXT "${TXT_HOST}")"
+ fi
+}
+
+controls_domain() (
+ cd "${ROOT}"
+ tinydns-get soa "${1}" | grep -q '^answer:'
+ # if no answer, then no control
+)
+
+# set all variable we need and such
+prepare() {
+ # set reliable path
+ PATH="$(command -p getconf PATH):${PATH}"
+ # add /command if available
+ [ -d /command ] && PATH="/command:${PATH}"
+
+ # make sure we all commands we need
+ for cmd in tinydns-get dig sleep sed grep cut mv wait echo; do
+ have_command "${cmd}"
+ done
+
+ # find tinydns root
+ for CANDIDATE in "${SERVICE_ROOT}" /service /etc/service /etc/sv; do
+ if [ -d "${CANDIDATE}" ]; then
+ SERVICE_ROOT="${CANDIDATE}"; break
+ fi
+ done
+ SERVICE="${SERVICE:-${SERVICE_ROOT}/tinydns}"
+ SERVICE_ENV="${SERVICE_ENV:-${SERVICE}/env}"
+ if [ -z "${ROOT}" ]; then
+ if [ -f "${SERVICE_ENV}/ROOT" ]; then
+ ROOT="$(cat "${SERVICE_ENV}/ROOT")"
+ else
+ ROOT="${SERVICE}/root"
+ fi
+ fi
+ # no tinydns root, no operation
+ [ -d "${ROOT}" ] || exit 1
+}
+
+# Get content of given TXT record via database
+get_txt_record() (
+ cd "${ROOT}"
+ tinydns-get txt "${1}" | parse_dnsq | parse_dnstxt
+)
+
+# write txt record to database
+set_txt_record() (
+ cd "${ROOT}"
+ if grep -q "${DATA_MARKER_START}" data; then :; else
+ echo "${DATA_MARKER_START}" >> data
+ echo "${DATA_MARKER_STOP}" >> data
+ fi
+ sed -e "/${DATA_MARKER_STOP}/i\'${1}:${2}:300" data > data.acmetmp \
+ && mv data.acmetmp data
+)
+
+# remove txt record from database
+del_txt_record() (
+ cd "${ROOT}"
+ sed -e "/^'${1}:${2}/d" data > data.acmetmp \
+ && mv data.acmetmp data
+)
+
+# update tinydns database (aka commit)
+update() (
+ cd "${ROOT}"
+ if have_command make && [ -f Makefile ]; then
+ make
+ else
+ tinydns-data
+ fi
+)
+
+# reload database and check this worked via DNS
+reload() (
+ TXT_HOST="${1}"
+ CHALLENGE="${2}"
+ update
+
+ index="${DNS_SYNC_MAX:-90}"
+ export NS_STATUS=1
+ get_all_ns "${TXT_HOST}" | while read NAMESERVER; do
+ while [ "${index}" -gt 0 ]; do
+ sleep 5 &
+ if [ -z "${CHALLENGE}" ]; then
+ if [ -z "$(get_txt "${TXT_HOST}" "${NAMESERVER}")" ]; then export NS_STATUS=0; break; fi
+ else
+ if [ "$(get_txt "${TXT_HOST}" "${NAMESERVER}")" = "${CHALLENGE}" ]; then NS_STATUS=0; break; fi
+ fi
+ index="$((${index} - 5))"
+ wait
+ done
+ [ "${NS_STATUS}" -eq 0 ] || return 1 # reached here because of timeout
+ done
+ return 0
+)
+
+# CALLBACK: insert acme challange
+start() {
+ HOST="${1}"; DOMAIN="${2}"; CHALLENGE="${3}"
+ TXT_HOST="_acme-challenge.${HOST}"
+ TXT_RECORD="$(get_txt_record "${TXT_HOST}" )"
+ [ "${TXT_RECORD}" = "${CHALLENGE}" ] && return 0 # challenge already there
+ [ -z "${TXT_RECORD}" ] # challenge not empty, doesn't match ours
+ set_txt_record "${TXT_HOST}" "${CHALLENGE}"
+ reload "${TXT_HOST}" "${CHALLENGE}" \
+ || (del_txt_record "${TXT_HOST}"; update; return 1)
+}
+
+# CALLBACK: remove acme challange
+stop() {
+ HOST="${1}"; DOMAIN="${2}"; CHALLENGE="${3}"
+ TXT_HOST="_acme-challenge.${HOST}"
+ [ "$(get_txt_record "${TXT_HOST}" )" = "${CHALLENGE}" ]
+ del_txt_record "${TXT_HOST}"
+ reload "${TXT_HOST}" "" \
+ || (set_txt_record "${TXT_HOST}" "${CHALLENGE}" ; update; return 1)
+}
+
+# include configuration from known locations
+[ -e "/etc/default/acme-tinydns" ] && . /etc/default/acme-tinydns
+[ -e "/etc/conf.d/acme-tinydns" ] && . /etc/conf.d/acme-tinydns
+
+# Contract is:
+# ACME_STATE_DIR=/var/lib/acme /usr/lib/acme/hooks/tinydns \
+# challenge-dns-start hostname.example.com target_file challenge
+EVENT="${1}"
+HOST="${2}"
+TARGET_FILE="${3}"
+CHALLENGE="${4}"
+
+case "${EVENT}" in
+ challenge-dns-*)
+ prepare
+ DOMAIN="$(get_domain ${HOST})"
+ controls_domain "${DOMAIN}"
+ "${EVENT##challenge-dns-}" "${HOST}" "${DOMAIN}" "${CHALLENGE}"
+ ;;
+ *)
+ exit "${EXIT_UNKNOWN_EVENT}"
+ ;;
+esac
diff -Nru acmetool-0.0.58/acmeapi/acmeutils/hostname.go acmetool-0.0.59/acmeapi/acmeutils/hostname.go
--- acmetool-0.0.58/acmeapi/acmeutils/hostname.go 1969-12-31 19:00:00.000000000 -0500
+++ acmetool-0.0.59/acmeapi/acmeutils/hostname.go 2017-02-17 06:26:01.000000000 -0500
@@ -0,0 +1,33 @@
+package acmeutils
+
+import (
+ "fmt"
+ "golang.org/x/net/idna"
+ "regexp"
+ "strings"
+)
+
+var reHostname = regexp.MustCompilePOSIX(`^([a-z0-9_-]+\.)*[a-z0-9_-]+$`)
+
+// Normalizes the hostname given. If the hostname is not valid, returns "" and
+// an error.
+func NormalizeHostname(name string) (string, error) {
+ name = strings.TrimSuffix(strings.ToLower(name), ".")
+
+ name, err := idna.ToASCII(name)
+ if err != nil {
+ return "", fmt.Errorf("IDN error: %#v: %v", name, err)
+ }
+
+ if !reHostname.MatchString(name) {
+ return "", fmt.Errorf("invalid hostname: %#v", name)
+ }
+
+ return name, nil
+}
+
+// Returns true iff the given string is a valid hostname.
+func ValidateHostname(name string) bool {
+ _, err := NormalizeHostname(name)
+ return err == nil
+}
diff -Nru acmetool-0.0.58/acmeapi/api.go acmetool-0.0.59/acmeapi/api.go
--- acmetool-0.0.58/acmeapi/api.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/acmeapi/api.go 2017-02-17 06:26:01.000000000 -0500
@@ -92,9 +92,10 @@
// Uses http.DefaultClient if nil.
HTTPClient *http.Client
- dir *directoryInfo
- nonceSource nonceSource
- initOnce sync.Once
+ dir *directoryInfo
+ nonceSource nonceSource
+ nonceReentrant int
+ initOnce sync.Once
}
// You should set this to a string identifying the code invoking this library.
@@ -126,6 +127,17 @@
}
}
+func (c *Client) obtainNewNonce(ctx context.Context) error {
+ if c.nonceReentrant > 0 {
+ panic("nonce reentrancy - this should never happen")
+ }
+ c.nonceReentrant++
+ defer func() { c.nonceReentrant-- }()
+
+ _, err := c.forceGetDirectory(ctx)
+ return err
+}
+
func (c *Client) doReqEx(method, url string, key crypto.PrivateKey, v, r interface{}, ctx context.Context) (*http.Response, error) {
if !ValidURL(url) {
return nil, fmt.Errorf("invalid URL: %#v", url)
@@ -135,6 +147,8 @@
key = c.AccountKey
}
+ c.nonceSource.GetNonceFunc = c.obtainNewNonce
+
var rdr io.Reader
if v != nil {
b, err := json.Marshal(v)
@@ -156,7 +170,7 @@
return nil, err
}
- signer.SetNonceSource(&c.nonceSource)
+ signer.SetNonceSource(c.nonceSource.WithContext(ctx))
sig, err := signer.Sign(b)
if err != nil {
@@ -217,11 +231,7 @@
return ctxhttp.Do(ctx, c.HTTPClient, req)
}
-func (c *Client) getDirectory(ctx context.Context) (*directoryInfo, error) {
- if c.dir != nil {
- return c.dir, nil
- }
-
+func (c *Client) forceGetDirectory(ctx context.Context) (*directoryInfo, error) {
if c.DirectoryURL == "" {
return nil, fmt.Errorf("must specify a directory URL")
}
@@ -239,6 +249,14 @@
return c.dir, nil
}
+func (c *Client) getDirectory(ctx context.Context) (*directoryInfo, error) {
+ if c.dir != nil {
+ return c.dir, nil
+ }
+
+ return c.forceGetDirectory(ctx)
+}
+
// API Methods
var newRegCodes = []int{201, 409}
diff -Nru acmetool-0.0.58/acmeapi/api_test.go acmetool-0.0.59/acmeapi/api_test.go
--- acmetool-0.0.58/acmeapi/api_test.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/acmeapi/api_test.go 2017-02-17 06:26:01.000000000 -0500
@@ -4,11 +4,14 @@
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
+ "encoding/hex"
"encoding/json"
"fmt"
"github.com/hlandau/goutils/test"
"github.com/hlandau/xlog"
+ "github.com/square/go-jose"
"golang.org/x/net/context"
+ "io/ioutil"
"net/http"
"reflect"
"testing"
@@ -26,6 +29,49 @@
},
}
+ issuedNonces := map[string]struct{}{}
+ issueNonce := func() string {
+ var b [8]byte
+ _, err := rand.Read(b[:])
+ if err != nil {
+ panic(err)
+ }
+
+ s := fmt.Sprintf("nonce-%s", hex.EncodeToString(b[:]))
+ issuedNonces[s] = struct{}{}
+ return s
+ }
+
+ checkNonce := func(rw http.ResponseWriter, req *http.Request) bool {
+ b, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ log.Fatalf("cannot read body: %v", err)
+ }
+
+ jws, err := jose.ParseSigned(string(b))
+ if err != nil {
+ log.Fatalf("malformed request body: %v", err)
+ }
+
+ if len(jws.Signatures) != 1 {
+ log.Fatalf("wrong number of signatures: %v", err)
+ }
+
+ n := jws.Signatures[0].Header.Nonce
+
+ _, ok := issuedNonces[n]
+ if !ok {
+ rw.Header().Set("Content-Type", "application/json")
+ rw.WriteHeader(400)
+ rw.Write([]byte(`{"type":"bad-nonce","message":"Bad nonce."}`))
+ t.Logf("invalid nonce: %#v", n)
+ t.Fail()
+ return false
+ }
+ delete(issuedNonces, n)
+ return true
+ }
+
// Load Certificate
mt.Add("boulder.test/acme/cert/some-certificate", &http.Response{
@@ -48,7 +94,7 @@
StatusCode: 200,
Header: http.Header{
"Content-Type": []string{"application/pkix-cert"},
- "Replay-Nonce": []string{"some-nonce-root"},
+ //"Replay-Nonce": []string{"some-nonce-root"},
},
}, []byte("root-cert-data"))
@@ -143,18 +189,17 @@
// Request Certificate
- mt.Add("boulder.test/directory", &http.Response{
- StatusCode: 200,
- Header: http.Header{
- "Content-Type": []string{"application/json"},
- "Replay-Nonce": []string{"foo-nonce"},
- },
- }, []byte(`{
- "new-reg": "https://boulder.test/acme/new-reg",
- "new-cert": "https://boulder.test/acme/new-cert",
- "new-authz": "https://boulder.test/acme/new-authz",
- "revoke-cert": "https://boulder.test/acme/revoke-cert"
- }`))
+ mt.AddHandlerFunc("boulder.test/directory", func(rw http.ResponseWriter, req *http.Request) {
+ rw.Header().Set("Content-Type", "application/json")
+ rw.Header().Set("Replay-Nonce", issueNonce())
+ rw.WriteHeader(200)
+ rw.Write([]byte(`{
+ "new-reg": "https://boulder.test/acme/new-reg",
+ "new-cert": "https://boulder.test/acme/new-cert",
+ "new-authz": "https://boulder.test/acme/new-authz",
+ "revoke-cert": "https://boulder.test/acme/revoke-cert"
+ }`))
+ })
mt.AddHandlerFunc("boulder.test/acme/new-cert", func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Location", "https://boulder.test/acme/cert/some-certificate")
@@ -186,15 +231,16 @@
// Upsert Registration
- i := 0
mt.AddHandlerFunc("boulder.test/acme/new-reg", func(rw http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
t.Fatal()
}
+ if !checkNonce(rw, req) {
+ return
+ }
rw.Header().Set("Location", "https://boulder.test/acme/reg/1")
- rw.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", i))
- i++
+ rw.Header().Set("Replay-Nonce", issueNonce())
rw.WriteHeader(409)
})
@@ -202,9 +248,11 @@
if req.Method != "POST" {
t.Fatal()
}
+ if !checkNonce(rw, req) {
+ return
+ }
- rw.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", i))
- i++
+ rw.Header().Set("Replay-Nonce", issueNonce())
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set("Link", "<urn:some:boulder:terms/of/service>; rel=\"terms-of-service\"")
rw.WriteHeader(200)
@@ -227,16 +275,28 @@
}
// New Authorization
+ e503Count := 0
+ total503 := 3
mt.AddHandlerFunc("boulder.test/acme/new-authz", func(rw http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
t.Fatal()
}
+ if !checkNonce(rw, req) {
+ return
+ }
- rw.Header().Set("Location", "https://boulder.test/acme/authz/1")
- rw.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", i))
rw.Header().Set("Content-Type", "application/json")
- i++
+
+ if e503Count < total503 {
+ rw.WriteHeader(503)
+ rw.Write([]byte(`{"type":"urn:acme:error:serverInternal","detail":"Down"}`))
+ e503Count++
+ return
+ }
+
+ rw.Header().Set("Location", "https://boulder.test/acme/authz/1")
+ rw.Header().Set("Replay-Nonce", issueNonce())
rw.WriteHeader(201)
rw.Write([]byte(`{
"challenges": [
@@ -256,13 +316,19 @@
})
mt.AddHandlerFunc("boulder.test/acme/challenge/some-challenge2", func(rw http.ResponseWriter, req *http.Request) {
- rw.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", i))
- i++
+ rw.Header().Set("Replay-Nonce", issueNonce())
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(200)
rw.Write([]byte(`{}`))
})
+ for i := 0; i < total503; i++ {
+ az, err = cl.NewAuthorization("example.com", context.TODO())
+ if err == nil {
+ t.Fatalf("no error when expected")
+ }
+ }
+
az, err = cl.NewAuthorization("example.com", context.TODO())
if err != nil {
t.Fatalf("%v", err)
@@ -277,8 +343,11 @@
if req.Method != "POST" {
t.Fatal()
}
- rw.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", i))
- i++
+ if !checkNonce(rw, req) {
+ return
+ }
+
+ rw.Header().Set("Replay-Nonce", issueNonce())
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(200)
rw.Write([]byte(`{}`))
diff -Nru acmetool-0.0.58/acmeapi/nonce.go acmetool-0.0.59/acmeapi/nonce.go
--- acmetool-0.0.58/acmeapi/nonce.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/acmeapi/nonce.go 2017-02-17 06:26:01.000000000 -0500
@@ -1,10 +1,13 @@
package acmeapi
-import "errors"
+import (
+ "errors"
+ "golang.org/x/net/context"
+)
type nonceSource struct {
pool map[string]struct{}
- GetNonceFunc func() (string, error)
+ GetNonceFunc func(ctx context.Context) error
}
func (ns *nonceSource) init() {
@@ -15,7 +18,7 @@
ns.pool = map[string]struct{}{}
}
-func (ns *nonceSource) Nonce() (string, error) {
+func (ns *nonceSource) Nonce(ctx context.Context) (string, error) {
ns.init()
var k string
@@ -23,22 +26,44 @@
break
}
if k == "" {
- return ns.obtainNonce()
+ err := ns.obtainNonce(ctx)
+ if err != nil {
+ return "", err
+ }
+ for k = range ns.pool {
+ break
+ }
+ if k == "" {
+ return "", errors.New("failed to retrieve additional nonce")
+ }
}
delete(ns.pool, k)
return k, nil
}
-func (ns *nonceSource) obtainNonce() (string, error) {
+func (ns *nonceSource) obtainNonce(ctx context.Context) error {
if ns.GetNonceFunc == nil {
- return "", errors.New("out of nonces - this should never happen")
+ return errors.New("out of nonces - this should never happen")
}
- return ns.GetNonceFunc()
+ return ns.GetNonceFunc(ctx)
}
func (ns *nonceSource) AddNonce(nonce string) {
ns.init()
ns.pool[nonce] = struct{}{}
}
+
+func (ns *nonceSource) WithContext(ctx context.Context) *nonceSourceWithCtx {
+ return &nonceSourceWithCtx{ns, ctx}
+}
+
+type nonceSourceWithCtx struct {
+ nonceSource *nonceSource
+ ctx context.Context
+}
+
+func (nc *nonceSourceWithCtx) Nonce() (string, error) {
+ return nc.nonceSource.Nonce(nc.ctx)
+}
diff -Nru acmetool-0.0.58/acmeapi/nonce_test.go acmetool-0.0.59/acmeapi/nonce_test.go
--- acmetool-0.0.58/acmeapi/nonce_test.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/acmeapi/nonce_test.go 2017-02-17 06:26:01.000000000 -0500
@@ -1,11 +1,15 @@
package acmeapi
-import "testing"
+import (
+ "golang.org/x/net/context"
+ "testing"
+)
func TestNonce(t *testing.T) {
ns := nonceSource{}
ns.AddNonce("my-nonce")
- n, err := ns.Nonce()
+ nsc := ns.WithContext(context.TODO())
+ n, err := nsc.Nonce()
if err != nil {
t.Fatal()
}
@@ -13,16 +17,17 @@
t.Fatal()
}
- n, err = ns.Nonce()
+ n, err = nsc.Nonce()
if err == nil {
t.Fatal()
}
- ns.GetNonceFunc = func() (string, error) {
- return "nonce2", nil
+ ns.GetNonceFunc = func(ctx context.Context) error {
+ ns.AddNonce("nonce2")
+ return nil
}
- n, err = ns.Nonce()
+ n, err = nsc.Nonce()
if err != nil {
t.Fatal()
}
diff -Nru acmetool-0.0.58/cmd/acmetool/main.go acmetool-0.0.59/cmd/acmetool/main.go
--- acmetool-0.0.58/cmd/acmetool/main.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/cmd/acmetool/main.go 2017-02-17 06:26:01.000000000 -0500
@@ -65,9 +65,11 @@
quickstartCmd = kingpin.Command("quickstart", "Interactively ask some getting started questions (recommended)")
expertFlag = quickstartCmd.Flag("expert", "Ask more questions in quickstart wizard").Bool()
- redirectorCmd = kingpin.Command("redirector", "HTTP to HTTPS redirector with challenge response support")
- redirectorPathFlag = redirectorCmd.Flag("path", "Path to serve challenge files from").String()
- redirectorGIDFlag = redirectorCmd.Flag("challenge-gid", "GID to chgrp the challenge path to (optional)").String()
+ redirectorCmd = kingpin.Command("redirector", "HTTP to HTTPS redirector with challenge response support")
+ redirectorPathFlag = redirectorCmd.Flag("path", "Path to serve challenge files from").String()
+ redirectorGIDFlag = redirectorCmd.Flag("challenge-gid", "GID to chgrp the challenge path to (optional)").String()
+ redirectorReadTimeout = redirectorCmd.Flag("read-timeout", "Maximum duration before timing out read of the request (default: '10s')").Default("10s").Duration()
+ redirectorWriteTimeout = redirectorCmd.Flag("write-timeout", "Maximum duration before timing out write of the request (default: '20s')").Default("20s").Duration()
testNotifyCmd = kingpin.Command("test-notify", "Test-execute notification hooks as though given hostnames were updated")
testNotifyArg = testNotifyCmd.Arg("hostname", "hostnames which have been updated").Strings()
@@ -305,6 +307,20 @@
}
func cmdWant() {
+ hostnames := *wantArg
+
+ // Ensure all hostnames provided are valid.
+ for idx := range hostnames {
+ norm, err := acmeutils.NormalizeHostname(hostnames[idx])
+ if err != nil {
+ log.Fatalf("invalid hostname: %#v: %v", hostnames[idx], err)
+ return
+ }
+ hostnames[idx] = norm
+ }
+
+ // Determine whether there already exists a target satisfying all given
+ // hostnames or a superset thereof.
s, err := storage.NewFDB(*stateFlag)
log.Fatale(err, "storage")
@@ -315,7 +331,7 @@
nm[n] = struct{}{}
}
- for _, w := range *wantArg {
+ for _, w := range hostnames {
if _, ok := nm[w]; !ok {
return nil
}
@@ -329,9 +345,10 @@
return
}
+ // Add the target.
tgt := storage.Target{
Satisfy: storage.TargetSatisfy{
- Names: *wantArg,
+ Names: hostnames,
},
}
@@ -366,6 +383,8 @@
Bind: ":80",
ChallengePath: rpath,
ChallengeGID: *redirectorGIDFlag,
+ ReadTimeout: *redirectorReadTimeout,
+ WriteTimeout: *redirectorWriteTimeout,
})
},
})
@@ -384,7 +403,11 @@
}
func cmdRunTestNotify() {
- err := hooks.NotifyLiveUpdated(*hooksFlag, *stateFlag, *testNotifyArg)
+ ctx := &hooks.Context{
+ HooksDir: *hooksFlag,
+ StateDir: *stateFlag,
+ }
+ err := hooks.NotifyLiveUpdated(ctx, *testNotifyArg)
log.Errore(err, "notify")
}
diff -Nru acmetool-0.0.58/debian/changelog acmetool-0.0.59/debian/changelog
--- acmetool-0.0.58/debian/changelog 2017-01-08 23:50:30.000000000 -0500
+++ acmetool-0.0.59/debian/changelog 2017-02-19 22:41:49.000000000 -0500
@@ -1,3 +1,18 @@
+acmetool (0.0.59-1) unstable; urgency=medium
+
+ * New upstream release
+ - Validate hostnames in 'acmetool want'
+ - Allow environment variables to be passed to challenge hooks
+ - Allow acmeapi to obtain new nonces if nonce pool is depleted
+ - Don't attempt fdb permission tests on non-cgo builds
+ - Add read/write timeouts to redirector server
+ - Allow hidden files within the state directory
+
+ [ Peter Colberg ]
+ * Fix import path of square/go-jose
+
+ -- Peter Colberg <peter@colberg.org> Sun, 19 Feb 2017 22:41:49 -0500
+
acmetool (0.0.58-5) unstable; urgency=medium
* Rewrite README.Debian
diff -Nru acmetool-0.0.58/debian/patches/fix-import-path-of-square-go-jose.patch acmetool-0.0.59/debian/patches/fix-import-path-of-square-go-jose.patch
--- acmetool-0.0.58/debian/patches/fix-import-path-of-square-go-jose.patch 1969-12-31 19:00:00.000000000 -0500
+++ acmetool-0.0.59/debian/patches/fix-import-path-of-square-go-jose.patch 2017-02-19 22:41:49.000000000 -0500
@@ -0,0 +1,18 @@
+Description: Fix import path of square/go-jose
+Author: Peter Colberg <peter@colberg.org>
+Forwarded: https://github.com/hlandau/acme/pull/242
+Applied-Upstream: https://github.com/hlandau/acme/commit/9cb3aa47c8786ccff014149e8db1b6b2872476f7
+Last-Update: 2017-02-19
+---
+This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
+--- a/acmeapi/api_test.go
++++ b/acmeapi/api_test.go
+@@ -9,7 +9,7 @@ import (
+ "fmt"
+ "github.com/hlandau/goutils/test"
+ "github.com/hlandau/xlog"
+- "github.com/square/go-jose"
++ "gopkg.in/square/go-jose.v1"
+ "golang.org/x/net/context"
+ "io/ioutil"
+ "net/http"
diff -Nru acmetool-0.0.58/debian/patches/parseperm-test-cgo.patch acmetool-0.0.59/debian/patches/parseperm-test-cgo.patch
--- acmetool-0.0.58/debian/patches/parseperm-test-cgo.patch 2016-11-25 23:28:31.000000000 -0500
+++ acmetool-0.0.59/debian/patches/parseperm-test-cgo.patch 1969-12-31 19:00:00.000000000 -0500
@@ -1,14 +0,0 @@
-Description: Skip parseperm test if cgo is disabled
-Author: Peter Colberg <peter@colberg.org>
-Bug: https://github.com/hlandau/acme/issues/219
-Last-Update: 2016-11-20
----
-This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
---- a/fdb/parseperm_test.go
-+++ b/fdb/parseperm_test.go
-@@ -1,3 +1,5 @@
-+// +build cgo
-+
- package fdb
-
- import (
diff -Nru acmetool-0.0.58/debian/patches/series acmetool-0.0.59/debian/patches/series
--- acmetool-0.0.58/debian/patches/series 2016-11-25 23:28:31.000000000 -0500
+++ acmetool-0.0.59/debian/patches/series 2017-02-19 22:41:49.000000000 -0500
@@ -1,3 +1,3 @@
go-1.6-text-template.patch
license.patch
-parseperm-test-cgo.patch
+fix-import-path-of-square-go-jose.patch
diff -Nru acmetool-0.0.58/fdb/fdb.go acmetool-0.0.59/fdb/fdb.go
--- acmetool-0.0.58/fdb/fdb.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/fdb/fdb.go 2017-02-17 06:26:01.000000000 -0500
@@ -224,6 +224,10 @@
return
}
+func isHiddenRelPath(rp string) bool {
+ return strings.HasPrefix(rp, ".") || strings.Index(rp, "/.") >= 0
+}
+
// Change all directory permissions to be correct.
func (db *DB) conformPermissions() error {
err := filepath.Walk(db.path, func(path string, info os.FileInfo, err error) error {
@@ -236,6 +240,18 @@
return err
}
+ // Some people want to store hidden files/directories inside the ACME state
+ // directory without permissions enforcement. Since it's reasonable to
+ // assume I'll never want to amend the ACME-SSS specification to specify
+ // top-level directories inside a state directory, this shouldn't have any
+ // security implications. Symlinks inside the state directory (whose state
+ // directory paths themselves don't contain "/." and are thus ignored)
+ // cannot reference ignored paths, as their permissions are not managed and
+ // this is not safe. This is enforced elsewhere.
+ if isHiddenRelPath(rpath) {
+ return nil
+ }
+
mode := info.Mode()
switch mode & os.ModeType {
case 0:
@@ -265,6 +281,14 @@
return fmt.Errorf("database symlinks must point to within the database directory: %v: %v", path, ll)
}
+ rll, err := filepath.Rel(db.path, ll)
+ if err != nil {
+ return err
+ }
+ if isHiddenRelPath(rll) {
+ return fmt.Errorf("database symlinks cannot target hidden files within the database directory: %v: %v", path, ll)
+ }
+
_, err = os.Stat(ll)
if os.IsNotExist(err) {
log.Warnf("broken symlink, removing: %v -> %v", path, l)
diff -Nru acmetool-0.0.58/fdb/parseperm_test.go acmetool-0.0.59/fdb/parseperm_test.go
--- acmetool-0.0.58/fdb/parseperm_test.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/fdb/parseperm_test.go 2017-02-17 06:26:01.000000000 -0500
@@ -1,3 +1,5 @@
+// +build cgo
+
package fdb
import (
diff -Nru acmetool-0.0.58/hooks/hooks.go acmetool-0.0.59/hooks/hooks.go
--- acmetool-0.0.58/hooks/hooks.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/hooks/hooks.go 2017-02-17 06:26:01.000000000 -0500
@@ -24,6 +24,18 @@
// changed at runtime.
var DefaultPath string
+// Provides contextual configuration information when executing a hook.
+type Context struct {
+ // The hook directory to use. May be "" for the default.
+ HooksDir string
+
+ // The state directory to report. Required.
+ StateDir string
+
+ // Arbitrary environment variables to set.
+ Env map[string]string
+}
+
func init() {
// Allow overriding at build time.
p := DefaultPath
@@ -43,13 +55,13 @@
//
// If hookDirectory is "", DefaultHookPath is used. stateDirectory and
// hostnames are passed as information to the hooks.
-func NotifyLiveUpdated(hookDirectory, stateDirectory string, hostnames []string) error {
+func NotifyLiveUpdated(ctx *Context, hostnames []string) error {
if len(hostnames) == 0 {
return nil
}
hostnameList := strings.Join(hostnames, "\n") + "\n"
- _, err := runParts(hookDirectory, stateDirectory, []byte(hostnameList), "live-updated")
+ _, err := runParts(ctx, []byte(hostnameList), "live-updated")
if err != nil {
return err
}
@@ -62,40 +74,67 @@
// installed indicates whether at least one hook script indicated success. err
// could still be returned in this case if an error occurs while executing some
// other hook.
-func ChallengeHTTPStart(hookDirectory, stateDirectory, hostname, targetFileName, token, ka string) (installed bool, err error) {
- return runParts(hookDirectory, stateDirectory, []byte(ka),
+func ChallengeHTTPStart(ctx *Context, hostname, targetFileName, token, ka string) (installed bool, err error) {
+ return runParts(ctx, []byte(ka),
"challenge-http-start", hostname, targetFileName, token)
}
-func ChallengeHTTPStop(hookDirectory, stateDirectory, hostname, targetFileName, token, ka string) error {
- _, err := runParts(hookDirectory, stateDirectory, []byte(ka),
+func ChallengeHTTPStop(ctx *Context, hostname, targetFileName, token, ka string) error {
+ _, err := runParts(ctx, []byte(ka),
"challenge-http-stop", hostname, targetFileName, token)
return err
}
-func ChallengeTLSSNIStart(hookDirectory, stateDirectory, hostname, targetFileName, validationName1, validationName2 string, pem string) (installed bool, err error) {
- return runParts(hookDirectory, stateDirectory, []byte(pem),
+func ChallengeTLSSNIStart(ctx *Context, hostname, targetFileName, validationName1, validationName2 string, pem string) (installed bool, err error) {
+ return runParts(ctx, []byte(pem),
"challenge-tls-sni-start", hostname, targetFileName, validationName1, validationName2)
}
-func ChallengeTLSSNIStop(hookDirectory, stateDirectory, hostname, targetFileName, validationName1, validationName2 string, pem string) (installed bool, err error) {
- return runParts(hookDirectory, stateDirectory, []byte(pem),
+func ChallengeTLSSNIStop(ctx *Context, hostname, targetFileName, validationName1, validationName2 string, pem string) (installed bool, err error) {
+ return runParts(ctx, []byte(pem),
"challenge-tls-sni-stop", hostname, targetFileName, validationName1, validationName2)
}
-func ChallengeDNSStart(hookDirectory, stateDirectory, hostname, targetFileName, body string) (installed bool, err error) {
- return runParts(hookDirectory, stateDirectory, nil,
+func ChallengeDNSStart(ctx *Context, hostname, targetFileName, body string) (installed bool, err error) {
+ return runParts(ctx, nil,
"challenge-dns-start", hostname, targetFileName, body)
}
-func ChallengeDNSStop(hookDirectory, stateDirectory, hostname, targetFileName, body string) (uninstalled bool, err error) {
- return runParts(hookDirectory, stateDirectory, nil,
+func ChallengeDNSStop(ctx *Context, hostname, targetFileName, body string) (uninstalled bool, err error) {
+ return runParts(ctx, nil,
"challenge-dns-stop", hostname, targetFileName, body)
}
+func mergeEnvMap(m map[string]string, e []string) {
+ for _, x := range e {
+ parts := strings.SplitN(x, "=", 2)
+ if len(parts) < 2 {
+ continue
+ }
+ m[parts[0]] = parts[1]
+ }
+}
+
+func flattenEnvMap(m map[string]string) []string {
+ var e []string
+ for k, v := range m {
+ e = append(e, k+"="+v)
+ }
+ return e
+}
+
+func mergeEnv(envs ...[]string) []string {
+ m := map[string]string{}
+ for _, env := range envs {
+ mergeEnvMap(m, env)
+ }
+ return flattenEnvMap(m)
+}
+
// Implements functionality similar to the "run-parts" command on many distros.
// Implementations vary, so it is reimplemented here.
-func runParts(directory, stateDirectory string, stdinData []byte, args ...string) (anySucceeded bool, err error) {
+func runParts(ctx *Context, stdinData []byte, args ...string) (anySucceeded bool, err error) {
+ directory := ctx.HooksDir
if directory == "" {
directory = DefaultPath
}
@@ -110,12 +149,7 @@
return false, err
}
- // Probably shouldn't propagate this to all child processes, but it's the
- // easiest way to not replace the entire environment when calling.
- err = os.Setenv("ACME_STATE_DIR", stateDirectory)
- if err != nil {
- return false, err
- }
+ env := mergeEnv(os.Environ(), flattenEnvMap(ctx.Env), []string{"ACME_STATE_DIR=" + ctx.StateDir})
// Do not execute a world-writable directory.
if (fi.Mode() & 02) != 0 {
@@ -174,6 +208,7 @@
}
cmd.Dir = "/"
+ cmd.Env = env
pipeR, pipeW, err := os.Pipe()
if err != nil {
diff -Nru acmetool-0.0.58/hooks/hooks_test.go acmetool-0.0.59/hooks/hooks_test.go
--- acmetool-0.0.58/hooks/hooks_test.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/hooks/hooks_test.go 2017-02-17 06:26:01.000000000 -0500
@@ -60,7 +60,11 @@
os.Remove(filepath.Join(dir, "log"))
- err = NotifyLiveUpdated(notifyDir, dir, []string{"a.b", "c.d", "e.f.g"})
+ ctx := &Context{
+ HooksDir: notifyDir,
+ StateDir: dir,
+ }
+ err = NotifyLiveUpdated(ctx, []string{"a.b", "c.d", "e.f.g"})
if err != nil {
t.Fatal(err)
}
diff -Nru acmetool-0.0.58/redirector/redirector.go acmetool-0.0.59/redirector/redirector.go
--- acmetool-0.0.58/redirector/redirector.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/redirector/redirector.go 2017-02-17 06:26:01.000000000 -0500
@@ -22,9 +22,11 @@
// Configuration for redirector.
type Config struct {
- Bind string `default:":80" usage:"Bind address"`
- ChallengePath string `default:"" usage:"Path containing HTTP challenge files"`
- ChallengeGID string `default:"" usage:"GID to chgrp the challenge path to (optional)"`
+ Bind string `default:":80" usage:"Bind address"`
+ ChallengePath string `default:"" usage:"Path containing HTTP challenge files"`
+ ChallengeGID string `default:"" usage:"GID to chgrp the challenge path to (optional)"`
+ ReadTimeout time.Duration `default:"" usage:"Maximum duration before timing out read of the request"`
+ WriteTimeout time.Duration `default:"" usage:"Maximum duration before timing out write of the response"`
}
// Simple HTTP to HTTPS redirector.
@@ -43,7 +45,9 @@
Timeout: 100 * time.Millisecond,
NoSignalHandling: true,
Server: &http.Server{
- Addr: cfg.Bind,
+ Addr: cfg.Bind,
+ ReadTimeout: cfg.ReadTimeout,
+ WriteTimeout: cfg.WriteTimeout,
},
},
}
diff -Nru acmetool-0.0.58/storage/types.go acmetool-0.0.59/storage/types.go
--- acmetool-0.0.58/storage/types.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/storage/types.go 2017-02-17 06:26:01.000000000 -0500
@@ -139,6 +139,11 @@
// N. Perform HTTP self-test? Defaults to true. Rarely needed. If disabled,
// HTTP challenges will be performed without self-testing.
HTTPSelfTest *bool `yaml:"http-self-test,omitempty"`
+
+ // N. Environment variables to pass to hooks.
+ Env map[string]string `yaml:"env,omitempty"`
+ // N. Inherited environment variables. Used internally.
+ InheritedEnv map[string]string `yaml:"-"`
}
// Represents a stored target descriptor.
@@ -202,6 +207,14 @@
// just copy the value. If Target is ever changed to reference any component
// of itself via pointer, this must be changed!
tt := *t
+ tt.Request.Challenge.InheritedEnv = map[string]string{}
+ for k, v := range t.Request.Challenge.InheritedEnv {
+ tt.Request.Challenge.InheritedEnv[k] = v
+ }
+ for k, v := range t.Request.Challenge.Env {
+ tt.Request.Challenge.InheritedEnv[k] = v
+ }
+ tt.Request.Challenge.Env = nil
return &tt
}
diff -Nru acmetool-0.0.58/storage/util.go acmetool-0.0.59/storage/util.go
--- acmetool-0.0.58/storage/util.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/storage/util.go 2017-02-17 06:26:01.000000000 -0500
@@ -9,7 +9,7 @@
"crypto/x509"
"encoding/base32"
"fmt"
- "golang.org/x/net/idna"
+ "github.com/hlandau/acme/acmeapi/acmeutils"
"io"
"math/big"
"net/url"
@@ -228,12 +228,6 @@
return re_certID.MatchString(certificateID)
}
-var re_hostname = regexp.MustCompilePOSIX(`^([a-z0-9_-]+\.)*[a-z0-9_-]+$`)
-
-func validHostname(name string) bool {
- return re_hostname.MatchString(name)
-}
-
func targetGt(a *Target, b *Target) bool {
if a == nil && b == nil {
return false // equal
@@ -263,15 +257,9 @@
func normalizeNames(names []string) error {
for i := range names {
- n := strings.TrimSuffix(strings.ToLower(names[i]), ".")
-
- n, err := idna.ToASCII(n)
+ n, err := acmeutils.NormalizeHostname(names[i])
if err != nil {
- return fmt.Errorf("IDN error: %v", err)
- }
-
- if !validHostname(n) {
- return fmt.Errorf("invalid hostname: %q", n)
+ return err
}
names[i] = n
diff -Nru acmetool-0.0.58/storageops/reconcile.go acmetool-0.0.59/storageops/reconcile.go
--- acmetool-0.0.58/storageops/reconcile.go 2016-09-03 08:30:08.000000000 -0400
+++ acmetool-0.0.59/storageops/reconcile.go 2017-02-17 06:26:01.000000000 -0500
@@ -118,7 +118,12 @@
}
}
- err = hooks.NotifyLiveUpdated("", r.store.Path(), updatedHostnames) // ignore error
+ ctx := &hooks.Context{
+ HooksDir: "",
+ StateDir: r.store.Path(),
+ }
+
+ err = hooks.NotifyLiveUpdated(ctx, updatedHostnames) // ignore error
log.Errore(err, "failed to call notify hooks")
return nil
@@ -428,10 +433,22 @@
func (r *reconcile) obtainAuthorization(name string, a *storage.Account, targetFilename string, trc *storage.TargetRequestChallenge) error {
cl := r.getClientForAccount(a)
+ ctx := &hooks.Context{
+ HooksDir: "",
+ StateDir: r.store.Path(),
+ Env: map[string]string{},
+ }
+ for k, v := range trc.InheritedEnv {
+ ctx.Env[k] = v
+ }
+ for k, v := range trc.Env {
+ ctx.Env[k] = v
+ }
+
startHookFunc := func(challengeInfo interface{}) error {
switch v := challengeInfo.(type) {
case *responder.HTTPChallengeInfo:
- _, err := hooks.ChallengeHTTPStart("", r.store.Path(), name, targetFilename, v.Filename, v.Body)
+ _, err := hooks.ChallengeHTTPStart(ctx, name, targetFilename, v.Filename, v.Body)
return err
case *responder.TLSSNIChallengeInfo:
hookPEM, err := generateHookPEM(v)
@@ -439,10 +456,10 @@
return err
}
- _, err = hooks.ChallengeTLSSNIStart("", r.store.Path(), name, targetFilename, v.Hostname1, v.Hostname2, hookPEM)
+ _, err = hooks.ChallengeTLSSNIStart(ctx, name, targetFilename, v.Hostname1, v.Hostname2, hookPEM)
return err
case *responder.DNSChallengeInfo:
- installed, err := hooks.ChallengeDNSStart("", r.store.Path(), name, targetFilename, v.Body)
+ installed, err := hooks.ChallengeDNSStart(ctx, name, targetFilename, v.Body)
if err == nil && !installed {
return fmt.Errorf("could not install DNS challenge, no hooks succeeded")
}
@@ -455,17 +472,17 @@
stopHookFunc := func(challengeInfo interface{}) error {
switch v := challengeInfo.(type) {
case *responder.HTTPChallengeInfo:
- return hooks.ChallengeHTTPStop("", r.store.Path(), name, targetFilename, v.Filename, v.Body)
+ return hooks.ChallengeHTTPStop(ctx, name, targetFilename, v.Filename, v.Body)
case *responder.TLSSNIChallengeInfo:
hookPEM, err := generateHookPEM(v)
if err != nil {
return err
}
- _, err = hooks.ChallengeTLSSNIStop("", r.store.Path(), name, targetFilename, v.Hostname1, v.Hostname2, hookPEM)
+ _, err = hooks.ChallengeTLSSNIStop(ctx, name, targetFilename, v.Hostname1, v.Hostname2, hookPEM)
return err
case *responder.DNSChallengeInfo:
- uninstalled, err := hooks.ChallengeDNSStop("", r.store.Path(), name, targetFilename, v.Body)
+ uninstalled, err := hooks.ChallengeDNSStop(ctx, name, targetFilename, v.Body)
if err == nil && !uninstalled {
return fmt.Errorf("could not uninstall DNS challenge, no hooks succeeded")
}
--- End Message ---