[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#863453: marked as done (unblock: acmetool/0.0.59-1)



Your message dated Sat, 3 Jun 2017 23:17:41 +0200
with message-id <20170603211739.GA12418@ugent.be>
and subject line Re: Bug#863453: unblock: acmetool/0.0.59-1
has caused the Debian Bug report #863453,
regarding unblock: acmetool/0.0.59-1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
863453: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863453
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
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 ---
--- Begin Message ---
Hi,

On Tue, May 30, 2017 at 11:05:56PM +0100, Jonathan Wiltshire wrote:
> I'm erring on the side of deferring all of these and cherry-picking them if
> real-world issues get reported for stable. It's an awful lot of changes for
> this late in the process and not really suitable.

Closing this bug report, as we're too close to the release for this now.

Cheers,

Ivo

--- End Message ---

Reply to: