Package: release.debian.org
Severity: normal
User: release.debian....@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) <hlan...@devever.net>"
+DEBSIGN_KEYID="Hugo Landau (2017 PPA Signing) <hlan...@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": "hostmas...@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 <pe...@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 <pe...@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 <pe...@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")
 			}

Reply via email to