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") }