pax_global_header00006660000000000000000000000064132125662540014520gustar00rootroot0000000000000052 comment=2c2e661404c54478d8086ae8f792de0fdddbaed0 acme-0.0.62/000077500000000000000000000000001321256625400125125ustar00rootroot00000000000000acme-0.0.62/.travis.yml000066400000000000000000000101601321256625400146210ustar00rootroot00000000000000language: go go: - 1.x addons: hosts: - dom1.acmetool-test.devever.net - dom2.acmetool-test.devever.net - boulder - boulder-mysql - boulder-rabbitmq - publisher.boulder - ra.boulder - ca.boulder - va.boulder - sa.boulder apt: packages: - lsb-release - gcc - libssl-dev - libffi-dev - ca-certificates - rsyslog - libcap-dev - gcc-multilib - libc6-dev-i386 - libcap-dev:i386 - debhelper - devscripts - realpath - dput - gnupg - lintian - ncftp - rpm mariadb: "10.1" sudo: false services: - rabbitmq before_install: - source ./.travis/before_install install: - go get -v -t ./... script: - source ./.travis/script after_success: - source ./.travis/after_success before_cache: - source ./.travis/before_cache env: 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: "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=2018-02-15 - secure: "X9xS2wGSeYXcPp8vs7z8nrzhtcvp8yLd+8vx+NaQuymX0S22tqLKzCSbkIaDk68GFpMxk19a9Tx4drLTq786uDvvpSe4PqzOdXJhiX1ZiwsGoltqET0z2JTm5c/b57Aj9SzLaKKUTjzlu9tAG9l0a82lsx5enm4uh2oOooLvk1nneXiiL01ijSr6FaN1q5jRhREcpglSDx8dXcgXHqq1xC8xVoQlxGqdNaWKBuV/HndLp7mKCwXTx1fDZRdS0m2+nWirUL1VN6jCduAVi9OYiNMYYlxu5qdBP5Nvt7sPgrZRIvHe+pNjLRmsWz6/sB3wnV/fgNo2va+0MfjcCCJUfqWyTti2tXgcYf77wXH+eHZrwSPeic5E5VAx3fFY1SKYoxk8CxCdE6WCCIlPNUtTxcY+TdNXV0nSxkRPJsxh25o9iT6I5K5Z8V/15cCGu6MkPSDV9UYalgdGu9IbMOevXqjmLudR5yV36p4VGWN6Kq5MwCc1jv9CjqfaRpWNVk0uqqPZ6+pNWqMegnhxkFSPlJCtxM6I0+Rm5KAf93pIrLAEF6vF5i7pxuGqedo+533GiWSy9oBGqh8uHknY3djT1V/8RKh3ln30BrHDd3BM+AEwJdAS8NA8E7X8yaqHTT92p5clgXGlqFIZay4baVa1tJ4UJK7Kz2lVCisT5NMWOqY=" branches: only: - master - dev - /^test-.*$/ - /^v[0-9].*$/ cache: directories: - $HOME/tcache notifications: webhooks: urls: - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MDE0ODgzMyUzQW1hdHJpeC5vcmcvJTIxSWhNWURnRHl4aXVjUlpMSW1yJTNBbWF0cml4Lm9yZw" on_success: change on_failure: always on_start: never acme-0.0.62/.travis/000077500000000000000000000000001321256625400141005ustar00rootroot00000000000000acme-0.0.62/.travis/after_success000066400000000000000000000076021321256625400166610ustar00rootroot00000000000000#!bash # Only upload version tags. if ! [[ "$TRAVIS_TAG" =~ ^v[0-9] ]]; then echo Skipping release upload because this build is not for a release tag. return 0 fi [ -n "$GITHUB_TOKEN" ] || { echo "Don't appear to have GitHub token, cannot continue."; return 0; } [ -e "/tmp/crosscompiled" ] || { echo "Not crosscompiled?"; return 1; } # Make archives. echo Archiving releases... ACME_DIR="$(pwd)" cd "$GOPATH/releasing/idist" for x in *; do echo "$x" cp "$GOPATH/src/github.com/$TRAVIS_REPO_SLUG/.travis/dist-readme.md" "$x"/README.md || \ cp "$GOPATH/src/github.com/$TRAVIS_REPO_SLUG/README.md" "$x"/ || true tar -zcf "../dist/$(basename "$x").tar.gz" "$x" done # Must be in the right directory when calling ghr. cd "$ACME_DIR" echo Uploading releases... PPA_NAME=rhea COPR_PROJECT_ID=5993 grep -F '[draft]' /tmp/commit-message && \ GHR_OPTIONS="--draft" PPA_NAME=testppa COPR_PROJECT_ID=6071 TRAVIS_REPO_OWNER="$(echo "$TRAVIS_REPO_SLUG" | sed 's#/.*##g')" travis_retry ghr $GHR_OPTIONS -u "$TRAVIS_REPO_OWNER" "$TRAVIS_TAG" "$GOPATH/releasing/dist/" # Prepare Ubuntu PPA signing key. echo Preparing Ubuntu PPA signing key... 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 < "$HOME/.devscripts" DEBSIGN_KEYID="Hugo Landau (2017 PPA Signing) " END 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" echo Creating Debian source archive for ${distro_name}... cd $GOPATH/releasing/dbuilds/$distro_name/acmetool_*[0-9] debuild -S done echo Deleting keys... find "$GNUPGHOME" -type f -exec shred -u '{}' ';' rm -rf "$GNUPGHOME" echo Uploading Debian source archives... cd "$GOPATH/releasing/dbuilds" ( echo 'open ppa.launchpad.net' echo 'set passive on' echo "cd ~hlandau/$PPA_NAME" for f in ./*/acmetool_*.dsc ./*/acmetool*.diff.gz ./*/acmetool_*_source.changes ./xenial/acmetool_*.orig.tar.gz; do echo "put $f" done echo 'quit' ) | ncftp # RPM. cd "$ACME_DIR/.travis" mkdir -p "$HOME/rpmbuild/SPECS" "$HOME/rpmbuild/SOURCES" RPMS="acmetool acmetool-nocgo" for x in $RPMS; do $GOPATH/src/github.com/$TRAVIS_REPO_SLUG/.travis/make_rpm_spec "$TRAVIS_TAG" "$x" > "$HOME/rpmbuild/SPECS/${x}.spec" done ln $GOPATH/releasing/dist/acmetool_*.orig.tar.gz $HOME/rpmbuild/SOURCES/ echo travis_fold:start:build-srpm for x in $RPMS; do rpmbuild -bs "$HOME/rpmbuild/SPECS/${x}.spec" done echo travis_fold:end:build-srpm for srpm in $HOME/rpmbuild/SRPMS/acmetool-*.rpm; do if [[ $srpm != *nocgo* ]]; then cat < /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", "fedora-25-i386", "fedora-25-x86_64", "fedora-26-i386", "fedora-26-x86_64"] } END else cat < /tmp/rpm-metadata { "project_id": $COPR_PROJECT_ID, "chroots": ["epel-5-i386", "epel-5-x86_64", "epel-6-i386", "epel-6-x86_64", "fedora-22-i386", "fedora-22-x86_64"] } END fi echo Uploading $srpm curl -u "$COPR_LOGIN_TOKEN" \ -F 'metadata=&1 | grep -qvE '(\.(md|txt)$)|_doc/' || { echo Documentation-only update. Skipping travis proper. exit } previous_build_failed() { let TRAVIS_PREVIOUS_BUILD_NUMBER="$TRAVIS_BUILD_NUMBER - 1" [ "$TRAVIS_BUILD_NUMBER" == "1" ] || \ curl -s -H 'Accept: application/vnd.travis-ci.2+json' "https://api.travis-ci.org/builds?slug=$TRAVIS_REPO_SLUG&number=$TRAVIS_PREVIOUS_BUILD_NUMBER" | \ python3 -c 'import sys,json; k=json.load(sys.stdin); print(k); sys.exit(int(k["builds"][0]["state"] == "passed"))' } if [ -n "$TRAVIS_TAG" ]; then git show "$TRAVIS_TAG" 2>&1 | sed -n '/^diff --git/q;p' > /tmp/commit-message else git log --format=%B --no-merges -n 1 &> /tmp/commit-message fi ! grep -qF '[ci fast]' /tmp/commit-message || \ [ -n "$TRAVIS_TAG" ] || \ previous_build_failed || { echo Travis not needed for this update. exit } acme-0.0.62/.travis/boulder.patch000066400000000000000000000047121321256625400165610ustar00rootroot00000000000000diff --git a/start.py b/start.py index 347883e..448295f 100755 --- a/start.py +++ b/start.py @@ -18,6 +18,7 @@ import startservers if not startservers.start(race_detection=False): sys.exit(1) try: + open('/tmp/boulder-has-started','wb').write('x') os.wait() # 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 374ff68..4e701da 100644 --- a/test/config-next/va.json +++ b/test/config-next/va.json @@ -4,7 +4,7 @@ "userAgent": "boulder", "debugAddr": "localhost:8004", "portConfig": { - "httpPort": 5002, + "httpPort": 80, "httpsPort": 5001, "tlsPort": 5001 }, diff --git a/test/config/ca.json b/test/config/ca.json index eb6a2c1..7c6c0e3 100644 --- a/test/config/ca.json +++ b/test/config/ca.json @@ -5,11 +5,11 @@ "ecdsaProfile": "ecdsaEE", "debugAddr": ":8001", "Issuers": [{ - "ConfigFile": "test/test-ca.key-pkcs11.json", + "File": "test/test-ca.key", "CertFile": "test/test-ca2.pem", "NumSessions": 2 }, { - "ConfigFile": "test/test-ca.key-pkcs11.json", + "File": "test/test-ca.key", "CertFile": "test/test-ca.pem", "NumSessions": 2 }], diff --git a/test/config/va.json b/test/config/va.json index 8d0fcef..4da51fc 100644 --- a/test/config/va.json +++ b/test/config/va.json @@ -4,7 +4,7 @@ "userAgent": "boulder", "debugAddr": "localhost:8004", "portConfig": { - "httpPort": 5002, + "httpPort": 80, "httpsPort": 5001, "tlsPort": 5001 }, diff --git a/test/hostname-policy.json b/test/hostname-policy.json index 6397ee9..15ad50c 100644 --- a/test/hostname-policy.json +++ b/test/hostname-policy.json @@ -1,12 +1,6 @@ { "Blacklist": [ "in-addr.arpa", - "example", - "example.net", - "example.org", - "invalid", - "local", - "localhost", - "test" + "invalid" ] } diff --git a/test/rate-limit-policies.yml b/test/rate-limit-policies.yml index 41aadd3..66c2662 100644 --- a/test/rate-limit-policies.yml +++ b/test/rate-limit-policies.yml @@ -4,7 +4,7 @@ totalCertificates: threshold: 100000 certificatesPerName: window: 2160h - threshold: 2 + threshold: 10000 overrides: ratelimit.me: 1 lim.it: 0 @@ -30,7 +30,7 @@ pendingAuthorizationsPerAccount: threshold: 3 certificatesPerFQDNSet: window: 24h - threshold: 5 + threshold: 5000 overrides: le.wtf: 10000 le1.wtf: 10000 acme-0.0.62/.travis/check-copr-token000077500000000000000000000006511321256625400171640ustar00rootroot00000000000000#!/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 acme-0.0.62/.travis/crosscompile000066400000000000000000000043231321256625400165270ustar00rootroot00000000000000#!bash # Test cross-compilation. The binaries produced are also used for release # upload in after_success if this is a release tag. [ -e "/tmp/crosscompiled" ] && return touch /tmp/crosscompiled echo travis_fold:start:crosscompile echo Cross-compiling releases... mkdir -p "$GOPATH/releasing/idist" "$GOPATH/releasing/dist" # Assume that x86 machines don't necessarily have SSE2. Whereas for amd64, # require SSE2. REPO=github.com/$TRAVIS_REPO_SLUG BINARIES=$REPO/cmd/acmetool export BUILDNAME="by travis" BUILDINFO="$($GOPATH/src/github.com/hlandau/buildinfo/gen $BINARIES)" # cgo crosscompile export GOARM=5 gox -ldflags "$BUILDINFO" -cgo -osarch 'linux/amd64' -output "$GOPATH/releasing/idist/acmetool-$TRAVIS_TAG-{{.OS}}_{{.Arch}}_cgo/bin/{{.Dir}}" $BINARIES RESULT1=$? GO386=387 gox -ldflags "$BUILDINFO" -cgo -osarch 'linux/386' -output "$GOPATH/releasing/idist/acmetool-$TRAVIS_TAG-{{.OS}}_{{.Arch}}_cgo/bin/{{.Dir}}" $BINARIES RESULT2=$? # non-cgo crosscompile gox -ldflags "$BUILDINFO" -osarch 'darwin/amd64 linux/amd64 linux/arm linux/arm64 freebsd/amd64 freebsd/arm openbsd/amd64 netbsd/amd64 netbsd/arm dragonfly/amd64 solaris/amd64' -output "$GOPATH/releasing/idist/acmetool-$TRAVIS_TAG-{{.OS}}_{{.Arch}}/bin/{{.Dir}}" $BINARIES RESULT3=$? GO386=387 gox -ldflags "$BUILDINFO" -osarch 'linux/386 darwin/386 freebsd/386 openbsd/386 netbsd/386' -output "$GOPATH/releasing/idist/acmetool-$TRAVIS_TAG-{{.OS}}_{{.Arch}}/bin/{{.Dir}}" $BINARIES RESULT4=$? echo travis_fold:end:crosscompile # Defer exiting to get as much error output as possible upfront. echo "cgo crosscompile (amd64) exited with code $RESULT1" echo "cgo crosscompile (386) exited with code $RESULT2" echo "non-cgo crosscompile (amd64) exited with code $RESULT3" echo "non-cgo crosscompile (386) exited with code $RESULT4" if [ "$RESULT1" != "0" ]; then exit $RESULT1 fi if [ "$RESULT2" != "0" ]; then exit $RESULT2 fi if [ "$RESULT3" != "0" ]; then exit $RESULT3 fi if [ "$RESULT4" != "0" ]; then exit $RESULT4 fi # Generate man page. "$GOPATH/releasing/idist/acmetool-$TRAVIS_TAG-linux_amd64_cgo/bin/acmetool" --help-man > acmetool.8 || echo Failed to generate man page for x in $GOPATH/releasing/idist/*; do mkdir -p "$x/doc" cp -a acmetool.8 "$x/doc/" done acme-0.0.62/.travis/dist-readme.md000066400000000000000000000013401321256625400166160ustar00rootroot00000000000000# ACME Client Utilities More information: ## Installation and Usage You have downloaded a binary release of acmetool. Here are some simple installation instructions: $ sudo cp -a bin/acmetool /usr/local/bin/ # Run the quickstart wizard. Sets up account, cronjob, etc. $ sudo acmetool quickstart # Request the hostnames you want: $ sudo acmetool want example.com www.example.com # Now you have certificates: $ ls /var/lib/acme/live/example.com/ For more information on using acmetool, please see the full README at https://github.com/hlandau/acme ## Licence © 2015 Hugo Landau MIT License File issues at . acme-0.0.62/.travis/make_debian_env000077500000000000000000000210601321256625400171140ustar00rootroot00000000000000#!/bin/bash # Script to generate files for debbuild causing it to generate a 'source package' # to be uploaded to launchpad to generate PPA 'binary packages'. These source packages # are just binaries with a build script that outputs them as a 'binary package'. # So sue me. if [ -z "$1" -o -z "$2" -o -z "$3" -o -z "$4" ]; then echo Usage: "$0" "" "" "" "" echo "Create a debian build environment suitable for running debuild [-S] in." exit 1 fi OUTDIR="$1" ARCHIVEDIR="$2" VERSION="$(echo "$3" | sed 's/^v//g')" ORIGDIR="$(dirname "$0")" DISTRO="$4" mkdir -p "$OUTDIR" "$ARCHIVEDIR" OUTDIR="$(realpath "$OUTDIR")" ARCHIVEDIR="$(realpath "$ARCHIVEDIR")" ORIGDIR="$(realpath "$ORIGDIR")" REVISION="1${DISTRO}1" DATE_R="$(date -R)" # The non-cgo builds here are used by the RPM builds, which also reply on the # .orig.tar.gz file built here. ARCHS="386_cgo amd64_cgo 386 amd64 arm arm64" mkdir -p "$ARCHIVEDIR" for ARCH in $ARCHS; do wget -nc -O "$ARCHIVEDIR/acmetool-v$VERSION-linux_$ARCH.tar.gz" "https://github.com/hlandau/acme/releases/download/v$VERSION/acmetool-v$VERSION-linux_$ARCH.tar.gz" done mkdir -p "$OUTDIR/acmetool_$VERSION" cd "$OUTDIR/acmetool_$VERSION" for ARCH in $ARCHS; do tar xvf "$ARCHIVEDIR/acmetool-v$VERSION-linux_$ARCH.tar.gz" done cd .. if [ ! -e "$ARCHIVEDIR/acmetool_$VERSION.orig.tar.gz" ]; then tar zcvf "$ARCHIVEDIR/acmetool_$VERSION.orig.tar.gz" "acmetool_$VERSION" fi [ -e "./acmetool_$VERSION.orig.tar.gz" ] || ln "$ARCHIVEDIR/acmetool_$VERSION.orig.tar.gz" cd "$ORIGDIR" UNPACKDIR="$OUTDIR/acmetool_$VERSION" DEBIANDIR="$UNPACKDIR/debian" mkdir -p "$DEBIANDIR/source" ### debian cruft ############################################################# echo 9 > "$DEBIANDIR/compat" echo 1.0 > "$DEBIANDIR/source/format" cat < "$DEBIANDIR/changelog" acmetool ($VERSION-$REVISION) $DISTRO; urgency=medium * Changelog information not maintained in Debian packaging. See -- Hugo Landau $DATE_R END cat <<'END' > "$DEBIANDIR/control" Source: acmetool Maintainer: Hugo Landau Section: utils Priority: optional Standards-Version: 3.9.6 Build-Depends: debhelper (>= 9), wget, ca-certificates, curl, libcap-dev Package: acmetool Architecture: amd64 i386 armhf arm64 Depends: ${misc:Depends}, ${shlibs:Depends} Description: command line tool for automatically acquiring certificates acmetool is an easy-to-use command line tool for automatically acquiring certificates from ACME servers (such as Let's Encrypt). Designed to flexibly integrate into your webserver setup to enable automatic verification. Unlike the official Let's Encrypt client, this doesn't modify your web server configuration. . You can perform verifications using port 80 or 443 (if you don't yet have a server running on one of them); via webroot; by configuring your webserver to proxy requests for /.well-known/acme-challenge/ to a special port (402) which acmetool can listen on. . acmetool is intended to be "magic-free". All of acmetool's state is stored in a simple, comprehensible directory of flat files. . acmetool is intended to work like "make". The state directory expresses target domain names, and whenever acmetool is invoked, it ensures that valid certificates are available to meet those names. Certificates which will expire soon are renewed. acmetool is thus idempotent and minimises the use of state. END cat <<'END' > "$DEBIANDIR/copyright" Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: acmetool Upstream-Contact: Hugo Landau Source: https://github.com/hlandau/acme Files: * Copyright: 2015, Hugo Landau License: MIT Copyright © 2015 Hugo Landau . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Files: debian/* Copyright: 2015, Christian Pointner License: GPL-3+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. . This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see '/usr/share/common-licenses/GPL-3'. END ### debian build script ############################################################# cat <<'END' > "$DEBIANDIR/rules" #!/usr/bin/make -f %: dh $@ END ### debian miscellaneous files ############################################################# cat <<'END' > "$DEBIANDIR/acmetool.lintian-overrides" acmetool: embedded-library usr/bin/acmetool: libyaml acmetool: new-package-should-close-itp-bug END cat <<'END' > "$DEBIANDIR/acmetool.dirs" /usr/lib/acme /var/lib/acme END cat <<'END' > "$DEBIANDIR/acmetool.postrm" #!/bin/sh # postrm script for acmetool set -e if [ "$1" = "purge" ]; then # Remove cron job. if [ -f "/etc/cron.d/acmetool" ]; then rm -f /etc/cron.d/acmetool if [ -x "$(which invoke-rc.d 2>/dev/null)" ]; then invoke-rc.d cron reload || true else /etc/init.d/cron reload || true fi fi # Remove managed hooks. if [ -d "/usr/lib/acme/hooks" ]; then for hook in /usr/lib/acme/hooks/*; do grep -q '#!acmetool-managed!#' "$hook" && rm -f "$hook" || true done rmdir --ignore-fail-on-non-empty /usr/lib/acme/hooks || true fi # Warn about non-removed data. rmdir --ignore-fail-on-non-empty "/var/lib/acme" || true if [ -d "/var/lib/acme" ]; then echo '╔══════ acmetool purge ══════════════════════════════════════════════╗' echo '║ The acmetool state directory will be kept. If you really want to ║' echo '║ delete all your certificates and configurations you need to remove ║' echo '║ it manually using the following command: ║' echo '║ rm -rf /var/lib/acme ║' echo '╚════════════════════════════════════════════════════════════════════╝' fi fi #DEBHELPER# exit 0 END ### "source" build file ############################################################# cat <<'END' > "$UNPACKDIR/Makefile" # Invoked with DESTDIR. ARCH := $(shell sh ./getarch) VERSION := $(shell cat ./version) SRCDIR := acmetool-v$(VERSION)-linux_$(ARCH) build: install: install -Dm 755 "$(SRCDIR)/bin/acmetool" "$(DESTDIR)/usr/bin/acmetool" install -Dm 644 ./acme-reload.default "$(DESTDIR)/etc/default/acme-reload" if [ -e "$(SRCDIR)/doc/acmetool.8" ]; then \ install -Dm 644 "$(SRCDIR)/doc/acmetool.8" "$(DESTDIR)/usr/share/man/man8/acmetool.8"; \ fi clean: END cat <<'END' > "$UNPACKDIR/acme-reload.default" # Space separated list of services to restart after certificates are changed. # By default, this is a list of common webservers like apache2, nginx, haproxy, # etc. You can append to this list or replace it entirely. SERVICES="$SERVICES" END cat <<'END' > "$UNPACKDIR/getarch" #!/bin/sh set -e case "$DEB_BUILD_ARCH" in i386) echo 386_cgo;; amd64) echo amd64_cgo;; armhf|armel) echo arm;; *) echo "$DEB_BUILD_ARCH";; esac END echo "$VERSION" > "$UNPACKDIR/version" acme-0.0.62/.travis/make_rpm_spec000077500000000000000000000024151321256625400166350ustar00rootroot00000000000000#!/bin/bash VERSION="$1" if [ -z "$VERSION" ]; then echo usage: $0 1.2.34 exit 1 fi VERSION="$(echo "$VERSION" | sed s/^v//g)" CGO_SUFFIX="_cgo" NOCGO_SUFFIX= NOTNOCGO_SUFFIX="-nocgo" if [ "$2" == "acmetool-nocgo" ]; then CGO_SUFFIX= NOCGO_SUFFIX="-nocgo" NOTNOCGO_SUFFIX= fi cat < boulder.log || cat boulder.log ; } & START_PID=$$ # Wait for boulder to come up. echo Waiting for boulder to come up... while ((1)); do kill -0 "$START_PID" || break [ -e /tmp/boulder-has-started ] && break sleep 1 done echo Boulder up. echo ---------------------------------------------------------------- # Run tests. cd "$ACME_DIR" echo travis_fold:start:go-tests time go test -v -tags=integration ./... RESULT=$? echo travis_fold:end:go-tests echo travis_fold:start:boulder-log echo Dumping boulder log cat $GOPATH/src/github.com/letsencrypt/boulder/boulder.log echo travis_fold:end:boulder-log echo Done with exit code $RESULT if [ "$RESULT" != "0" ]; then exit $RESULT fi # Crosscompilation failures are rare now and crosscompiling takes a long time # so only do it for tags. if [ -n "$TRAVIS_TAG" ]; then time source ./.travis/crosscompile fi # No point stopping boulder, travis will do it. # Don't exit here, we need after_success to run and this script is sourced. acme-0.0.62/Makefile000066400000000000000000000054371321256625400141630ustar00rootroot00000000000000PROJNAME=github.com/hlandau/acme BINARIES=$(PROJNAME)/cmd/acmetool ############################################################################### # v1.12 NNSC:github.com/hlandau/degoutils/_stdenv/Makefile.ref # This is a standard Makefile for building Go code designed to be copied into # other projects. Code below this line is not intended to be modified. # # NOTE: Use of this Makefile is not mandatory. People familiar with the use # of the "go" command who have a GOPATH setup can use go get/go install. # XXX: prebuild-checks needs bash, fix this at some point SHELL := $(shell which bash) -include Makefile.extra -include Makefile.assets ## Paths ifeq ($(GOPATH),) # for some reason export is necessary for FreeBSD's gmake export GOPATH := $(shell pwd) endif ifeq ($(GOBIN),) export GOBIN := $(GOPATH)/bin endif ifeq ($(PREFIX),) export PREFIX := /usr/local endif DIRS=src bin public ## Quieting Q=@ QI=@echo -e "\t[$(1)]\t $(2)"; ifeq ($(V),1) Q= QI= endif ## Buildinfo ifeq ($(USE_BUILDINFO),1) BUILDINFO_FLAG=-ldflags "$$($$GOPATH/src/github.com/hlandau/buildinfo/gen $(1))" endif ## Standard Rules all: prebuild-checks $(DIRS) $(call QI,GO-INSTALL,$(BINARIES))go install $(BUILDFLAGS) $(call BUILDINFO_FLAG,$(BINARIES)) $(BINARIES) prebuild-checks: $(call QI,RELOCATE)if [ `find . -iname '*.go' | grep -v ./src/ | wc -l` != 0 ]; then \ if [ -e "$(GOPATH)/src/$(PROJNAME)/" ]; then \ echo "$$GOPATH/src/$(PROJNAME)/ already exists, can't auto-relocate. Since you appear to have a GOPATH configured, just use go get -u '$(PROJNAME)/...; go install $(BINARIES)'. Alternatively, move this Makefile to either GOPATH or an empty directory outside GOPATH (preferred) and run it. Or delete '$$GOPATH/src/$(PROJNAME)/'."; \ exit 1; \ fi; \ mkdir -p "$(GOPATH)/src/$(PROJNAME)/"; \ for x in ./* ./.*; do \ [ "$$x" == "./src" ] && continue; \ mv -n "$$x" "$(GOPATH)/src/$(PROJNAME)/"; \ done; \ ln -s "$(GOPATH)/src/$(PROJNAME)/Makefile"; \ [ -e "$(GOPATH)/src/$(PROJNAME)/_doc" ] && ln -s "$(GOPATH)/src/$(PROJNAME)/_doc" doc; \ [ -e "$(GOPATH)/src/$(PROJNAME)/_tpl" ] && ln -s "$(GOPATH)/src/$(PROJNAME)/_tpl" tpl; \ fi; \ exit 0 $(DIRS): | .gotten $(call QI,DIRS)mkdir -p $(GOPATH)/src $(GOBIN); \ if [ ! -e "src" ]; then \ ln -s $(GOPATH)/src src; \ fi; \ if [ ! -e "bin" ]; then \ ln -s $(GOBIN) bin; \ fi .gotten: $(call QI,GO-GET,$(PROJNAME))go get $(PROJNAME)/... $(Q)touch .gotten .NOTPARALLEL: prebuild-checks $(DIRS) .PHONY: all test install prebuild-checks test: $(call QI,GO-TEST,$(PROJNAME))for x in $(PROJNAME); do go test -cover -v $$x/...; done install: all $(call QI,INSTALL,$(BINARIES))for x in $(BINARIES); do \ install -Dp $(GOBIN)/`basename "$$x"` $(DESTDIR)$(PREFIX)/bin; \ done update: | .gotten $(call QI,GO-GET,$(PROJNAME))go get -u $(PROJNAME)/... acme-0.0.62/README.md000066400000000000000000000414151321256625400137760ustar00rootroot00000000000000#
acmetool

[issue stats] [webchat: freenode #acmetool]
[download count] [version] [ppa
debian/ubuntu]
Build Status [pull
request stats]

acmetool is an easy-to-use command line tool for automatically acquiring certificates from ACME servers (such as Let's Encrypt). Designed to flexibly integrate into your webserver setup to enable automatic verification. Unlike the official Let's Encrypt client, this doesn't modify your web server configuration.

:white_check_mark: Zero-downtime autorenewal
:white_check_mark: Supports any webserver
:white_check_mark: Fully automatable
:white_check_mark: Single-file dependency-free binary
:white_check_mark: Idempotent
:white_check_mark: Fast setup

You can perform verifications using port 80 or 443 (if you don't yet have a server running on one of them); via webroot; by configuring your webserver to proxy requests for `/.well-known/acme-challenge/` to a special port (402) which acmetool can listen on; or by configuring your webserver not to listen on port 80, and instead running acmetool's built in HTTPS redirector (and challenge responder) on port 80. This is useful if all you want to do with port 80 is redirect people to port 443. You can run acmetool on a cron job to renew certificates automatically (`acmetool --batch`). The preferred certificate for a given hostname is always at `/var/lib/acme/live/HOSTNAME/{cert,chain,fullchain,privkey}`. You can configure acmetool to reload your webserver automatically when it renews a certificate. acmetool is intended to be "magic-free". All of acmetool's state is stored in a simple, comprehensible directory of flat files. [The schema for this directory is documented.](https://github.com/hlandau/acme/blob/master/_doc/SCHEMA.md) acmetool is intended to work like "make". The state directory expresses target domain names, and whenever acmetool is invoked, it ensures that valid certificates are available to meet those names. Certificates which will expire soon are renewed. acmetool is thus idempotent and minimises the use of state. acmetool can optionally be used [without running it as root.](https://hlandau.github.io/acme/userguide#annex-root-configured-non-root-operation) If you have existing certificates issued using the official client, acmetool can import those certificates, keys and account keys (`acmetool import-le`). acmetool supports both RSA and ECDSA keys and certificates. acmetool's notification hooks system allows you to write arbitrary shell scripts to be executed when new certificates are obtained. By default, this is used to reload webservers automatically, but it can also be used to distribute certificates to other servers or for other purposes. ## Getting Started **Binary releases:** [Binary releases are available.](https://github.com/hlandau/acme/releases) Download the release appropriate for your platform and simply copy the `acmetool` binary to `/usr/bin`. `_cgo` releases are preferred over non-`_cgo` releases where available, but non-`_cgo` releases may be more compatible with older OSes. **Ubuntu users:** A binary release PPA, `ppa:hlandau/rhea` (package `acmetool`) is available. ```bash $ sudo add-apt-repository ppa:hlandau/rhea $ sudo apt-get update $ sudo apt-get install acmetool ``` You can also [download .deb files manually.](https://launchpad.net/~hlandau/+archive/ubuntu/rhea/+packages) (Note: There is no difference between the .deb files for different Ubuntu release codenames; they are interchangeable and completely equivalent.) **Debian users:** The Ubuntu binary release PPA also works with Debian: ``` # echo 'deb http://ppa.launchpad.net/hlandau/rhea/ubuntu xenial main' > /etc/apt/sources.list.d/rhea.list # apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9862409EF124EC763B84972FF5AC9651EDB58DFA # apt-get update # apt-get install acmetool ``` You can also [download .deb files manually.](https://launchpad.net/~hlandau/+archive/ubuntu/rhea/+packages) (Note: There is no difference between the .deb files for different Ubuntu release codenames; they are interchangeable and completely equivalent.) **RPM-based distros:** [A copr RPM repository is available.](https://copr.fedorainfracloud.org/coprs/hlandau/acmetool/) If you have `dnf` installed: ```bash $ sudo dnf copr enable hlandau/acmetool $ sudo dnf install acmetool ``` Otherwise use the `.repo` files on the [repository page](https://copr.fedorainfracloud.org/coprs/hlandau/acmetool/) and use `yum`, or download RPMs and use `rpm` directly. **Void Linux users:** `acmetool` is in the repositories: ```bash $ sudo xbps-install acmetool ``` **Arch Linux users:** [An AUR PKGBUILD for building from source is available.](https://aur.archlinux.org/packages/acmetool-git/) ```bash $ wget https://aur.archlinux.org/cgit/aur.git/snapshot/acmetool-git.tar.gz $ tar xvf acmetool-git.tar.gz $ cd acmetool-git $ makepkg -s $ sudo pacman -U ./acmetool*.pkg.tar.xz ``` **Alpine Linux users:** [An APKBUILD for building from source is available.](_doc/APKBUILD) **FreeBSD users:** [FreeBSD port is available.](http://www.freshports.org/security/acmetool/) **Building from source:** You will need Go installed to build from source. If you are on Linux, you will need to make sure the development files for `libcap` are installed. This is probably a package for your distro called `libcap-dev` or `libcap-devel` or similar. ```bash # This is necessary to work around a change in Git's default configuration # which hasn't yet been accounted for in some places. $ git config --global http.followRedirects true $ git clone https://github.com/hlandau/acme $ cd acme $ make && sudo make install # (People familiar with Go with a GOPATH setup can alternatively use go get/go install:) $ git config --global http.followRedirects true $ go get github.com/hlandau/acme/cmd/acmetool ``` ### After installation ```bash # Run the quickstart wizard. Sets up account, cronjob, etc. # (If you want to use ECDSA keys or set RSA key size, pass "--expert".) $ sudo acmetool quickstart # Configure your webserver to serve challenges if necessary. # See https://hlandau.github.io/acme/userguide#web-server-configuration $ ... # Request the hostnames you want: $ sudo acmetool want example.com www.example.com # Now you have certificates: $ ls -l /var/lib/acme/live/example.com/ ``` The `quickstart` subcommand is a recommended wizard which guides you through the setup of ACME on your system. The `want` subcommand states that you want a certificate for the given hostnames. (If you want separate certificates for each of the hostnames, run the want subcommand separately for each hostname.) The default subcommand, `reconcile`, is like "make" and makes sure all desired hostnames are satisfied by valid certificates which aren't soon to expire. `want` calls `reconcile` automatically. If you run `acmetool reconcile` on a cronjob to facilitate automatic renewal, pass `--batch` to ensure it doesn't attempt to interact with a terminal. You can increase logging severity for debugging purposes by passing `--xlog.severity=debug`. ## Validation Options [screenshot] **Webroot:** acmetool can place challenge files in a given directory, allowing your normal web server to serve them. The files must be served from the path you specify at `/.well-known/acme-challenge/`. [Information on configuring your web server.](https://hlandau.github.io/acme/userguide#web-server-configuration-challenges) **Proxy:** acmetool can respond to validation challenges by serving them on port 402. In order for this to be useful, you must configure your webserver to proxy requests under `/.well-known/acme-challenge/` to `http://127.0.0.1:402/.well-known/acme-challenge`. [Information on configuring your web server.](https://hlandau.github.io/acme/userguide#web-server-configuration-challenges) **Stateless:** You configure your webserver to respond statelessly to challenges for a given account key without consulting acmetool. This requires nothing more than a one-time web server configuration change and no "moving parts". [Information on configuring stateless challenges.](https://hlandau.github.io/acme/userguide#web-server-configuration-challenges) **Redirector:** `acmetool redirector` starts an HTTP server on port 80 which redirects all requests to HTTPS, as well as serving any necessary validation responses. The `acmetool quickstart` wizard can set it up for you if you use systemd. Otherwise, you'll need to configure your system to run `acmetool redirector --service.uid=USERNAME --service.daemon=1` as a service, where `USERNAME` is the username you want the daemon to drop to. Make sure your web server is not listening on port 80. **Listen:** If you are for some reason not running anything on port 80 or 443, acmetool will use those ports. Either port being available is sufficient. This is only really useful for development purposes. **Hook:** You can write custom shell scripts (or binary executables) which acmetool invokes to provision challenge files at the desired location. For example, you could rsync challenge files to a directory on a remote server. [More information.](https://hlandau.github.io/acme/userguide#challenge-hooks) ## Renewal acmetool will try to renew certificates automatically once they are 30 days from expiry, or 66% through their validity period, whichever is lower. Note that Let's Encrypt currently issues 90 day certificates. acmetool will exit with an error message with nonzero exit status if it cannot renew a certificate, so it is suitable for use in a cronjob. Ensure your system is configured so that you get notifications of failing cronjobs. If a cronjob fails, you should intervene manually to see what went wrong by running `acmetool` (possibly with `--xlog.severity=debug` for verbose logging). ## Library The [client library which these utilities use](https://github.com/hlandau/acme/tree/master/acmeapi) can be used independently by any Go code. [README and source code.](https://github.com/hlandau/acme/tree/master/acmeapi) [Godoc.](https://godoc.org/github.com/hlandau/acme/acmeapi) ## Comparison with... **certbot:** A heavyweight Python implementation which is a bit too “magic” for my tastes. Tries to mutate your webserver configuration automatically. acmetool is a single-file binary which only depends on basic system libraries (on Linux, these are libc, libpthread, libcap, libattr). It doesn't do anything to your webserver; it just places certificates at a standard location and can also reload your webserver (whichever webserver it is) by executing hook shell scripts. acmetool isn't based around individual transactions for obtaining certificates; it's about satisfying expressed requirements by any means necessary. Its comprehensible, magic-free state directory makes it as stateless and idempotent as possible. **lego:** Like acmetool, [xenolf/lego](https://github.com/xenolf/lego) provides a library and client utility. The utility provides commands for creating certificates, but doesn't provide a compelling system for managing the lifetime of the short-lived certificates offered by Let's Encrypt. The user is expected to generate and install all certificates manually. **gethttpsforfree:** [diafygi/gethttpsforfree](https://github.com/diafygi/gethttpsforfree) provides an HTML file which uses JavaScript to make requests to an ACME server and obtain certificates. It's a functional user interface, but like lego it provides no answer for the automation issue, and is thus impractical given the short lifetime of certificates issued by Let's Encrypt. ### Comparison, list of client implementations
acmetoolcertbotlegogethttpsforfree
Automatic renewalYesNot yetNoNo
SAN supportYesYesYesYes
ECC supportYesNoNoNo
OCSP Must Staple supportYesNoNoNo
Revocation supportYesYesYesNo
State managementYes†Yes
Single-file binaryYesNoYesYes
Quickstart wizardYesYesNoNo
Modifies webserver configNoBy defaultNoNo
Non-root supportOptionalOptionalOptional
Supports ApacheYesYes
Supports nginxYesExperimental
Supports HAProxyYesNo
Supports HitchYesNo
Supports any web serverYesWebroot‡
Authorization via webrootYesYesManual
Authorization via port 80 redirectorYesNoNoNo
Authorization via proxyYesNoNoNo
Authorization via listener§YesYesYesNo
Authorization via DNSHook onlyNoYesNo
Authorization via custom hookYesNoNoNo
Import state from official clientYes
Windows (basic) supportNoNoYes
Windows integration supportNoNoNo
† acmetool has a different philosophy to state management and configuration to the Let's Encrypt client; see the beginning of this README. ‡ The webroot method does not appear to provide any means of reloading the webserver once the certificate has been changed, which means auto-renewal requires manual intervention. § Requires downtime. This table is maintained in good faith; I believe the above comparison to be accurate. If notified of any inaccuracies, I will rectify the table and publish a notice of correction here: - This table previously stated that the official Let's Encrypt client doesn't support non-root operation. This was incorrect; it can be installed at user level and be configured to use user-writable directories. ## Documentation & Support For more documentation see: - [User Guide](https://hlandau.github.io/acme/userguide) - [Troubleshooting](https://hlandau.github.io/acme/userguide#troubleshooting) - [FAQ](https://hlandau.github.io/acme/userguide#faq) - [manpage](https://hlandau.github.io/acme/acmetool.8) If your question or issue isn't resolved by any of the above, file an issue. IRC: [#acmetool](irc://chat.freenode.net/#acmetool) on [Freenode](http://freenode.net/) ([webchat](https://webchat.freenode.net/?channels=%23acmetool)). ## Licence © 2015—2016 Hugo Landau MIT License [Licenced under the licence with SHA256 hash `fd80a26fbb3f644af1fa994134446702932968519797227e07a1368dea80f0bc`, a copy of which can be found here.](https://raw.githubusercontent.com/hlandau/rilts/master/licences/COPYING.MIT) acme-0.0.62/_doc/000077500000000000000000000000001321256625400134165ustar00rootroot00000000000000acme-0.0.62/_doc/APKBUILD000066400000000000000000000027761321256625400145500ustar00rootroot00000000000000# Contributor: Hugo Landau # Maintainer: Hugo Landau # # This is a build script for the Alpine Linux build system. # Please do not submit it to Alpine Linux at this time. # # To build, put this file in an empty directory and run abuild. # You may need to setup abuild signing keys first; see Alpine documentation. # if [ "$(find version -mmin -30 2>/dev/null | wc -l)" == "0" ]; then curl -s -H 'Accept: application/vnd.github.v3+json' \ 'https://api.github.com/repos/hlandau/acme/releases/latest' | \ sed 's/^.*"tag_name": *"v\([^"]*\)".*$/\1/;tx;d;:x' > version.tmp || exit 1 mv version.tmp version fi pkgname=acmetool pkgver="$(cat version)" pkgrel=0 pkgdesc="ACME/Let's Encrypt client" url="https://github.com/hlandau/acme" arch="all" license="MIT" depends="libcap" makedepends="libcap-dev go bash git curl" install="" subpackages="$pkgname-doc" options="!strip" source="" prepare() { cd "$srcdir" git clone -b "v$pkgver" https://github.com/hlandau/acme || return 1 } build() { cd "$srcdir/acme" || return 1 make USE_BUILDINFO=1 || return 1 # For some reason this is necessary in order for the buildinfo to get # included properly. rm "$srcdir/acme/bin/$pkgname" || return 1 make USE_BUILDINFO=1 || return 1 } package() { install -Dm0755 "$srcdir/acme/bin/$pkgname" "$pkgdir"/usr/bin/$pkgname || return 1 mkdir -p "$pkgdir"/usr/share/man/man8 "$pkgdir"/usr/bin/$pkgname --help-man | gzip > \ "$pkgdir"/usr/share/man/man8/acmetool.man.gz || return 1 } acme-0.0.62/_doc/FAQ.md000066400000000000000000000001141321256625400143430ustar00rootroot00000000000000# [This document has moved.](https://hlandau.github.com/acme/userguide#faq) acme-0.0.62/_doc/NOROOT.md000066400000000000000000000001521321256625400147560ustar00rootroot00000000000000# [This document has moved.](https://hlandau.github.io/acme/userguide#root-configured-non-root-operation) acme-0.0.62/_doc/PACKAGING-PATHS.md000066400000000000000000000022121321256625400160760ustar00rootroot00000000000000# On packaging acmetool for distribution: changing default paths acmetool uses paths such as "/var/lib/acme" and "/usr/lib(exec)/acme/hooks" by default. It may be desired to change these paths for the purposes of a specific distribution. It is thus possible to override these paths when building acmetool. The following arguments to `go build` demonstrate which paths may be customized and how. This example also includes version information, which ensures that `--version` output will be informative. If you set the BUILDNAME environment variable, you can specify a short, one-line string providing your build information. This defaults, if not set, to the date and hostname. (You could set it to a constant value if you are pursing reproducible builds.) ```sh $ go build -ldflags " -X github.com/hlandau/acme/storage.RecommendedPath=\"/var/lib/acme\" -X github.com/hlandau/acme/hooks.DefaultPath=\"/usr/lib/acme/hooks\" -X github.com/hlandau/acme/responder.StandardWebrootPath=\"/var/run/acme/acme-challenge\" $($GOPATH/src/github.com/hlandau/buildinfo/gen github.com/hlandau/acme/cmd/acmetool) " github.com/hlandau/acme/cmd/acmetool ``` acme-0.0.62/_doc/PROGRAMMATIC-DOWNLOADING.md000066400000000000000000000013731321256625400173740ustar00rootroot00000000000000# How to download binary releases programmatically ## With curl ```sh VER="$(curl -s -H 'Accept: application/vnd.github.v3+json' 'https://api.github.com/repos/hlandau/acme/releases/latest' | python -c 'import sys,json;k=json.load(sys.stdin);print(k["tag_name"])')" curl -Ls -o acmetool-bin.tar.gz "https://github.com/hlandau/acme/releases/download/$VER/acmetool-$VER-linux_amd64_cgo.tar.gz" ``` ## With wget ```sh VER="$(wget --quiet -O - --header='Accept: application/vnd.github.v3+json' 'https://api.github.com/repos/hlandau/acme/releases/latest' | python -c 'import sys,json;k=json.load(sys.stdin);print(k["tag_name"])')" wget --quiet -O acmetool-bin.tar.gz "https://github.com/hlandau/acme/releases/download/$VER/acmetool-$VER-linux_amd64_cgo.tar.gz" ``` acme-0.0.62/_doc/SCHEMA.md000066400000000000000000001062401321256625400147030ustar00rootroot00000000000000ACME State Storage Specification ================================ The ACME State Storage Specification (ACME-SSS) specifies how an ACME client can store state information in a directory on the local system in a way that facilitates its own access to and mutation of its state and the exposure of certificates and keys to other system services as required. This specification relates to the use of ACME but has no official endorsement. This specification is intended for use on POSIX-like systems. Synopsis -------- The following example shows a state directory configured to obtain a certificate with hostnames example.com and www.example.com. No state other than that shown below is used. /var/lib/acme desired/ example.com ; Target expression file ;; By default a target expression file expresses a desire for the ;; hostname which is its filename. The following YAML-format ;; configuration directives are all optional. names: - example.com - www.example.com provider: URL of ACME server live/ example.com ; Symlink to appropriate cert directory www.example.com ; certs/ (certificate ID)/ cert ; Contains the certificate chain ; Contains the necessary chaining certificates fullchain ; Contains the certificate and the necessary chaining certificates privkey ; Symlink to a key privkey file url ; URL of the certificate revoke ; Empty file indicating certificate should be revoked revoked ; Empty file indicating certificate has been revoked keys/ (key ID)/ privkey ; PEM-encoded certificate private key accounts/ (account ID)/ privkey ; PEM-encoded account private key authorizations/ (domain)/ expiry ; File containing RFC 3336 expiry timestamp url ; URL of the authorization (optional) conf/ ; Configuration data target ; This has the same format as a target expression file ; and is used to specify defaults. It is used to specify ; a default provider URL. Not all values which are valid ; in a target expression file may be used. webroot-path ; DEPRECATED. rsa-key-size ; DEPRECATED. ; Other, implementation-specific files may be placed in conf. tmp/ ; (used for writing files only) Preferred Location ------------------ All ACME state is stored within a directory, the State Directory. On UNIX-like systems, this directory SHOULD be "/var/lib/acme". Directory Tree -------------- ### desired An ACME State Directory expresses targets in the form of desired hostnames for which certificates are required. This normatively expresses the target set of domains. The desired hostnames list is a directory named "desired" inside the State Directory, containing zero or more files. Each file represents a target. The function of an implementation of this specification is simply this: to ensure that currently valid certificates are always provisioned which provide for all hostnames expressed across all targets. Each target is a YAML file. The file schema looks like this: satisfy: names: - example.com - www.example.com - foo.example.com request: names: ... provider: (URL of ACME server) priority: 0 The target file is principally divided into "satisfy" and "request" sections. The satisfy section controls the conditions which must be satisfied by a certificate in order to satisfy this target. The request section controls the parameters of a new certificate request made to satisfy the target. **Conditions to be satisfied.** The "satisfy" section can contain the following values: - `names`: A list of hostname strings. If not set, and the filename is a valid hostname, defaults to a list containing only that hostname. Hostnames SHOULD be in lowercase with no trailing dot but hostnames not in this form MUST be accepted and so canonicalized. IDN domain names in Unicode form MUST be converted to their equivalent ASCII form. (All text files in a State Directory must be UTF-8 encoded.) (The lumping of hostnames into different target files controls when separate certificates are issued, and when single certificates with multiple SANs are issued. For example, creating two empty files, `example.com` and `www.example.com`, would result in two certificates. Creating a single file `example.com` which specifies names to be satisfied of `example.com` and `www.example.com` would result in one certificate with both names.) **Certificate request parameters.** The "request" section can contain the following values: - `names`: A list of hostname strings. Defaults to the names set under "satisfy". This setting is specified for design parity, but it is not envisioned that a user will ever need to set it explicitly. - `provider`: A string which is the URL of an ACME server from which to request certificates. Optional; if not specified, an implementation-specific default ACME server is used. **Backwards compatibility.** For compatibility with previous versions, implementations SHOULD check for keys "names" and "provider" at the root level and if present, move them to the "satisfy" and "request" sections respectively. **Target set disjunction priority.** The "priority" value is special. It is an integer defaulting to 0. **Target set disjunction procedure.** In order to ensure consistent and deterministic behaviour, and to minimise the number of certificate requests which need to be made in regard of overlapping name sets, the sets of names to be satisfied by each target are modified to ensure that the sets are fully disjoint. That is, any given hostname must appear in at most one target's list of names to be satisfied. The procedure operates as follows: - Take the list of targets and sort it in descending order of priority value. For targets with equal priority, tiebreak using the number of hostnames to be satisfied in descending order. Where the number of hostnames is equal, the tiebreaker is implementation-specified, but SHOULD be deterministic. - Now iterate through the targets in that order. Create an empty dictionary mapping hostnames to targets. This dictionary shall be called the Hostname-Target Mapping and should be retained in memory even after the disjunction procedure is completed. - For each hostname to be satisfied for a target, if that hostname is not already in the dictionary, add it, pointing to the given target. - Copy the list of hostnames to be satisfied by the given target, and remove any hostnames from it which were already in the dictionary (i.e., where the map does not point to this target). This modified hostname list is called the reduced set of hostnames to be satisfied. Keep both the full set of hostnames to be satisfied and the reduced set of hostnames to be satisfied in memory for each target. The on-disk target files are not modified. Wildcard certificates, if ACME ever supports them, may be indicated just as they would be in a certificate. For example, an empty file named `*.example.com` could be created in the desired directory. **Disjunction example.** This section is non-normative. Suppose that the following targets were created: Target 01: a.example.com b.example.com c.example.com Target 02: a.example.com b.example.com Target 03: b.example.com c.example.com Target 04: a.example.com c.example.com Target 05: a.example.com Target 06: b.example.com Target 07: c.example.com Target 08: c.example.com d.example.com e.example.com f.example.com Target 09: c.example.com d.example.com Target 10: c.example.com d.example.com e.example.com Suppose that all targets have the default priority zero and have filenames "Target 01", etc. The targets would be sorted as follows. The hostnames in brackets are not in the reduced set. Target 08: c.example.com d.example.com e.example.com f.example.com Target 01: a.example.com b.example.com [c.example.com] Target 10:[c.example.com][d.example.com][e.example.com] Target 02:[a.example.com][b.example.com] Target 03: [b.example.com][c.example.com] Target 04:[a.example.com] [c.example.com] Target 09:[c.example.com][d.example.com] Target 05:[a.example.com] Target 06: [b.example.com] Target 07: [c.example.com] Suppose that Target 01 was changed to have a priority of 10. The sorted, reduced targets would now look like this: Target 01: a.example.com b.example.com c.example.com Target 08:[c.example.com] d.example.com e.example.com f.example.com Target 10:[c.example.com][d.example.com][e.example.com] Target 02:[a.example.com][b.example.com] Target 03: [b.example.com][c.example.com] Target 04:[a.example.com] [c.example.com] Target 09:[c.example.com][d.example.com] Target 05:[a.example.com] Target 06: [b.example.com] Target 07: [c.example.com] **Extensions for specific implementations: acmetool.** This section is non-normative, added as a practicality since this document serves as both specification and documentation. acmetool supports the following extensions: request: # Determines whether RSA or ECDSA keys are used. ECDSA keys must be # supported by the server. Let's Encrypt does not yet support ECDSA # keys, though support is imminent. Default RSA. key: type: rsa (must be "rsa" or "ecdsa") # RSA modulus size when using an RSA key. Default 2048 bits. # # Legacy compatibility: if not present, the number of bits may be # contained in a file "rsa-key-size" inside the conf directory. rsa-size: 2048 # ECDSA curve when using an ecdsa key. Default "nistp256". # # It is strongly recommended that you use nistp256. Let's Encrypt # will not support nistp521. ecdsa-curve: nistp256 (must be "nistp256", "nistp384" or "nistp521") # If specified, specifies a key ID which should be used as the private # key for all generated requests. If not set or the key ID is not found, # generate a new key for every request. id: string # Request OCSP Must Staple in certificates. Defaults to false. ocsp-must-staple: true challenge: # Webroot paths to use when requesting certificates. Defaults to none. # This is usually used in the default target file. While you _can_ override # this in a specific target, you should think very carefully by doing so. # In almost all cases, it is better to use symlinks or aliases to ensure # that the same directory is used for all vhosts. # # Legacy compatibility: the file "webroot-path" in the conf directory # contains a list of webroot paths, one per line. webroot-paths: - /some/webroot/path/.well-known/acme-challenge # A list of additional ports to listen on. Each item can be a port # number, or an explicit bind address (e.g. "[::1]:402"). Specifying a # port number x is equivalent to specifying "[::1]:x" and "127.0.0.1:x". http-ports: - 80 - 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 account-specific information. It contains zero or more subdirectories, each of which relates to a specific account. Each subdirectory MUST be named after the Account ID. Each account subdirectory MUST contain a file "privkey" which MUST contain the account private key in PEM form. An ACME client which needs to request a certificate from a given provider (as expressed by a target or used as a default) which finds that no account corresponding to that provider URL exists should generate a new account key and store it for that provider URL. #### authorizations An ACME client MAY keep track of unexpired ACME authorizations it has obtained from a provider in order to avoid unnecessarily rerequesting authorizations. It does this by maintaining a directory "authorizations" underneath a given account directory. Each directory in this directory represents a hostname. Each such directory MAY contain the following files: - "expiry", a file containing an RFC 3336 timestamp representing the expiry time of the authorization. - "url", a file containing the URL of the authorization. An authorization is deemed valid and useable for the purposes of requesting a certificate only if it has an "expiry" file expressing a point in time in the future. ### keys An ACME State Directory MUST contain a subdirectory "keys" which contains private keys used for certificates. It contains zero or more subdirectories, each of which relates to a specific key. Each subdirectory MUST be named after the Key ID. Each key subdirectory MUST contain a file "privkey" which MUST contain the private key in PEM form. An ACME client creates keys as necessary to correspond to certificates it requests. An ACME client SHOULD create a new key for every certificate request. ### certs An ACME State Directory MUST contain a subdirectory "certs" which contains information about issued or requested certificates. It contains zero or more subdirectories, each of which relates to a specific certificate. Each subdirectory MUST be named after the Certificate ID. Each certificate subdirectory MUST contain a file "url" which contains the URL for the certificate encoded in UTF-8. Clients MUST NOT include trailing newlines or whitespace but SHOULD accept such whitespace and strip it. A client SHOULD automatically delete any certificate directory if the certificate it contains is expired AND is not referenced by the "live" directory. Certificates which have expired but are still referenced by the "live" directory MUST NOT be deleted to avoid breaking reliant applications. A certificate subdirectory MAY also contain information obtained from the "url" file. If an ACME client finds only an "url" file, it MUST retrieve the certificate information to ensure that local system services can make use of the certificate: - If retrieval of the certificate fails with a permament error (e.g. 404), the certificate directory SHOULD be deleted. - If retrieval of the certificate fails with a temporary error (e.g. 202), the client tries again later. If provided, the Retry-After HTTP header should be consulted. - If retrieval of the certificate succeeds, but the private key required to use it cannot be found, the certificate directory SHOULD be deleted. After having successfully retrieved the certificate, the following files MUST be written in the certificate subdirectory: - "cert": A file which MUST contain the PEM-encoded certificate. - "chain": A file which MUST contain the PEM-encoded certificate chain, i.e. the concatenation of the PEM encodings of all certificates but for the issued certificate itself and the root certificate which are necessary to validate the certificate. In other words, this contains any necessary intermediate certificates. - "fullchain": A file which MUST contain the concatenation of the "cert" and "chain" files. - "privkey": This MUST be a relative symlink to the privkey file of the private key used to create the certificate (i.e. a symlink pointing to `../../keys/(key ID)/privkey`). ### live An ACME State Directory MUST contain a subdirectory "live". It contains zero or more relative symlinks, each of which MUST link to a subdirectory of the "certs" directory. The name of each symlink MUST be a hostname which is expressed, or was previously expressed by one or more targets. The "live" directory MUST point to the Most Preferred Certificate for each target, as specified below. Thus an application requiring a certificate for a given hostname can unconditionally specify `/var/lib/acme/live/example.com/{cert,privkey}` for the certificate, private key, etc. ### tmp, Rules for State Directory Mutation An ACME State Directory MUST contain a subdirectory "tmp" which is used for storing temporary files. This directory is used instead of some system-scope temporary directory to ensure that new files are created on the same filesystem and thus can be atomically renamed to their desired final locations in the ACME State Directory. For temporary files which do not require this, other temporary directories may be more suitable. **Any change to any object in the ACME State Directory MUST be one of the following operations:** - An idempotent recursive directory creation ("mkdir -p"). - Writing to a temporary file securely created with a high-entropy filename in "tmp" and appropriately locked, then either atomically moving it to its desired location in the ACME State Directory (potentially overwriting an existing file) or deleting it (e.g. in the event of an error before the file is completely written). - Securely creating a new symlink with a high-entropy filename in "tmp", then either atomically moving it to its desired location in the ACME State Directory (potentially overwriting an existing symlink) or deleting it. - Atomically deleting a file or recursively deleting a directory. - Idempotently changing file or directory permissions or ownership to conform with security requirements. When an ACME client finds files in the "tmp" directory which it did not itself open (in its current invocation), it SHOULD delete them. It SHOULD perform this check whenever invoked. Files MUST be created with the permissions they are to ultimately hold, not have their permissions modified afterwards. Where particular permissions are required of certain files, those permissions SHOULD be verified on every invocation of the client. Where particular permissions are required of a directory, those permissions MUST be verified before moving any file into that directory. Note that because all files begin in the "tmp" directory, their permissions MUST be strictly as strict or stricter than the permissions of any direct or indirect parent directory, at least until the move is completed. ### Permissions (POSIX) The following permissions on a State Directory MUST be enforced: - The "accounts", "keys" and "tmp" directories and all subdirectories within them MUST have mode 0770 or stricter. All files directly or ultimately within these directories MUST have mode 0660 or stricter, except for files in "tmp", which MUST have the permissions appropriate for their ultimate location before they are moved to that location. - For all other files and directories, appropriate permissions MUST be enforced as determined by the implementation. Generally this will mean directories having mode 0755 and files having mode 0644. Files and directories MUST NOT be world writable. The ownership of a State Directory and all files and directories directly or ultimately within it SHOULD be verified and enforced. ### Use of Symlinks All symlinks used within the State Directory MUST be unbroken, MUST point to locations within the State Directory and MUST be relatively expressed (i.e., they MUST NOT break if the State Directory were to be moved). Implementations SHOULD verify these properties for any symlinks they encounter in the State Directory. Hooks ----- It is desirable to provide extensibility in certain circumstances via the means of hooks. These hooks are implemented using executable shell scripts or binaries external to an implementation. Several types of hook are defined. All hooks are kept in a separate directory, the ACME Hooks Directory. The RECOMMENDED path is `/usr/lib/acme/hooks`, except for systems which use `/usr/libexec`, which SHOULD use `/usr/libexec/acme/hooks`. The hooks directory MUST contain only executable objects (i.e. executable scripts or binaries or symlinks to them). However, implementations SHOULD ignore non-executable objects. "Executable" here means executable in practical terms, and does not refer merely to the file having the executable bits set in its mode, which is a necessary but not sufficient condition. ### Calling Convention An ACME client MUST invoke hooks as follows: Take the list of objects in the hooks directory and sort them in ascending lexicographical order by filename. Execute each object in that order. If execution of an object fails, execution of subsequent objects MUST continue. The first argument when invoking a hook is always the event type causing invocation of the hook. When invoking a hook, the environment variable `ACME_STATE_DIR` MUST be set to the absolute path of the State Directory. A hook is invoked successfully if it exits with exit code 0. A hook which exits with exit code 42 indicates a lack of support for the event type. Any other exit code indicates an error. ### sudo Protocol It may be desirable for an implementation to run as an unprivileged user. In this case, it is necessary to have some way to elevate notification hooks so they can perform privileged operations such as restarting system services. Since most POSIX systems do not support the setuid bit on scripts, the use of "sudo" is suggested. When an implementation is not running as root, and executes a hook, and that hook is owned by root, and it has the setuid bit set, and the OS does not (as currently configured) support setuid on scripts, and the "sudo" command is available, and the file begins with the characters "#!", execute "sudo -n -- FILE EVENT-TYPE ARGS...", where FILE is the absolute path to the file and ARGS are dictated by hook event type. Success is not guaranteed as the system administrator must have configured the sudoers file to allow this operation. ### live-updated The "live-updated" hook is invoked when one or more symlinks in the "live" directory are created or updated. There are no arguments. Each object invoked MUST have passed to stdin a list of the names of the symlinks in the "live" directory which have changed target, i.e. the hostnames for which the preferred certificate has changed. The hostnames are separated by newlines, and the final hostname also ends with a newline. ### challenge-http-start, challenge-http-stop These hooks are invoked when an HTTP challenge attempt begins and ends. They can be used to install challenge files at arbitrary locations. The first argument is the hostname to which the challenge relates. The second argument is the filename of the target file causing the challenge to be completed. This may be the empty string in some circumstances; for example, when an authorization is being obtained for the purposes of performing revocation rather than for obtaining a certificate. The third argument is the filename which must be provisioned under `/.well-known/acme-challenge/`. The required contents of the file is passed as stdin. A hook should exit with exit code 0 only if it successfully installs or removes the challenge file. For `challenge-http-start`, an implementation may consider such an exit to authoritatively indicate that it is now feasible to complete the challenge. Example call: ```sh echo evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA.nP1qzpXGymHBrUEepNY9HCsQk7K8KhOypzEt62jcerQ | \ ACME_STATE_DIR=/var/lib/acme /usr/lib/acme/hooks/foo \ challenge-http-start example.com some-target-file \ evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA ``` ### challenge-tls-sni-start, challenge-tls-sni-stop These hooks are invoked when a TLS-SNI challenge begins and ends. They can be used to install the necessary validation certificate by arbitrary means. The hook MUST return 0 only if it succeeds at provisioning/deprovisioning the challenge. When returning 0 in the `challenge-tls-sni-start` case, it MUST return only once the certificate is globally visible. The first argument is the hostname to which the challenge relates. The second argument is the filename of the target file causing the challenge to be completed. This may be the empty string in some circumstances; for example, when an authorization is being obtained for the purposes of performing revocation rather than for obtaining a certificate. The third argument is the hostname which will be specified via SNI when the validation server checks for the certificate. The fourth argument is an additional hostname which must appear in the certificate. Both hostnames must appear as dNSName SubjectAlternateNames in the certificate returned. The third and fourth argument may be equal in some cases. A PEM-encoded certificate followed by a PEM-encoded private key is fed on stdin. A hook can choose to provision this certificate to satisfy the challenge. It can also construct its own certificate. ### challenge-dns-start, challenge-dns-stop These hooks are invoked when a DNS challenge begins and ends. They can be used to install the necessary validation DNS records, for example via DNS UPDATE. The hook MUST return 0 only if it succeeds at provisioning/deprovisioning the challenge. When returning 0 in the `challenge-dns-start` case, it MUST return only once the record to be provisioned is globally visible at all of the authoritative nameservers for the applicable zone. The hook is not required to consider the effects of caching resolvers as ACME servers will perform the lookup directly. The first argument is the hostname to which the challenge relates. The second argument is the filename of the target file causing the challenge to be completed. This may be the empty string in some circumstances; for example, when an authorization is being obtained for the purposes of performing revocation rather than for obtaining a certificate. The third argument is the value of the DNS TXT record to be provisioned. Note that as per the ACME specification, the TXT record must be provisioned at `_acme-challenge.HOSTNAME`, where HOSTNAME is the hostname given. Example call: ```sh ACME_STATE_DIR=/var/lib/acme /usr/lib/acme/hooks/foo \ challenge-dns-start example.com some-target-file \ evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA ``` SRV-ID ------ The desire for a certificate containing a SRV-ID subjectAltName is expressed by placing a file in the "desired" directory named after the SRV-ID, e.g. "\_xmpp-client.example.com". This is recognised as a SRV-ID automatically by virtue of it starting with an underscore. Since hostnames may not contain underscores, this is not ambiguous. Support for SRV-ID in ACME implementations remains to be seen. Operations ---------- ### Conform To conform a State Directory means to examine everything in the directory for consistency and validity. Permissions are changed as necessary to ensure they match the implementation's policy. The implementation verifies that all symlinks are unbroken, relative and point to locations within the State Directory. Remnant temporary files are deleted. Errors are indicated for any malformed directory (e.g. account directory with no private key, etc.) This operation is idempotent. ### Reconcile A certificate can be described as "satisfying" a target, or as being the Most Preferred Certificate for a target. These are distinct classifications, and neither implies the other. A certificate might be the Most Preferred Certificate for a target even though it does not satisfy it, because it is the "least worst option". A certificate might satisfy a target but not be the Most Preferred Certificate for it. The reconcile operation is the actual act of “building” the State Directory. - Begin by performing the Conform operation. - If there are any uncached certificates (certificate directories containing only an "url" file), cache them, waiting for them to become available if necessary. - If there are any certificates marked for revocation (meaning that a "revoke" file exists in the certificate directory), but which are not marked as being revoked (meaning that a "revoked" file exists in the certificate directory), request revocation of the certificate and, having obtained confirmation of that revocation, create an empty file "revoked" in the certificate directory. - For each target, satisfy that target. To satisfy a target: - If there exists a certificate satisfying the target, the target is satisfied. Done. - Otherwise, request a certificate with the hostnames listed under the "request" section of the target. If a certificate cannot be obtained, fail. Satisfy the target again. When making certificate requests, use the provider/account information specified in the "request" section. To request a certificate: - Obtain any necessary authorizations, using the authorization information stored for the account to be used in the State Directory to determine which authorizations definitely do not need to be acquired. - Having successfully acquired all necessary authorizations, form an appropriate CSR containing the SANs specified in the "request" section of the applicable target and request a certificate. Write the certificate URL to the State Directory. - Update the "live" directory as follows: - For each (hostname, target) pair in the Hostname-Target Mapping, create a symlink for the hostname pointing to the Most Preferred Certificate for the target, if one exists. - If any certificates were requested while satisfying targets, perform the Reconcile operation again; stop. - Optionally perform cleanup operations: - Delete the certificate directories for any cullable certificates. - Delete (optionally, securely erase) the key directories for any cullable private keys. This operation is idempotent. **Satisfying targets.** A certifigate satisfies a target if: - the private key for the certificate is available in the State Directory, and - the certificate is not known to be revoked, and - all stipulations listed in the "satisfy" section of the target are met: - the "names" stipulation is met if the dNSName SANs in a given certificate are a superset of the names specified. and - the certificate is not self-signed, and - the current time lies between the Not Before and Not After times, and - the certificate is not near expiry. **Near expiry.** A certificate is near expiry if the difference between the current time and the "Not After" time is less than some implementation-specific threshold. The RECOMMENDED threshold is 30 days or 33% of the validity period, whichever is lower. **Most Preferred Certificate.** The Most Preferred Certificate for a given target is determined as follows: - Certificates which satisfy the target are preferred over certificates that do not satisfy the target. - For two certficates neither of which satisfies the target, one is preferred over the other if the first criterion in the list of the criteria for satisfying a target which it does not satisfy is later in the list of criteria than for the other. For example, a revoked certificate for which a private key is available is preferred over a certificate for which no private key is available. A self-signed certificate with the right names is preferred over a self or CA-signed certificate with the wrong names. A self-signed certificate is preferred over a revoked certificate. (A revoked certificate may not be exemptible by a user; thus even a self-signed certificate is preferable to a certificate known to be revoked.) - Certificates with later "Not After" times are preferred. **Cullability.** A certificate is cullable if: - it is expired, and - after reconcilation, it is unreferenced by any "live" symlink. A private key is cullable if: - it does not relate to any known certificate, and - it was not recently created or imported. The definition of "recently" is implementation-specific. ### Revocation A certificate is revoked by creating an empty file "revoke" in the certificate directory and reconciling. Identifiers ----------- Accounts, keys and certificates are stored in directories named by their identifiers. Their identifiers are calculated as follows: **Key ID:** Lowercase base32 encoding with padding stripped of the SHA256 hash of the subjectPublicKeyInfo constructed from the private key. **Account ID:** Take the Directory URL for the ACME server. Take the hostname, port (if applicable) and path, stripping the scheme (e.g. "example.com/directory"). If the path is "/", strip it ("example.com/" becomes "example.com"). URL-encode this string so that any slashes are percent-encoded using lowercase hexadecimal. Take this string and append "/" followed by the string formed by calculating a Key ID using the account's private key. e.g. `example.com%2fdirectory/irq7564p5siu3zngnc2caqygp3v53dfmh6idwtpyfkxojssqglta` Each account directory is thus an account key-specific subdirectory of the string formed from the directory URL. For production use the scheme MUST be "https". In some cases, it may be desirable to test using HTTP. Where an HTTP URL is specified, it is prefixed with `http:`. For example: `http:example.com%2fdirectory/irq7564p5siu3zngnc2caqygp3v53dfmh6idwtpyfkxojssqglta` **Certificate ID:** A certificate ID must be assignable before a certificate has been issued, when only the public key and certificate URL are known. Thus, the Certificate ID shall be the lowercase base32 encoding with padding stripped of the SHA256 hash of the certificate URL. A certificate directory is invalid if the "url" file does not match the Certificate ID. Such a directory should be deleted. Temporary Use of Self-Signed Certificates ----------------------------------------- Some daemons may fail terminally when a certificate file referenced by their configuration is not present. Thus, where a client is unable to procure a certificate immediately, it MAY choose to provision a self-signed certificate referenced by symlinks under 'live' instead. This will allow a daemon to continue operating (perhaps serving non-TLS requests or requests for other hostnames) with reduced functionality. If a client uses such interim self-signed certificates, it MUST create an empty 'selfsigned' file in the certificate directory to indicate that the certificate is a self-signed certificate. The 'url' file MUST NOT exist. The 'cert' and 'fullchain' files MUST be identical, and the 'chain' file MUST exist and MUST be an empty file. The self-signed certificate MAY contain information in it which points out the configuration issue the certificate poses, e.g. by placing a short description of the problem in the O and OU fields, e.g.: OU=ACME Cannot Acquire Certificate O=ACME Failure Please Check Server Logs The Certificate ID of a self-signed certificate is the string "selfsigned-" followed by the lowercase base32 encoding with padding stripped of the SHA256 hash of the DER encoded certificate. acme-0.0.62/_doc/WSCONFIG.md000066400000000000000000000001401321256625400151520ustar00rootroot00000000000000# [This document has moved.](https://hlandau.github.io/acme/userguide#web-server-configuration) acme-0.0.62/_doc/dns.hook000077500000000000000000000053731321256625400150770ustar00rootroot00000000000000#!/bin/bash # This is an example DNS hook script which uses the nsupdate utility to update # nameservers. The script waits until updates have propagated to all # nameservers listed for a zone. The script fails if this takes more than 60 # seconds by default; this timeout can be adjusted. # # The script is ready to use, but to use it you must create # /etc/default/acme-dns or /etc/conf.d/acme-dns and set the following options: # # # Needed if using TKIP for updates. If authenticating updates by source IP, # # not necessary. # TKIP_KEY_NAME="hmac-sha256:tk1" # TKIP_KEY="a base64-encoded TKIP key" # # # DNS synchronization timeout in seconds. Default is 60. # DNS_SYNC_TIMEOUT=60 # # # Optional: inject extra arguments and commands to nsupdate. # NSUPDATE_ARGS="-v" # nsupdate_cmds() { # # Usually not necessary: # echo zone example.com. # } # # Having done this, rename it to /usr/lib[exec]/acme/hooks/dns. # # How to test this script: # ./dns.hook challenge-dns-start example.com "" "foobar" # ./dns.hook challenge-dns-stop example.com "" "foobar" # set -e get_apex() { local name="$1" if [ -z "$name" ]; then echo "$0: couldn't get apex for $name" >&2 return 1 fi 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 local sname="$(echo $name | sed 's/^[^.]\+\.//')" get_apex "$sname" } waitns() { local ns="$1" for ctr in $(seq 1 "$DNS_SYNC_TIMEOUT"); do [ "$(dig +short "@${ns}" TXT "_acme-challenge.${CH_HOSTNAME}." | grep -- "$CH_TXT_VALUE" | wc -l)" == "1" ] && return 0 sleep 1 done # Best effort cleanup. echo $0: timed out waiting ${DNS_SYNC_TIMEOUT}s for nameserver $ns >&2 updns delete || echo $0: failed to clean up records after timing out >&2 return 1 } updns() { local op="$1" ( declare -f nsupdate_cmds >/dev/null && nsupdate_cmds "$APEX" [ -n "$TKIP_KEY" ] && echo key "$TKIP_KEY_NAME" "$TKIP_KEY" echo update $op "_acme-challenge.${CH_HOSTNAME}." 60 IN TXT "\"${CH_TXT_VALUE}\"" echo send ) | nsupdate $NSUPDATE_ARGS } [ -e "/etc/default/acme-dns" ] && . /etc/default/acme-dns [ -e "/etc/conf.d/acme-dns" ] && . /etc/conf.d/acme-dns # e.g. # TKIP_KEY_NAME="hmac-sha256:tk1" # TKIP_KEY="base64-key-value" EVENT_NAME="$1" CH_HOSTNAME="$2" CH_TARGET_FILENAME="$3" CH_TXT_VALUE="$4" [ -z "$DNS_SYNC_TIMEOUT" ] && DNS_SYNC_TIMEOUT=60 case "$EVENT_NAME" in challenge-dns-start) get_apex "$CH_HOSTNAME" updns add # Wait for all nameservers to update. for ns in $(dig +short NS "${APEX}."); do waitns "$ns" done ;; challenge-dns-stop) get_apex "$CH_HOSTNAME" updns delete ;; *) exit 42 ;; esac acme-0.0.62/_doc/ovh.hook000077500000000000000000000130451321256625400151020ustar00rootroot00000000000000#!/bin/bash # # This is a hook to complete DNS challenge using OVH API. # # For this script to work you need the following prereqs: # # 1) Install ovh-cli (https://github.com/toorop/ovh-cli) # 2) Install dig (Centos/RH: bind-utils; Ubuntu/Debian: dnsutils) # 3) Register OVH API (execute ovh-cli without parameters and set environment variables: OVH_CONSUMER_KEY/OVH_APP_SECRET/OVH_APP_KEY) # 4) Place this script to /usr/lib[exec]/acme/hooks/ovh # # FAQ: # # Q: I have my domains under several OVH accounts, how do I manage them all? # A: Setup wrapper scripts and pass them to OVH_API_SCRIPTS environment variable, ie. # # echo -en "OVH_CONSUMER_KEY=key1 OVH_APP_SECRET=secret1 OVH_APP_KEY=app1 ovh-cli $*" >/usr/local/bin/ovh-ab1234 # echo -en "OVH_CONSUMER_KEY=key2 OVH_APP_SECRET=secret2 OVH_APP_KEY=app2 ovh-cli $*" >/usr/local/bin/ovh-ab1235 # chmod a+rx /usr/local/bin/ovh-ab1234 /usr/local/bin/ovh-ab1235 # export OVH_API_SCRIPTS="/usr/local/bin/ovh-ab1234 /usr/local/bin/ovh-ab1235" # # Q: Why it takes so long? # A: Dunno, ask OVH :) # You can also increase timeout by setting environment variable DNS_SYNC_TIMEOUT. # # Q: Do I need to pass my environment variables all the times? # A: No, you can place them in /etc/default/acme-ovh or /etc/conf.d/acme-ovh # # Q: How to test this script? # A: Just call "test" action and pass it a domain name (./ovh.hook is hook name here, not ovh-cli): # # ./ovh.hook test example.com # set -e configure() { if [ -z "$OVH_API_SCRIPTS" ] ; then if which ovh-cli >/dev/null 2>&1 ; then OVH_API_SCRIPTS=ovh-cli elif which ovh >/dev/null 2>&1 ; then OVH_API_SCRIPTS=ovh else echo "OVH_API_SCRIPTS were not provided and ovh-cli has not been found" >&2 exit 1 fi if [ -z "OVH_CONSUMER_KEY" ] || [ -z "$OVH_APP_SECRET" ] || [ -z "$OVH_APP_KEY" ] ; then echo "ovh-cli has been found, but one of configuration variables OVH_CONSUMER_KEY/OVH_APP_SECRET/OVH_APP_KEY is empty" >&2 exit 1 fi fi } get_apex() { local name="$1" if [ -z "$name" ]; then echo "$0: couldn't get apex for $name" >&2 return 1 fi if ! echo "$name"|grep -q "\." ; then echo "$0: \"$name\" does not have a dot, which most likely mean that \"$CH_HOSTNAME\" is not delegated (expired?)" >&2 return 1 fi if dig +noall +answer SOA "${name}." |grep -q SOA ; then APEX="$name" return fi local sname="$(echo $name | sed 's/^[^.]\+\.//')" get_apex "$sname" } get_api_script() { APEX_SCRIPT=$(for script in $OVH_API_SCRIPTS ; do if $script domain list --json 2>&1 | grep -q "\"${APEX}\"" ; then echo $script fi done) if [ -z "$APEX_SCRIPT" ] ; then echo "$0: it's not possible to manage $APEX with $OVH_API_SCRIPTS" >&2 for script in $OVH_API_SCRIPTS ; do echo "$script domains list:" >&2 $script domain list 2>&1 | sed -e 's/^/ /' >&2 echo >&2 done return 1 fi } waitns() { local ns="$1" local tick="$2" for ctr in $(seq $DNS_SYNC_TIMEOUT); do seq="$ctr/$DNS_SYNC_TIMEOUT" dig +short "@${ns}" TXT "_acme-challenge.${CH_HOSTNAME}." | grep -q "$CH_TXT_VALUE" && return 0 [ ! -z "$tick" ] && echo -n "$tick" >&2 sleep 1 done # Best effort cleanup. echo $0: timed out waiting ${DNS_SYNC_TIMEOUT}s for nameserver $ns >&2 remove_challenge || echo $0: failed to clean up records after timing out >&2 return 1 } add_challenge() { [ -z "$CH_TXT_VALUE" ] && return 1 $APEX_SCRIPT domain zone newrecord $APEX --field TXT --target "$CH_TXT_VALUE" --sub _acme-challenge --ttl 60 --json >&2 $APEX_SCRIPT domain zone reload $APEX } remove_challenge() { [ -z "$CH_TXT_VALUE" ] && return 1 recid=$($APEX_SCRIPT domain zone getrecords $APEX 2>&1 | while read id rec data ; do if [ "$rec" == "_acme-challenge.${APEX}" ] ; then echo $id ; fi ; done) for id in $recid ; do $APEX_SCRIPT domain zone delrecord $APEX $id >&2 done $APEX_SCRIPT domain zone reload $APEX } test() { echo "Managing \"$APEX\" with script: $APEX_SCRIPT" >&2 [ -z "$CH_TXT_VALUE" ] && CH_TXT_VALUE="foobarszcz" echo -n "Trying to add record _acme-challenge.${APEX} record: " >&2 add_challenge # will return json, so we don't print anything here echo -n "Checking DNSes: " >&2 for ns in $(dig +short NS "${APEX}."); do echo -n "$ns" >&2 waitns "$ns" "." echo -n " " >&2 done echo "done." >&2 echo -n "Trying to remove _acme-challenge.${APEX} record: " >&2 remove_challenge && echo "done." >&2 echo -n "Testing if there is any garbage left: " >&2 if $APEX_SCRIPT domain zone getrecords $APEX --json 2>&1 | grep -q "\"_acme-challenge.${APEX}\"" ; then echo "Oops, it seems there's _acme-challenge.${APEX} listed, please examine domain manually!" >&2 return 1 fi echo "done." >&2 echo -en "All good!\n\nDomain contents:$($APEX_SCRIPT domain zone import $APEX 2>&1)\n" >&2 } [ -e "/etc/default/acme-ovh" ] && . /etc/default/acme-ovh [ -e "/etc/conf.d/acme-ovh" ] && . /etc/conf.d/acme-ovh EVENT_NAME="$1" CH_HOSTNAME="$2" CH_TARGET_FILENAME="$3" CH_TXT_VALUE="$4" [ -z "$DNS_SYNC_TIMEOUT" ] && DNS_SYNC_TIMEOUT=180 case "$EVENT_NAME" in challenge-dns-start) configure get_apex "$CH_HOSTNAME" get_api_script remove_challenge add_challenge # Wait for all nameservers to update. for ns in $(dig +short NS "${APEX}."); do waitns "$ns" done ;; challenge-dns-stop) configure get_apex "$CH_HOSTNAME" get_api_script remove_challenge ;; test) configure get_apex "$CH_HOSTNAME" get_api_script test ;; *) exit 42 ;; esac acme-0.0.62/_doc/perm.example000066400000000000000000000047371321256625400157510ustar00rootroot00000000000000# In some rare cases it can be necessary to override the permissions that # acmetool sets on files. You can override those permissions using the # permissions configuration file, which should be placed at # $ACME_STATE_DIR/conf/perm. This is an example of such a file. You should be # very careful when using this file, and only include the minimum changes that # you need to make. # # Each line has the following syntax: # path-pattern file-mode dir-mode [uid gid] # # For example: # keys 0640 0750 # or # keys 0640 0750 root exim # # If you specify a UID, you must also specify a GID and vice versa. # UIDs and GIDs can be specified numerically, and on some platforms # they may also be specifiable as names. # # The special UID/GID value "$r" means the current UID/GID of the running # acmetool process; you can use this to ensure that the file UID/GID is # enforced to the user which acmetool runs as. # # Not specifying UID/GID values, or specifying both as "-", means that acmetool # will not pay attention to file ownership. Files will be created with their # "natural" owner (i.e., the UID/GID under which acmetool is running). # # Mode enforcement cannot be disabled. # # Nothing acmetool does should affect POSIX ACLs, if you wish to use them. # # A path-pattern is a glob pattern. Specifying the same path-pattern as a built # in permissions rule overrides that rule. You cannot place two entries for # the same path-pattern in this file. acmetool uses the longest matching pattern # when deciding what rule to use when enforcing permissions. # # The default rules are shown below: # # . 0644 0750 # Default for anything without a longer match # accounts 0600 0700 # desired 0644 0755 # live 0644 0755 # certs 0644 0755 # certs/*/haproxy 0600 0700 # Support for the HAProxy extension; contains private keys # keys 0600 0700 # conf 0644 0755 # tmp 0600 0700 # Do NOT change this # # If you wish to disable a path-pattern rule allowing policy to be inherited # from a shorter match, you can do this using the special keyword 'inherit': # # path-pattern inherit # # For example, maybe you want to make the whole directory restricted: # . 0600 0700 # accounts inherit # certs inherit # conf inherit # desired inherit # keys inherit # # Again, you should rarely ever need to use this file. When you use this file, # add only the entries that you absolutely need. acme-0.0.62/_doc/response-file.yaml000066400000000000000000000024111321256625400170530ustar00rootroot00000000000000# This is a example of a response file, used with --response-file. # It automatically answers prompts for unattended operation. # grep for UniqueID in the source code for prompt names. # Pass --response-file to all invocations, not just quickstart. # If you don't pass --response-file, it will be looked for at "(state-dir)/conf/responses". # You will typically want to use --response-file with --stdio or --batch. # 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.2-November-15-2017.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". "acmetool-quickstart-webroot-path": "/var/www/foo/bar/.well-known/acme-challenge" "acmetool-quickstart-complete": true "acmetool-quickstart-install-cronjob": true "acmetool-quickstart-install-haproxy-script": true "acmetool-quickstart-install-redirector-systemd": true "acmetool-quickstart-key-type": ecdsa "acmetool-quickstart-rsa-key-size": 4096 "acmetool-quickstart-ecdsa-curve": nistp256 acme-0.0.62/_doc/tinydns.hook000077500000000000000000000151461321256625400160020ustar00rootroot00000000000000#!/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 acme-0.0.62/acmeapi/000077500000000000000000000000001321256625400141115ustar00rootroot00000000000000acme-0.0.62/acmeapi/README.md000066400000000000000000000015051321256625400153710ustar00rootroot00000000000000# ACME Client Library [![GoDoc](https://godoc.org/github.com/hlandau/acme/acmeapi?status.svg)](https://godoc.org/github.com/hlandau/acme/acmeapi) [![Build Status](https://travis-ci.org/hlandau/acme.svg?branch=master)](https://travis-ci.org/hlandau/acme) [![Issue Stats](http://issuestats.com/github/hlandau/acme/badge/issue?style=flat)](http://issuestats.com/github/hlandau/acme) Basic ACME client library. [See godoc for the API.](https://godoc.org/github.com/hlandau/acme/acmeapi) This is distinct from acmetool in that it simply calls the server as you request. It isn't smart and it doesn't manage certificate lifetimes. It can be used by Go code independently of acmetool. [For the acmetool command line tool, see here.](https://github.com/hlandau/acme) ## Licence © 2015 Hugo Landau MIT License acme-0.0.62/acmeapi/acmeendpoints/000077500000000000000000000000001321256625400167425ustar00rootroot00000000000000acme-0.0.62/acmeapi/acmeendpoints/endpoint.go000066400000000000000000000042031321256625400211100ustar00rootroot00000000000000// Package acmeendpoints provides information on known ACME servers. package acmeendpoints import ( "fmt" "regexp" "sync" "text/template" ) // Provides information on a known ACME endpoint. type Endpoint struct { // Friendly name for the provider. Should be a short, single-line, title case // human readable description of the endpoint. Title string // Short unique endpoint identifier. Must match ^[a-zA-Z][a-zA-Z0-9_]*$ and // should use CamelCase. Code string // The ACME directory URL. Must be an HTTPS URL and typically ends in // "/directory". DirectoryURL string // If this is not "", this is a regexp which must be matched iff an OCSP // endpoint URL as found in a certificate implies that a certificate was // issued by this endpoint. OCSPURLRegexp string ocspURLRegexp *regexp.Regexp // If this is not "", this is a regexp which must be matched iff an URL // appears to be an ACME certificate URL for this endpoint. CertificateURLRegexp string certificateURLRegexp *regexp.Regexp // If this is not "", it is a Go template used to construct a certificate URL // from an *x509.Certificate. The certificate is passed as variable // "Certificate". CertificateURLTemplate string certificateURLTemplate *template.Template initOnce sync.Once } func (e *Endpoint) String() string { return fmt.Sprintf("Endpoint(%v)", e.DirectoryURL) } func (e *Endpoint) init() { e.initOnce.Do(func() { if e.OCSPURLRegexp != "" { e.ocspURLRegexp = regexp.MustCompile(e.OCSPURLRegexp) } if e.CertificateURLRegexp != "" { e.certificateURLRegexp = regexp.MustCompile(e.CertificateURLRegexp) } if e.CertificateURLTemplate != "" { e.certificateURLTemplate = template.Must(template.New("certificate-url").Parse(e.CertificateURLTemplate)) } }) } var endpoints []*Endpoint // Visit all registered endpoints. func Visit(f func(p *Endpoint) error) error { for _, p := range endpoints { err := f(p) if err != nil { return err } } return nil } // Register a new endpoint. func RegisterEndpoint(p *Endpoint) { endpoints = append(endpoints, p) } func init() { for _, p := range builtinEndpoints { RegisterEndpoint(p) } } acme-0.0.62/acmeapi/acmeendpoints/endpoint_test.go000066400000000000000000000011721321256625400221510ustar00rootroot00000000000000package acmeendpoints import ( "fmt" "testing" ) func TestVisit(t *testing.T) { ep := map[*Endpoint]struct{}{} err := Visit(func(e *Endpoint) error { ep[e] = struct{}{} return nil }) if err != nil { t.Fail() } _, ok := ep[&LetsEncryptLive] if !ok { t.Fail() } _, ok = ep[&LetsEncryptStaging] if !ok { t.Fail() } ep = map[*Endpoint]struct{}{} e1 := fmt.Errorf("e1") i := 0 err = Visit(func(e *Endpoint) error { if i == 1 { return e1 } i++ ep[e] = struct{}{} return nil }) if err != e1 { t.Fail() } if len(ep) != 1 { t.Fail() } _, ok = ep[&LetsEncryptLive] if !ok { t.Fail() } } acme-0.0.62/acmeapi/acmeendpoints/endpoints.go000066400000000000000000000023471321256625400213020ustar00rootroot00000000000000package acmeendpoints var ( // Let's Encrypt (Live) LetsEncryptLive = Endpoint{ Code: "LetsEncryptLive", Title: "Let's Encrypt (Live)", DirectoryURL: "https://acme-v01.api.letsencrypt.org/directory", OCSPURLRegexp: `^http://ocsp\.int-[^.]+\.letsencrypt\.org\.?(/.*)?$`, CertificateURLRegexp: `^https://acme-v01\.api\.letsencrypt\.org\.?/acme/cert/.*$`, CertificateURLTemplate: `https://acme-v01.api.letsencrypt.org/acme/cert/{{.Certificate.SerialNumber|printf "%036x"}}`, } // Let's Encrypt (Staging) LetsEncryptStaging = Endpoint{ Code: "LetsEncryptStaging", Title: "Let's Encrypt (Staging)", DirectoryURL: "https://acme-staging.api.letsencrypt.org/directory", OCSPURLRegexp: `^http://ocsp\.(staging|stg-int)-[^.]+\.letsencrypt\.org\.?(/.*)?$`, CertificateURLRegexp: `^https://acme-staging\.api\.letsencrypt\.org\.?/acme/cert/.*$`, CertificateURLTemplate: `https://acme-staging.api.letsencrypt.org/acme/cert/{{.Certificate.SerialNumber|printf "%036x"}}`, } ) // Suggested default endpoint. var DefaultEndpoint = &LetsEncryptLive var builtinEndpoints = []*Endpoint{ &LetsEncryptLive, &LetsEncryptStaging, } acme-0.0.62/acmeapi/acmeendpoints/url.go000066400000000000000000000107531321256625400201010ustar00rootroot00000000000000package acmeendpoints import ( "bytes" "crypto/sha256" "crypto/x509" "errors" "fmt" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/xlog" "golang.org/x/net/context" "net/url" "regexp" ) var log, Log = xlog.New("acme.endpoints") // Returned when no matching endpoint can be found. var ErrNotFound = errors.New("no corresponding endpoint found") // Finds an endpoint with the given directory URL. If no such endpoint is // found, returns ErrNotFound. func ByDirectoryURL(directoryURL string) (*Endpoint, error) { for _, e := range endpoints { if directoryURL == e.DirectoryURL { return e, nil } } return nil, ErrNotFound } // If an endpoint exists with the given directory URL, returns it. // // Otherwise, tries to create a new endpoint for the directory URL. Where // possible, endpoint parameters are guessed. Currently boulder is supported. // Non-boulder based endpoints will not have any parameters set other than the // directory URL, which means some operations on the endpoint will not succeed. // // It is acceptable to change the fields of the returned endpoint. // By default, the title of the endpoint is the directory URL. func CreateByDirectoryURL(directoryURL string) (*Endpoint, error) { e, err := ByDirectoryURL(directoryURL) if err == nil { return e, nil } // Make a code for the endpoint by hashing the directory URL... h := sha256.New() h.Write([]byte(directoryURL)) code := fmt.Sprintf("Temp%08x", h.Sum(nil)[0:4]) e = &Endpoint{ Title: directoryURL, DirectoryURL: directoryURL, Code: code, } guessParameters(e) return e, nil } func guessParameters(e *Endpoint) { u, err := url.Parse(e.DirectoryURL) if err != nil { return } // not boulder if u.Path != "/directory" { return } if e.CertificateURLRegexp == "" { e.CertificateURLRegexp = "^https://" + regexp.QuoteMeta(u.Host) + "/acme/cert/.*$" } if e.CertificateURLTemplate == "" { e.CertificateURLTemplate = "https://" + u.Host + "/acme/cert/{{.Certificate.SerialNumber|printf \"%036x\"}}" } } // Given an URL to a certificate, tries to determine the directory URL. func CertificateURLToDirectoryURL(certificateURL string) (string, error) { for _, e := range endpoints { e.init() if e.certificateURLRegexp != nil && e.certificateURLRegexp.MatchString(certificateURL) { return e.DirectoryURL, nil } } return "", ErrNotFound } // Given a certificate in DER form, tries to determine the set of endpoints // which may have issued the certificate. certain is true if the returned // endpoint definitely issued the certificate, in which case len(endpoints) == // 1 (but len(endpoints) == 1 does not necessarily imply certainty). func CertificateToEndpoints(cert *x509.Certificate) (endp []*Endpoint, certain bool, err error) { var unknownEndpoints []*Endpoint for _, e := range endpoints { e.init() if e.ocspURLRegexp == nil { unknownEndpoints = append(unknownEndpoints, e) } log.Debugf("cert has OCSP %v", cert.OCSPServer) for _, ocspServer := range cert.OCSPServer { log.Debugf("%v %v", e, ocspServer) if e.ocspURLRegexp != nil && e.ocspURLRegexp.MatchString(ocspServer) { return []*Endpoint{e}, true, nil } } } if len(unknownEndpoints) > 0 { return unknownEndpoints, false, nil } log.Debugf("cannot find any endpoints for certificate") return nil, false, ErrNotFound } // Given a certificate, tries to determine the certificate URL and definite endpoint. func CertificateToEndpointURL(cl *acmeapi.Client, cert *x509.Certificate, ctx context.Context) (*Endpoint, string, error) { es, certain, err := CertificateToEndpoints(cert) if err != nil { return nil, "", err } for _, e := range es { if e.certificateURLTemplate == nil { continue } var b bytes.Buffer err = e.certificateURLTemplate.Execute(&b, map[string]interface{}{ "Certificate": cert, }) if err != nil { return nil, "", err } u := b.String() if !certain { // Check that this is the right endpoint via an HTTP request. acrt := acmeapi.Certificate{ URI: u, } err := cl.LoadCertificate(&acrt, ctx) if err != nil { continue } // check that the certificate DER matches if !bytes.Equal(acrt.Certificate, cert.Raw) { continue } } return e, u, nil } return nil, "", ErrNotFound } // Given a certificate, tries to determine the definite endpoint. func CertificateToEndpoint(cl *acmeapi.Client, cert *x509.Certificate, ctx context.Context) (*Endpoint, error) { e, _, err := CertificateToEndpointURL(cl, cert, ctx) return e, err } acme-0.0.62/acmeapi/acmeendpoints/url_test.go000066400000000000000000000177021321256625400211410ustar00rootroot00000000000000package acmeendpoints import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/acme/acmeapi/acmeutils" "github.com/hlandau/goutils/test" "golang.org/x/net/context" "math/big" "net/http" "testing" ) const leStagingTestCert = ` -----BEGIN CERTIFICATE----- MIIE6DCCA9CgAwIBAgITAPo8NeGtZ2xhrKoeMR+onLNgFzANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAxMTcxNjAz MDBaFw0xNjA0MTYxNjAzMDBaMB4xHDAaBgNVBAMTE2FxMS5saGguZGV2ZXZlci5u ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTP6pFjvAzkVohGaGH hIJ746SGTdw2cjDfqZimiBc1Yrjl1AFlLfHHLZ7Uyt3b7EYlYao6P6Vx9wKigCI+ vaeAudlZNerJa8fWNJXf4eqYoYH7vf+xnZP7TYUmiWLSGES9p8QBRCHwWPycP7mm X4kneqo/oF/asQnOmUy0hi2VyCCT/XQ93ApN5pHz8dg7A3OtOGlHXd38rJ3uBJ0N JXM6Dx5Oj833nDaa2ndkBxq5m0SLnOimE5GsqX7bWNfllMeZXqH5/3E25cgh2YTR 6JBDLqpzO9ZvFOOWcOVk0QG+zfXhHVx++6I6fs36p3/+DN58WB/JP4CLV3JvC6cE NyuvAgMBAAGjggIcMIICGDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNB3WkfIcwYM bXABE4q5k3/o1vNHMB8GA1UdIwQYMBaAFPt4TxL5YBWDLJ8XfzQZsy426kGJMHgG CCsGAQUFBwEBBGwwajAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Auc3RhZ2luZy14 MS5sZXRzZW5jcnlwdC5vcmcvMDMGCCsGAQUFBzAChidodHRwOi8vY2VydC5zdGFn aW5nLXgxLmxldHNlbmNyeXB0Lm9yZy8wHgYDVR0RBBcwFYITYXExLmxoaC5kZXZl dmVyLm5ldDCB/gYDVR0gBIH2MIHzMAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEw gdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggr BgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVk IHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ug d2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0 c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUAA4IBAQABcut7 1jVicQnHvSkQgY1CRiGSmlHyOyEKimNtCuyaAVwm3cavV/wpGTDFnePyNds4cst/ 8BcL0QaKLmE1an/oeGmfs0U8maiKbL69Yun0qTNTKOaqJP/iitwAbliQ3TzO2kOZ +a2RkPKx0/zYlZb0GzhfIwHE4Qd7/P0qLphu2UaaEpzBnRlT1F9k+cGe4DZYb4XL BZHnOmXeZrhfPeeTw4VYAEtZ7fpwRhirBjshU8kRbO7KgZh4Id+v26FQpBE3eMQ2 CWV8q8XThKcX3OaMOkLOIB2xZA7Fpj3JoDcsLPEKn5sxVgkxfjs03glTWd839qcE YAC6drs6Fev1cVa9 -----END CERTIFICATE-----` const leLiveTestCert = ` -----BEGIN CERTIFICATE----- MIIFCzCCA/OgAwIBAgISAaoIVMlVWnr9Vfrj+Ak2new4MA0GCSqGSIb3DQEBCwUA MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTAeFw0xNjAxMTEwNDQ0MDBaFw0x NjA0MTAwNDQ0MDBaMBYxFDASBgNVBAMTC2RldmV2ZXIubmV0MIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEArBzKQy0inr2oheVRuCDS2prucTF+8xQW66WP D5ZNzoypPFB9uvFSJN1QzMeq7fdLGWn3QIFj9HlntYxI7Sy47nFeciHG2lN7zfGL Lex0vREZ21ST3IfUuD/LogkAMqgjcymBiMdrO5hcPf0OIkboBe96BrBAKXTFVlme guwkdexNjedlFQ4egtzKZ2YrJXR4z9VOW0qaRNqk+9zvjLGG2mIay+NN0aTHomOk Ow+y8bFFJ9wrkMtn+/IwP1uIbyMEgF2qmKnB/G6H/Qdq52IBF1rCC5xlpWNB0w/3 aJd512AqC5WFC/yFy8ksFS7EjIhQeyqBx1unyaz13C3yrRimbwIDAQABo4ICHTCC AhkwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQY0ADTmNDEDmrqY45CJCJdwvHp7DAf BgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBwBggrBgEFBQcBAQRkMGIw LwYIKwYBBQUHMAGGI2h0dHA6Ly9vY3NwLmludC14MS5sZXRzZW5jcnlwdC5vcmcv MC8GCCsGAQUFBzAChiNodHRwOi8vY2VydC5pbnQteDEubGV0c2VuY3J5cHQub3Jn LzAnBgNVHREEIDAeggtkZXZldmVyLm5ldIIPd3d3LmRldmV2ZXIubmV0MIH+BgNV HSAEgfYwgfMwCAYGZ4EMAQIBMIHmBgsrBgEEAYLfEwEBATCB1jAmBggrBgEFBQcC ARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwgasGCCsGAQUFBwICMIGeDIGb VGhpcyBDZXJ0aWZpY2F0ZSBtYXkgb25seSBiZSByZWxpZWQgdXBvbiBieSBSZWx5 aW5nIFBhcnRpZXMgYW5kIG9ubHkgaW4gYWNjb3JkYW5jZSB3aXRoIHRoZSBDZXJ0 aWZpY2F0ZSBQb2xpY3kgZm91bmQgYXQgaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcv cmVwb3NpdG9yeS8wDQYJKoZIhvcNAQELBQADggEBAHcD+3AjdbfZylPHFYyYSIWk no90p+rWZwh3sDnWC5KzZ8jm7uMynCvr7NK0BBxIzuqlWQ0vjKq41KFkTA+GllS/ a4/1XnzrKIJ8udX698Ofsn6HEqxoT0/sAQhxGChrXDRl33QDowqquHWh8HGXx1ke jV1U4H69KjWYRNx7EN2kbik4GDznwOGpkAUPFCiW2g40zs8Lw4+RiTGPHNELzm7c TMAyWtPi4eJpMz87jYxv+jB6a4Zy5gAdEySejtGwerhGrrmntkliR8MKZQ6Lisd8 h6xyLde4iNUiXtPOr9I87FBLC1U2AnP+GldAKYB3PO1qPHy6u/a15Xg34FrD8SM= -----END CERTIFICATE-----` type urlTestCase struct { Cert string Endpoint *Endpoint } var urlTestCases = []*urlTestCase{ { Cert: leStagingTestCert, Endpoint: &LetsEncryptStaging, }, { Cert: leLiveTestCert, Endpoint: &LetsEncryptLive, }, } func TestURL(t *testing.T) { _, err := ByDirectoryURL("https://unknown/directory") if err != ErrNotFound { t.Fail() } for _, tc := range urlTestCases { e, err := ByDirectoryURL(tc.Endpoint.DirectoryURL) if err != nil { t.Fatalf("cannot get by directory URL") } if e != tc.Endpoint { t.Fatalf("got wrong endpoint: %v != %v", e, tc.Endpoint) } certs, err := acmeutils.LoadCertificates([]byte(tc.Cert)) if err != nil { t.Fatalf("cannot load test certificate") } c0, err := x509.ParseCertificate(certs[0]) if err != nil { t.Fatalf("cannot parse certificate") } cl := acmeapi.Client{} e, certURL, err := CertificateToEndpointURL(&cl, c0, context.TODO()) if err != nil { t.Fatalf("cannot map certificate to endpoint") } e2, err := CertificateToEndpoint(&cl, c0, context.TODO()) if e2 != e { t.Fatalf("mismatch") } if e != tc.Endpoint { t.Fatalf("certificate mapped to wrong endpoint: %v != %v", e, tc.Endpoint) } dURL, err := CertificateURLToDirectoryURL(certURL) if err != nil { t.Fatalf("cannot map certificate URL to directory URL: %v", err) } if dURL != e.DirectoryURL { t.Fatalf("directory URL mismatch: %v != %v", dURL, e.DirectoryURL) } } } func TestGuess(t *testing.T) { crt := &x509.Certificate{ OCSPServer: []string{ "https://example.com/", }, SerialNumber: big.NewInt(0xdeadb33f), } endp, certain, err := CertificateToEndpoints(crt) if err != ErrNotFound || endp != nil || certain { t.Fail() } e, err := CreateByDirectoryURL("https://unknown-boulder.test/directory") if err != nil { t.Fail() } RegisterEndpoint(e) e2, err := CreateByDirectoryURL("https://unknown-boulder.test/directory") if e2 != e || err != nil { t.Fatal() } e3, err := CreateByDirectoryURL("https://unknown-boulder3.test/") if err != nil { t.Fatal() } RegisterEndpoint(e3) e4, err := CreateByDirectoryURL("https://unknown-boulder4.test/directory") if err != nil { t.Fatal() } RegisterEndpoint(e4) du, err := CertificateURLToDirectoryURL("https://unknown-boulder.test/acme/cert/deadb33f") if err != nil { t.Fatal() } if du != e.DirectoryURL { t.Fatal() } du, err = CertificateURLToDirectoryURL("https://other-boulder.test/acme/cert/deadb33f") if err != ErrNotFound { t.Fatal() } endp, certain, err = CertificateToEndpoints(crt) if err != nil || certain || len(endp) != 3 { t.Fatal() } if endp[0] != e || endp[1] != e3 { t.Fail() } privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) crtb, err := x509.CreateCertificate(rand.Reader, crt, crt, &privKey.PublicKey, privKey) if err != nil { t.Fatalf("%v", err) } crtb2 := make([]byte, len(crtb)) copy(crtb2, crtb) mt := test.HTTPMockTransport{} mt.Add("unknown-boulder4.test/acme/cert/0000000000000000000000000000deadb33f", &http.Response{ StatusCode: 200, Header: http.Header{ "Content-Type": []string{"application/pkix-cert"}, }, }, crtb2) crt, _ = x509.ParseCertificate(crtb) cl := &acmeapi.Client{ HTTPClient: &http.Client{ Transport: &mt, }, } _, cURL, err := CertificateToEndpointURL(cl, crt, context.TODO()) if err != nil { t.Fatalf("%v", err) } if cURL != "https://unknown-boulder4.test/acme/cert/0000000000000000000000000000deadb33f" { t.Fatalf("curl %v", cURL) } mt.Clear() mt.Add("unknown-boulder.test/acme/cert/0000000000000000000000000000deadb33f", &http.Response{ StatusCode: 200, Header: http.Header{ "Content-Type": []string{"application/pkix-cert"}, }, }, crtb2) _, cURL, err = CertificateToEndpointURL(cl, crt, context.TODO()) if err != nil { t.Fatalf("%v", err) } if cURL != "https://unknown-boulder.test/acme/cert/0000000000000000000000000000deadb33f" { t.Fatalf("curl %v", cURL) } crtb2[5] ^= 1 _, cURL, err = CertificateToEndpointURL(cl, crt, context.TODO()) if err == nil { t.Fatal() } mt.Clear() _, cURL, err = CertificateToEndpointURL(cl, crt, context.TODO()) if err == nil { t.Fatal() } } acme-0.0.62/acmeapi/acmeutils/000077500000000000000000000000001321256625400160775ustar00rootroot00000000000000acme-0.0.62/acmeapi/acmeutils/hostname.go000066400000000000000000000013501321256625400202430ustar00rootroot00000000000000package 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 } acme-0.0.62/acmeapi/acmeutils/keyauth.go000066400000000000000000000065641321256625400201130ustar00rootroot00000000000000package acmeutils import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/hex" "encoding/json" "gopkg.in/square/go-jose.v1" "math/big" "time" ) // Calculates the base64 thumbprint of a public or private key. Returns an // error if the key is of an unknown type. func Base64Thumbprint(key interface{}) (string, error) { k := jose.JsonWebKey{Key: key} thumbprint, err := k.Thumbprint(crypto.SHA256) if err != nil { return "", err } return b64enc(thumbprint), nil } // Calculates a key authorization using the given account public or private key // and the token to prefix. func KeyAuthorization(accountKey interface{}, token string) (string, error) { thumbprint, err := Base64Thumbprint(accountKey) if err != nil { return "", err } return token + "." + thumbprint, nil } // Calculates a key authorization which is then hashed and base64 encoded as is // required for the DNS challenge. func DNSKeyAuthorization(accountKey interface{}, token string) (string, error) { ka, err := KeyAuthorization(accountKey, token) if err != nil { return "", err } return b64enc(sha256Bytes([]byte(ka))), nil } // Determines the hostname which must appear in a TLS-SNI challenge // certificate. func TLSSNIHostname(accountKey interface{}, token string) (string, error) { ka, err := KeyAuthorization(accountKey, token) if err != nil { return "", err } kaHex := sha256BytesHex([]byte(ka)) return kaHex[0:32] + "." + kaHex[32:64] + ".acme.invalid", nil } // Creates a self-signed certificate and matching private key suitable for // responding to a TLS-SNI challenge. hostname should be a hostname returned by // TLSSNIHostname. func CreateTLSSNICertificate(hostname string) (certDER []byte, privateKey crypto.PrivateKey, err error) { crt := x509.Certificate{ Subject: pkix.Name{ CommonName: hostname, }, Issuer: pkix.Name{ CommonName: hostname, }, SerialNumber: big.NewInt(1), NotBefore: time.Now().Add(-24 * time.Hour), NotAfter: time.Now().Add(365 * 24 * time.Hour), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, DNSNames: []string{hostname}, } pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return } certDER, err = x509.CreateCertificate(rand.Reader, &crt, &crt, &pk.PublicKey, pk) privateKey = pk return } // Returns JSON suitable as a generic challenge initiation response to the ACME // server. You pass this to RespondToChallenge as a json.RawMessage. Suitable // for most, but not all, challenge types. func ChallengeResponseJSON(accountKey interface{}, token, challengeType string) ([]byte, error) { ka, err := KeyAuthorization(accountKey, token) if err != nil { return nil, err } info := map[string]interface{}{ "resource": "challenge", "type": challengeType, "keyAuthorization": ka, } bb, err := json.Marshal(&info) if err != nil { return nil, err } return bb, nil } func sha256Bytes(b []byte) []byte { h := sha256.New() h.Write(b) return h.Sum(nil) } func sha256BytesHex(b []byte) string { return hex.EncodeToString(sha256Bytes(b)) } func b64enc(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) } acme-0.0.62/acmeapi/acmeutils/keyauth_test.go000066400000000000000000000017371321256625400211470ustar00rootroot00000000000000package acmeutils import "testing" func TestKeyAuthorization(t *testing.T) { pk, err := LoadPrivateKey([]byte(testKey)) if err != nil { t.Fatal() } ka, err := KeyAuthorization(pk, "foo") if err != nil { t.Fatal() } if ka != "foo.UOn6kBbQDrwoTc2BcjGS1_JeF5rDIVYrZmBhs5bgXWo" { t.Fatal() } pk, err = LoadPrivateKey([]byte(testECKey)) if err != nil { t.Fatal() } ka, err = KeyAuthorization(pk, "foo") if err != nil { t.Fatal() } if ka != "foo.S8MUz-12EEFgpVWWfDpvolnpTkuD9yVV6qHdzFuJyj8" { t.Fatalf("%v", ka) } ka, err = DNSKeyAuthorization(pk, "foo") if err != nil { t.Fatal() } if ka != "efdLQjp7LK3TpMZ4b5UsX-vVaexjtxTNfn1M3Shfqjo" { t.Fatalf("#v", ka) } hostname, err := TLSSNIHostname(pk, "foo") if err != nil { t.Fatal() } if hostname != "79f74b423a7b2cadd3a4c6786f952c5f.ebd569ec63b714cd7e7d4cdd285faa3a.acme.invalid" { t.Fatalf("%#v", hostname) } _, _, err = CreateTLSSNICertificate(hostname) if err != nil { t.Fatal() } } acme-0.0.62/acmeapi/acmeutils/load.go000066400000000000000000000057371321256625400173610ustar00rootroot00000000000000// Package acmeutils provides miscellaneous ACME-related utility functions. package acmeutils import ( "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "io" "strings" ) // Load one or more certificates from a sequence of PEM-encoded certificates. func LoadCertificates(pemBlock []byte) ([][]byte, error) { var derBlock *pem.Block var certs [][]byte for { derBlock, pemBlock = pem.Decode(pemBlock) if derBlock == nil { break } if derBlock.Type != "CERTIFICATE" { return nil, fmt.Errorf("is not a certificate") } certs = append(certs, derBlock.Bytes) } if len(certs) == 0 { return nil, fmt.Errorf("no certificates found") } return certs, nil } // Writes one or more DER-formatted certificates in PEM format. func SaveCertificates(w io.Writer, certificates ...[]byte) error { for _, c := range certificates { err := pem.Encode(w, &pem.Block{ Type: "CERTIFICATE", Bytes: c, }) if err != nil { return err } } return nil } // Load a PEM private key from a stream. func LoadPrivateKey(keyPEMBlock []byte) (crypto.PrivateKey, error) { var keyDERBlock *pem.Block for { keyDERBlock, keyPEMBlock = pem.Decode(keyPEMBlock) if keyDERBlock == nil { return nil, fmt.Errorf("failed to parse key PEM data") } if keyDERBlock.Type == "PRIVATE KEY" || strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") { break } } pk, err := LoadPrivateKeyDER(keyDERBlock.Bytes) if err != nil { return nil, err } return pk, nil } // Parse a DER private key. The key can be RSA or ECDSA. PKCS8 containers are // supported. func LoadPrivateKeyDER(der []byte) (crypto.PrivateKey, error) { pk, err := x509.ParsePKCS1PrivateKey(der) if err == nil { return pk, nil } pk2, err := x509.ParsePKCS8PrivateKey(der) if err == nil { switch pk2 := pk2.(type) { case *rsa.PrivateKey, *ecdsa.PrivateKey: return pk2, nil default: return nil, fmt.Errorf("unknown private key type") } } epk, err := x509.ParseECPrivateKey(der) if err == nil { return epk, nil } return nil, fmt.Errorf("failed to parse private key") } // Write a private key in PEM form. func SavePrivateKey(w io.Writer, pk crypto.PrivateKey) error { var kb []byte var hdr string var err error switch v := pk.(type) { case *rsa.PrivateKey: kb = x509.MarshalPKCS1PrivateKey(v) hdr = "RSA PRIVATE KEY" case *ecdsa.PrivateKey: kb, err = x509.MarshalECPrivateKey(v) hdr = "EC PRIVATE KEY" default: return fmt.Errorf("unsupported private key type: %T", pk) } if err != nil { return err } err = pem.Encode(w, &pem.Block{ Type: hdr, Bytes: kb, }) if err != nil { return err } return nil } // Load a PEM CSR from a stream and return it in DER form. func LoadCSR(pemBlock []byte) ([]byte, error) { var derBlock *pem.Block for { derBlock, pemBlock = pem.Decode(pemBlock) if derBlock == nil { return nil, fmt.Errorf("failed to parse CSR PEM data") } if derBlock.Type == "CERTIFICATE REQUEST" { break } } return derBlock.Bytes, nil } acme-0.0.62/acmeapi/acmeutils/load_test.go000066400000000000000000000762601321256625400204170ustar00rootroot00000000000000package acmeutils import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" "math/big" "reflect" "testing" ) const testCerts = `-----BEGIN CERTIFICATE----- MIIEmzCCA4OgAwIBAgITAP8qTldZTvtEDieNbQA7/6O6mDANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDExRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNTExMjcxMTE1 MDBaFw0xNjAyMjUxMTE1MDBaMCkxJzAlBgNVBAMTHmRvbTEuYWNtZXRvb2wtdGVz dC5kZXZldmVyLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMKQ an2mcZAIWqu7J5/6n+KQ521+VlsJEe3189hLJfzTbxpDxag/DiJo2xcl4jZeEAkr FR4JZxs693lh92YnbSfWwZ0IvjEUNOYJGs9L59wADTu7wEoM2xAWWSUh85ziKwpb P/JE7i1D4+Mlll/xEcfbzpbYdlGKND76wAGyapJ4ozdXr2Bjtp4rEiMGjyOfgbur ex20Fyv1A40UwdaKdypnmtEyXIdP++/dj4yWGX3rszyO1erGzsCIng9EDhPreL3c 7uZVnZJrrlUPS7mPJzPfjUZhCWK9BQniAoSHiZiB0lAB2TUNU4gaqGgj4TDv1KPa pqP1spdcIWGqA+40j70CAwEAAaOCAcQwggHAMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E FgQUO1JtuzcEAU6kv0VQlEcL59SvNOQwHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/ NBmzLjbqQYkwagYIKwYBBQUHAQEEXjBcMCYGCCsGAQUFBzABhhpodHRwOi8vMTI3 LjAuMC4xOjQwMDIvb2NzcDAyBggrBgEFBQcwAoYmaHR0cDovLzEyNy4wLjAuMTo0 MDAwL2FjbWUvaXNzdWVyLWNlcnQwSQYDVR0RBEIwQIIeZG9tMS5hY21ldG9vbC10 ZXN0LmRldmV2ZXIubmV0gh5kb20yLmFjbWV0b29sLXRlc3QuZGV2ZXZlci5uZXQw JwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL2V4YW1wbGUuY29tL2NybDBhBgNVHSAE WjBYMAgGBmeBDAECATBMBgMqAwQwRTAiBggrBgEFBQcCARYWaHR0cDovL2V4YW1w bGUuY29tL2NwczAfBggrBgEFBQcCAjATDBFEbyBXaGF0IFRob3UgV2lsdDANBgkq hkiG9w0BAQsFAAOCAQEAhpchBW1k++LrbyaCG0Y8dpJY01TDhKxNoMrEGTd7UH0F 7Ar+kBPOcf0gglvX6gcZzcJkILQTbBct5Lvqta+j/JMkseAoAFr31GWP7SdeOsmi txzWmbL+Mm256jqXPYewUiK1k9HpmPT9CajS6T/f2Q0RvRQgRD4e2B61kTRMt3t4 p7u0/wF5PDzsj8oC0D/yUSBU7icWHPkzuKhw+zjYZPoVdyEe0CoLMtTskPZooZby M+ngFyg3Boy1R4px/mmdV1fds5nHdZ+R6g2qe7FT7LB8KUVsUMEyCUiDIzeu1WLn vWa/xVfwBbKvgMjnHs31qODbsSzRRpNgX0NWE14jew== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEijCCA3KgAwIBAgICEk0wDQYJKoZIhvcNAQELBQAwKzEpMCcGA1UEAwwgY2Fj a2xpbmcgY3J5cHRvZ3JhcGhlciBmYWtlIFJPT1QwHhcNMTUxMDIxMjAxMTUyWhcN MjAxMDE5MjAxMTUyWjAfMR0wGwYDVQQDExRoYXBweSBoYWNrZXIgZmFrZSBDQTCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMIKR3maBcUSsncXYzQT13D5 Nr+Z3mLxMMh3TUdt6sACmqbJ0btRlgXfMtNLM2OU1I6a3Ju+tIZSdn2v21JBwvxU zpZQ4zy2cimIiMQDZCQHJwzC9GZn8HaW091iz9H0Go3A7WDXwYNmsdLNRi00o14U joaVqaPsYrZWvRKaIRqaU0hHmS0AWwQSvN/93iMIXuyiwywmkwKbWnnxCQ/gsctK FUtcNrwEx9Wgj6KlhwDTyI1QWSBbxVYNyUgPFzKxrSmwMO0yNff7ho+QT9x5+Y/7 XE59S4Mc4ZXxcXKew/gSlN9U5mvT+D2BhDtkCupdfsZNCQWp27A+b/DmrFI9NqsC AwEAAaOCAcIwggG+MBIGA1UdEwEB/wQIMAYBAf8CAQAwQwYDVR0eBDwwOqE4MAaC BC5taWwwCocIAAAAAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAyBggrBgEFBQcw AYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5jb20wOwYIKwYB BQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMvZHN0cm9vdGNh eDMucDdjMB8GA1UdIwQYMBaAFOmkP+6epeby1dd5YDyTpi4kjpeqMFQGA1UdIARN MEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUHAgEWImh0dHA6 Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUwMzAxoC+gLYYr aHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JMLmNybDAdBgNV HQ4EFgQU+3hPEvlgFYMsnxd/NBmzLjbqQYkwDQYJKoZIhvcNAQELBQADggEBAA0Y AeLXOklx4hhCikUUl+BdnFfn1g0W5AiQLVNIOL6PnqXu0wjnhNyhqdwnfhYMnoy4 idRh4lB6pz8Gf9pnlLd/DnWSV3gS+/I/mAl1dCkKby6H2V790e6IHmIK2KYm3jm+ U++FIdGpBdsQTSdmiX/rAyuxMDM0adMkNBwTfQmZQCz6nGHw1QcSPZMvZpsC8Skv ekzxsjF1otOrMUPNPQvtTWrVx8GlR2qfx/4xbQa1v2frNvFBCmO59goz+jnWvfTt j2NjwDZ7vlMBsPm16dbKYC840uvRoZjxqsdc3ChCZjqimFqlNG/xoPA8+dTicZzC XE9ijPIcvW6y1aa3bGw= -----END CERTIFICATE-----` var testCertsDER = [][]byte{ { 0x30, 0x82, 0x04, 0x9b, 0x30, 0x82, 0x03, 0x83, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x13, 0x00, 0xff, 0x2a, 0x4e, 0x57, 0x59, 0x4e, 0xfb, 0x44, 0x0e, 0x27, 0x8d, 0x6d, 0x00, 0x3b, 0xff, 0xa3, 0xba, 0x98, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x1f, 0x31, 0x1d, 0x30, 0x1b, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x14, 0x68, 0x61, 0x70, 0x70, 0x79, 0x20, 0x68, 0x61, 0x63, 0x6b, 0x65, 0x72, 0x20, 0x66, 0x61, 0x6b, 0x65, 0x20, 0x43, 0x41, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x35, 0x31, 0x31, 0x32, 0x37, 0x31, 0x31, 0x31, 0x35, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x31, 0x36, 0x30, 0x32, 0x32, 0x35, 0x31, 0x31, 0x31, 0x35, 0x30, 0x30, 0x5a, 0x30, 0x29, 0x31, 0x27, 0x30, 0x25, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x1e, 0x64, 0x6f, 0x6d, 0x31, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x74, 0x6f, 0x6f, 0x6c, 0x2d, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x64, 0x65, 0x76, 0x65, 0x76, 0x65, 0x72, 0x2e, 0x6e, 0x65, 0x74, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xc2, 0x90, 0x6a, 0x7d, 0xa6, 0x71, 0x90, 0x08, 0x5a, 0xab, 0xbb, 0x27, 0x9f, 0xfa, 0x9f, 0xe2, 0x90, 0xe7, 0x6d, 0x7e, 0x56, 0x5b, 0x09, 0x11, 0xed, 0xf5, 0xf3, 0xd8, 0x4b, 0x25, 0xfc, 0xd3, 0x6f, 0x1a, 0x43, 0xc5, 0xa8, 0x3f, 0x0e, 0x22, 0x68, 0xdb, 0x17, 0x25, 0xe2, 0x36, 0x5e, 0x10, 0x09, 0x2b, 0x15, 0x1e, 0x09, 0x67, 0x1b, 0x3a, 0xf7, 0x79, 0x61, 0xf7, 0x66, 0x27, 0x6d, 0x27, 0xd6, 0xc1, 0x9d, 0x08, 0xbe, 0x31, 0x14, 0x34, 0xe6, 0x09, 0x1a, 0xcf, 0x4b, 0xe7, 0xdc, 0x00, 0x0d, 0x3b, 0xbb, 0xc0, 0x4a, 0x0c, 0xdb, 0x10, 0x16, 0x59, 0x25, 0x21, 0xf3, 0x9c, 0xe2, 0x2b, 0x0a, 0x5b, 0x3f, 0xf2, 0x44, 0xee, 0x2d, 0x43, 0xe3, 0xe3, 0x25, 0x96, 0x5f, 0xf1, 0x11, 0xc7, 0xdb, 0xce, 0x96, 0xd8, 0x76, 0x51, 0x8a, 0x34, 0x3e, 0xfa, 0xc0, 0x01, 0xb2, 0x6a, 0x92, 0x78, 0xa3, 0x37, 0x57, 0xaf, 0x60, 0x63, 0xb6, 0x9e, 0x2b, 0x12, 0x23, 0x06, 0x8f, 0x23, 0x9f, 0x81, 0xbb, 0xab, 0x7b, 0x1d, 0xb4, 0x17, 0x2b, 0xf5, 0x03, 0x8d, 0x14, 0xc1, 0xd6, 0x8a, 0x77, 0x2a, 0x67, 0x9a, 0xd1, 0x32, 0x5c, 0x87, 0x4f, 0xfb, 0xef, 0xdd, 0x8f, 0x8c, 0x96, 0x19, 0x7d, 0xeb, 0xb3, 0x3c, 0x8e, 0xd5, 0xea, 0xc6, 0xce, 0xc0, 0x88, 0x9e, 0x0f, 0x44, 0x0e, 0x13, 0xeb, 0x78, 0xbd, 0xdc, 0xee, 0xe6, 0x55, 0x9d, 0x92, 0x6b, 0xae, 0x55, 0x0f, 0x4b, 0xb9, 0x8f, 0x27, 0x33, 0xdf, 0x8d, 0x46, 0x61, 0x09, 0x62, 0xbd, 0x05, 0x09, 0xe2, 0x02, 0x84, 0x87, 0x89, 0x98, 0x81, 0xd2, 0x50, 0x01, 0xd9, 0x35, 0x0d, 0x53, 0x88, 0x1a, 0xa8, 0x68, 0x23, 0xe1, 0x30, 0xef, 0xd4, 0xa3, 0xda, 0xa6, 0xa3, 0xf5, 0xb2, 0x97, 0x5c, 0x21, 0x61, 0xaa, 0x03, 0xee, 0x34, 0x8f, 0xbd, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x82, 0x01, 0xc4, 0x30, 0x82, 0x01, 0xc0, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x05, 0xa0, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x25, 0x04, 0x16, 0x30, 0x14, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02, 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, 0x3b, 0x52, 0x6d, 0xbb, 0x37, 0x04, 0x01, 0x4e, 0xa4, 0xbf, 0x45, 0x50, 0x94, 0x47, 0x0b, 0xe7, 0xd4, 0xaf, 0x34, 0xe4, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0xfb, 0x78, 0x4f, 0x12, 0xf9, 0x60, 0x15, 0x83, 0x2c, 0x9f, 0x17, 0x7f, 0x34, 0x19, 0xb3, 0x2e, 0x36, 0xea, 0x41, 0x89, 0x30, 0x6a, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01, 0x04, 0x5e, 0x30, 0x5c, 0x30, 0x26, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x86, 0x1a, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x3a, 0x34, 0x30, 0x30, 0x32, 0x2f, 0x6f, 0x63, 0x73, 0x70, 0x30, 0x32, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02, 0x86, 0x26, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x3a, 0x34, 0x30, 0x30, 0x30, 0x2f, 0x61, 0x63, 0x6d, 0x65, 0x2f, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x2d, 0x63, 0x65, 0x72, 0x74, 0x30, 0x49, 0x06, 0x03, 0x55, 0x1d, 0x11, 0x04, 0x42, 0x30, 0x40, 0x82, 0x1e, 0x64, 0x6f, 0x6d, 0x31, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x74, 0x6f, 0x6f, 0x6c, 0x2d, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x64, 0x65, 0x76, 0x65, 0x76, 0x65, 0x72, 0x2e, 0x6e, 0x65, 0x74, 0x82, 0x1e, 0x64, 0x6f, 0x6d, 0x32, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x74, 0x6f, 0x6f, 0x6c, 0x2d, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x64, 0x65, 0x76, 0x65, 0x76, 0x65, 0x72, 0x2e, 0x6e, 0x65, 0x74, 0x30, 0x27, 0x06, 0x03, 0x55, 0x1d, 0x1f, 0x04, 0x20, 0x30, 0x1e, 0x30, 0x1c, 0xa0, 0x1a, 0xa0, 0x18, 0x86, 0x16, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6c, 0x30, 0x61, 0x06, 0x03, 0x55, 0x1d, 0x20, 0x04, 0x5a, 0x30, 0x58, 0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0c, 0x01, 0x02, 0x01, 0x30, 0x4c, 0x06, 0x03, 0x2a, 0x03, 0x04, 0x30, 0x45, 0x30, 0x22, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x16, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x70, 0x73, 0x30, 0x1f, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x13, 0x0c, 0x11, 0x44, 0x6f, 0x20, 0x57, 0x68, 0x61, 0x74, 0x20, 0x54, 0x68, 0x6f, 0x75, 0x20, 0x57, 0x69, 0x6c, 0x74, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x86, 0x97, 0x21, 0x05, 0x6d, 0x64, 0xfb, 0xe2, 0xeb, 0x6f, 0x26, 0x82, 0x1b, 0x46, 0x3c, 0x76, 0x92, 0x58, 0xd3, 0x54, 0xc3, 0x84, 0xac, 0x4d, 0xa0, 0xca, 0xc4, 0x19, 0x37, 0x7b, 0x50, 0x7d, 0x05, 0xec, 0x0a, 0xfe, 0x90, 0x13, 0xce, 0x71, 0xfd, 0x20, 0x82, 0x5b, 0xd7, 0xea, 0x07, 0x19, 0xcd, 0xc2, 0x64, 0x20, 0xb4, 0x13, 0x6c, 0x17, 0x2d, 0xe4, 0xbb, 0xea, 0xb5, 0xaf, 0xa3, 0xfc, 0x93, 0x24, 0xb1, 0xe0, 0x28, 0x00, 0x5a, 0xf7, 0xd4, 0x65, 0x8f, 0xed, 0x27, 0x5e, 0x3a, 0xc9, 0xa2, 0xb7, 0x1c, 0xd6, 0x99, 0xb2, 0xfe, 0x32, 0x6d, 0xb9, 0xea, 0x3a, 0x97, 0x3d, 0x87, 0xb0, 0x52, 0x22, 0xb5, 0x93, 0xd1, 0xe9, 0x98, 0xf4, 0xfd, 0x09, 0xa8, 0xd2, 0xe9, 0x3f, 0xdf, 0xd9, 0x0d, 0x11, 0xbd, 0x14, 0x20, 0x44, 0x3e, 0x1e, 0xd8, 0x1e, 0xb5, 0x91, 0x34, 0x4c, 0xb7, 0x7b, 0x78, 0xa7, 0xbb, 0xb4, 0xff, 0x01, 0x79, 0x3c, 0x3c, 0xec, 0x8f, 0xca, 0x02, 0xd0, 0x3f, 0xf2, 0x51, 0x20, 0x54, 0xee, 0x27, 0x16, 0x1c, 0xf9, 0x33, 0xb8, 0xa8, 0x70, 0xfb, 0x38, 0xd8, 0x64, 0xfa, 0x15, 0x77, 0x21, 0x1e, 0xd0, 0x2a, 0x0b, 0x32, 0xd4, 0xec, 0x90, 0xf6, 0x68, 0xa1, 0x96, 0xf2, 0x33, 0xe9, 0xe0, 0x17, 0x28, 0x37, 0x06, 0x8c, 0xb5, 0x47, 0x8a, 0x71, 0xfe, 0x69, 0x9d, 0x57, 0x57, 0xdd, 0xb3, 0x99, 0xc7, 0x75, 0x9f, 0x91, 0xea, 0x0d, 0xaa, 0x7b, 0xb1, 0x53, 0xec, 0xb0, 0x7c, 0x29, 0x45, 0x6c, 0x50, 0xc1, 0x32, 0x09, 0x48, 0x83, 0x23, 0x37, 0xae, 0xd5, 0x62, 0xe7, 0xbd, 0x66, 0xbf, 0xc5, 0x57, 0xf0, 0x05, 0xb2, 0xaf, 0x80, 0xc8, 0xe7, 0x1e, 0xcd, 0xf5, 0xa8, 0xe0, 0xdb, 0xb1, 0x2c, 0xd1, 0x46, 0x93, 0x60, 0x5f, 0x43, 0x56, 0x13, 0x5e, 0x23, 0x7b, }, { 0x30, 0x82, 0x04, 0x8a, 0x30, 0x82, 0x03, 0x72, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x12, 0x4d, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x2b, 0x31, 0x29, 0x30, 0x27, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x20, 0x63, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x6e, 0x67, 0x20, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x70, 0x68, 0x65, 0x72, 0x20, 0x66, 0x61, 0x6b, 0x65, 0x20, 0x52, 0x4f, 0x4f, 0x54, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x35, 0x31, 0x30, 0x32, 0x31, 0x32, 0x30, 0x31, 0x31, 0x35, 0x32, 0x5a, 0x17, 0x0d, 0x32, 0x30, 0x31, 0x30, 0x31, 0x39, 0x32, 0x30, 0x31, 0x31, 0x35, 0x32, 0x5a, 0x30, 0x1f, 0x31, 0x1d, 0x30, 0x1b, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x14, 0x68, 0x61, 0x70, 0x70, 0x79, 0x20, 0x68, 0x61, 0x63, 0x6b, 0x65, 0x72, 0x20, 0x66, 0x61, 0x6b, 0x65, 0x20, 0x43, 0x41, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xc2, 0x0a, 0x47, 0x79, 0x9a, 0x05, 0xc5, 0x12, 0xb2, 0x77, 0x17, 0x63, 0x34, 0x13, 0xd7, 0x70, 0xf9, 0x36, 0xbf, 0x99, 0xde, 0x62, 0xf1, 0x30, 0xc8, 0x77, 0x4d, 0x47, 0x6d, 0xea, 0xc0, 0x02, 0x9a, 0xa6, 0xc9, 0xd1, 0xbb, 0x51, 0x96, 0x05, 0xdf, 0x32, 0xd3, 0x4b, 0x33, 0x63, 0x94, 0xd4, 0x8e, 0x9a, 0xdc, 0x9b, 0xbe, 0xb4, 0x86, 0x52, 0x76, 0x7d, 0xaf, 0xdb, 0x52, 0x41, 0xc2, 0xfc, 0x54, 0xce, 0x96, 0x50, 0xe3, 0x3c, 0xb6, 0x72, 0x29, 0x88, 0x88, 0xc4, 0x03, 0x64, 0x24, 0x07, 0x27, 0x0c, 0xc2, 0xf4, 0x66, 0x67, 0xf0, 0x76, 0x96, 0xd3, 0xdd, 0x62, 0xcf, 0xd1, 0xf4, 0x1a, 0x8d, 0xc0, 0xed, 0x60, 0xd7, 0xc1, 0x83, 0x66, 0xb1, 0xd2, 0xcd, 0x46, 0x2d, 0x34, 0xa3, 0x5e, 0x14, 0x8e, 0x86, 0x95, 0xa9, 0xa3, 0xec, 0x62, 0xb6, 0x56, 0xbd, 0x12, 0x9a, 0x21, 0x1a, 0x9a, 0x53, 0x48, 0x47, 0x99, 0x2d, 0x00, 0x5b, 0x04, 0x12, 0xbc, 0xdf, 0xfd, 0xde, 0x23, 0x08, 0x5e, 0xec, 0xa2, 0xc3, 0x2c, 0x26, 0x93, 0x02, 0x9b, 0x5a, 0x79, 0xf1, 0x09, 0x0f, 0xe0, 0xb1, 0xcb, 0x4a, 0x15, 0x4b, 0x5c, 0x36, 0xbc, 0x04, 0xc7, 0xd5, 0xa0, 0x8f, 0xa2, 0xa5, 0x87, 0x00, 0xd3, 0xc8, 0x8d, 0x50, 0x59, 0x20, 0x5b, 0xc5, 0x56, 0x0d, 0xc9, 0x48, 0x0f, 0x17, 0x32, 0xb1, 0xad, 0x29, 0xb0, 0x30, 0xed, 0x32, 0x35, 0xf7, 0xfb, 0x86, 0x8f, 0x90, 0x4f, 0xdc, 0x79, 0xf9, 0x8f, 0xfb, 0x5c, 0x4e, 0x7d, 0x4b, 0x83, 0x1c, 0xe1, 0x95, 0xf1, 0x71, 0x72, 0x9e, 0xc3, 0xf8, 0x12, 0x94, 0xdf, 0x54, 0xe6, 0x6b, 0xd3, 0xf8, 0x3d, 0x81, 0x84, 0x3b, 0x64, 0x0a, 0xea, 0x5d, 0x7e, 0xc6, 0x4d, 0x09, 0x05, 0xa9, 0xdb, 0xb0, 0x3e, 0x6f, 0xf0, 0xe6, 0xac, 0x52, 0x3d, 0x36, 0xab, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x82, 0x01, 0xc2, 0x30, 0x82, 0x01, 0xbe, 0x30, 0x12, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x08, 0x30, 0x06, 0x01, 0x01, 0xff, 0x02, 0x01, 0x00, 0x30, 0x43, 0x06, 0x03, 0x55, 0x1d, 0x1e, 0x04, 0x3c, 0x30, 0x3a, 0xa1, 0x38, 0x30, 0x06, 0x82, 0x04, 0x2e, 0x6d, 0x69, 0x6c, 0x30, 0x0a, 0x87, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x22, 0x87, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x01, 0x86, 0x30, 0x7f, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01, 0x04, 0x73, 0x30, 0x71, 0x30, 0x32, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x86, 0x26, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x69, 0x73, 0x72, 0x67, 0x2e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x69, 0x64, 0x2e, 0x6f, 0x63, 0x73, 0x70, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0x3b, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02, 0x86, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x61, 0x70, 0x70, 0x73, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x2f, 0x64, 0x73, 0x74, 0x72, 0x6f, 0x6f, 0x74, 0x63, 0x61, 0x78, 0x33, 0x2e, 0x70, 0x37, 0x63, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0xe9, 0xa4, 0x3f, 0xee, 0x9e, 0xa5, 0xe6, 0xf2, 0xd5, 0xd7, 0x79, 0x60, 0x3c, 0x93, 0xa6, 0x2e, 0x24, 0x8e, 0x97, 0xaa, 0x30, 0x54, 0x06, 0x03, 0x55, 0x1d, 0x20, 0x04, 0x4d, 0x30, 0x4b, 0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0c, 0x01, 0x02, 0x01, 0x30, 0x3f, 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xdf, 0x13, 0x01, 0x01, 0x01, 0x30, 0x30, 0x30, 0x2e, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x63, 0x70, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x78, 0x31, 0x2e, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, 0x6f, 0x72, 0x67, 0x30, 0x3c, 0x06, 0x03, 0x55, 0x1d, 0x1f, 0x04, 0x35, 0x30, 0x33, 0x30, 0x31, 0xa0, 0x2f, 0xa0, 0x2d, 0x86, 0x2b, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x63, 0x72, 0x6c, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x44, 0x53, 0x54, 0x52, 0x4f, 0x4f, 0x54, 0x43, 0x41, 0x58, 0x33, 0x43, 0x52, 0x4c, 0x2e, 0x63, 0x72, 0x6c, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, 0xfb, 0x78, 0x4f, 0x12, 0xf9, 0x60, 0x15, 0x83, 0x2c, 0x9f, 0x17, 0x7f, 0x34, 0x19, 0xb3, 0x2e, 0x36, 0xea, 0x41, 0x89, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x0d, 0x18, 0x01, 0xe2, 0xd7, 0x3a, 0x49, 0x71, 0xe2, 0x18, 0x42, 0x8a, 0x45, 0x14, 0x97, 0xe0, 0x5d, 0x9c, 0x57, 0xe7, 0xd6, 0x0d, 0x16, 0xe4, 0x08, 0x90, 0x2d, 0x53, 0x48, 0x38, 0xbe, 0x8f, 0x9e, 0xa5, 0xee, 0xd3, 0x08, 0xe7, 0x84, 0xdc, 0xa1, 0xa9, 0xdc, 0x27, 0x7e, 0x16, 0x0c, 0x9e, 0x8c, 0xb8, 0x89, 0xd4, 0x61, 0xe2, 0x50, 0x7a, 0xa7, 0x3f, 0x06, 0x7f, 0xda, 0x67, 0x94, 0xb7, 0x7f, 0x0e, 0x75, 0x92, 0x57, 0x78, 0x12, 0xfb, 0xf2, 0x3f, 0x98, 0x09, 0x75, 0x74, 0x29, 0x0a, 0x6f, 0x2e, 0x87, 0xd9, 0x5e, 0xfd, 0xd1, 0xee, 0x88, 0x1e, 0x62, 0x0a, 0xd8, 0xa6, 0x26, 0xde, 0x39, 0xbe, 0x53, 0xef, 0x85, 0x21, 0xd1, 0xa9, 0x05, 0xdb, 0x10, 0x4d, 0x27, 0x66, 0x89, 0x7f, 0xeb, 0x03, 0x2b, 0xb1, 0x30, 0x33, 0x34, 0x69, 0xd3, 0x24, 0x34, 0x1c, 0x13, 0x7d, 0x09, 0x99, 0x40, 0x2c, 0xfa, 0x9c, 0x61, 0xf0, 0xd5, 0x07, 0x12, 0x3d, 0x93, 0x2f, 0x66, 0x9b, 0x02, 0xf1, 0x29, 0x2f, 0x7a, 0x4c, 0xf1, 0xb2, 0x31, 0x75, 0xa2, 0xd3, 0xab, 0x31, 0x43, 0xcd, 0x3d, 0x0b, 0xed, 0x4d, 0x6a, 0xd5, 0xc7, 0xc1, 0xa5, 0x47, 0x6a, 0x9f, 0xc7, 0xfe, 0x31, 0x6d, 0x06, 0xb5, 0xbf, 0x67, 0xeb, 0x36, 0xf1, 0x41, 0x0a, 0x63, 0xb9, 0xf6, 0x0a, 0x33, 0xfa, 0x39, 0xd6, 0xbd, 0xf4, 0xed, 0x8f, 0x63, 0x63, 0xc0, 0x36, 0x7b, 0xbe, 0x53, 0x01, 0xb0, 0xf9, 0xb5, 0xe9, 0xd6, 0xca, 0x60, 0x2f, 0x38, 0xd2, 0xeb, 0xd1, 0xa1, 0x98, 0xf1, 0xaa, 0xc7, 0x5c, 0xdc, 0x28, 0x42, 0x66, 0x3a, 0xa2, 0x98, 0x5a, 0xa5, 0x34, 0x6f, 0xf1, 0xa0, 0xf0, 0x3c, 0xf9, 0xd4, 0xe2, 0x71, 0x9c, 0xc2, 0x5c, 0x4f, 0x62, 0x8c, 0xf2, 0x1c, 0xbd, 0x6e, 0xb2, 0xd5, 0xa6, 0xb7, 0x6c, 0x6c, }, } func TestLoadCertificates(t *testing.T) { b, err := LoadCertificates([]byte(testCerts)) if err != nil { t.Fatalf("cannot load certificates") } if !reflect.DeepEqual(b, testCertsDER) { t.Fatalf("certificate load mismatch") } } const testKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAwpBqfaZxkAhaq7snn/qf4pDnbX5WWwkR7fXz2Esl/NNvGkPF qD8OImjbFyXiNl4QCSsVHglnGzr3eWH3ZidtJ9bBnQi+MRQ05gkaz0vn3AANO7vA SgzbEBZZJSHznOIrCls/8kTuLUPj4yWWX/ERx9vOlth2UYo0PvrAAbJqknijN1ev YGO2nisSIwaPI5+Bu6t7HbQXK/UDjRTB1op3Kmea0TJch0/7792PjJYZfeuzPI7V 6sbOwIieD0QOE+t4vdzu5lWdkmuuVQ9LuY8nM9+NRmEJYr0FCeIChIeJmIHSUAHZ NQ1TiBqoaCPhMO/Uo9qmo/Wyl1whYaoD7jSPvQIDAQABAoIBAH69WlE9Ui6T8pR5 lsdUmEAbSlX9/TzR/Lb5B332/ejixjXivefqI0fw6/75M5Fc1q9SPDBTybFoSPru AR2vQyC3eWnU3yfTVN4gQjGU7ZVXB0fI9uqF01F4bVuN8UZZ4dWeOVqU4l3kIpe9 zWX0ADQjkECdAv38vCnmZ5rzYQ7FmPoZse8fC7TqJIqvMN2lkBQd/7ecfSGiDsxS jdV/a2gE+AuS1d06yfLiu8UuRYe7le1VEx84NHBAQv71IG1kc+9bKWqOlXPXvS7o WQi976wETxbqEbZLtQlHhX7ND/Svof+O/IwbfHcfz1qhMGZ8wP4CeuLQGXpVqSpc PWTR/bUCgYEAy6zdOmvpj/Ss6o2oHWqXek6PKWVmnE13NZ45GrWaBT8tG9znJRoG qA3Cj5BedJSzCEfrVZ4Yi29PrRl7HbHYEcIiJZ1rwdmPmlRK/VWgk8RpBAzT/ilS 90jTXBPlA/WvzMgwtO0hJEbnpvH5EB2WviyNWgvRhwdzPWqJVEy/S3MCgYEA9IxW 2VU1Rnx1mcudl0Y68pcLU25pXA7VsA5IsyCk1a5rwecgRDF3dVJ1lJGp8+Xf4/Wo eDhqk1tdTAp0Uy+GfJcTkrAJZO1jeYoJj6Hy4QT50xPNMQZHZ0uOkIbLfHHcXJHa 9qcEIzKpcPiq5cD0PYqJQO3f3gw5V/aLKCTDTA8CgYAYQmi/UqcpLF2EOocxqcaN HNpUde145Ik0a7olgkfsJBA9Z1xowObWEX2BGBMIE8YXUGmpcwE9am61EXnmmY5A 3zyt+kgheMZv/WZJMm/D6fsQLm75sPZe2d/C8eOvSw47eATFKBFwfrmM2vltf/i6 ghf8B7hXOv3w7MWasuF+HQKBgEixQsu9uWVnb6m0zfZ+qN0dqv807dqwijKYPGIK zRS7kUqFQqEityjHxy3Pkt5uMXxYtKhv1LZSzUviO6vSj76PRgEvlMtMiWpCbw8k C8d5rC1jUHZHMIhy/EDX4LrwOZnGvXjeMqunzphHQr2i+rckbCJB02704ULVhY38 R5VfAoGBAISqeTh28iF9SirK18xpsW6c6j+x6qfjs8Rr6pPAnO1vovUCH6vlxV4I RLeWOCS9ROFhbef4WqWpa82vGd0OxoUBirPRWRFyt0pgH9qAmwqD2YN8KFCyPztD xtiaTxLKSHYuEVxbJZJmDNkjDEtGk7hVzOQ0+GO+Tvknkp/kkHsn -----END RSA PRIVATE KEY-----` const testKeyPKCS8 = `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCkGp9pnGQCFqr uyef+p/ikOdtflZbCRHt9fPYSyX8028aQ8WoPw4iaNsXJeI2XhAJKxUeCWcbOvd5 YfdmJ20n1sGdCL4xFDTmCRrPS+fcAA07u8BKDNsQFlklIfOc4isKWz/yRO4tQ+Pj JZZf8RHH286W2HZRijQ++sABsmqSeKM3V69gY7aeKxIjBo8jn4G7q3sdtBcr9QON FMHWincqZ5rRMlyHT/vv3Y+Mlhl967M8jtXqxs7AiJ4PRA4T63i93O7mVZ2Sa65V D0u5jycz341GYQlivQUJ4gKEh4mYgdJQAdk1DVOIGqhoI+Ew79Sj2qaj9bKXXCFh qgPuNI+9AgMBAAECggEAfr1aUT1SLpPylHmWx1SYQBtKVf39PNH8tvkHffb96OLG NeK95+ojR/Dr/vkzkVzWr1I8MFPJsWhI+u4BHa9DILd5adTfJ9NU3iBCMZTtlVcH R8j26oXTUXhtW43xRlnh1Z45WpTiXeQil73NZfQANCOQQJ0C/fy8KeZnmvNhDsWY +hmx7x8LtOokiq8w3aWQFB3/t5x9IaIOzFKN1X9raAT4C5LV3TrJ8uK7xS5Fh7uV 7VUTHzg0cEBC/vUgbWRz71spao6Vc9e9LuhZCL3vrARPFuoRtku1CUeFfs0P9K+h /478jBt8dx/PWqEwZnzA/gJ64tAZelWpKlw9ZNH9tQKBgQDLrN06a+mP9Kzqjagd apd6To8pZWacTXc1njkatZoFPy0b3OclGgaoDcKPkF50lLMIR+tVnhiLb0+tGXsd sdgRwiIlnWvB2Y+aVEr9VaCTxGkEDNP+KVL3SNNcE+UD9a/MyDC07SEkRuem8fkQ HZa+LI1aC9GHB3M9aolUTL9LcwKBgQD0jFbZVTVGfHWZy52XRjrylwtTbmlcDtWw DkizIKTVrmvB5yBEMXd1UnWUkanz5d/j9ah4OGqTW11MCnRTL4Z8lxOSsAlk7WN5 igmPofLhBPnTE80xBkdnS46Qhst8cdxckdr2pwQjMqlw+KrlwPQ9iolA7d/eDDlX 9osoJMNMDwKBgBhCaL9SpyksXYQ6hzGpxo0c2lR17XjkiTRruiWCR+wkED1nXGjA 5tYRfYEYEwgTxhdQaalzAT1qbrUReeaZjkDfPK36SCF4xm/9Zkkyb8Pp+xAubvmw 9l7Z38Lx469LDjt4BMUoEXB+uYza+W1/+LqCF/wHuFc6/fDsxZqy4X4dAoGASLFC y725ZWdvqbTN9n6o3R2q/zTt2rCKMpg8YgrNFLuRSoVCoSK3KMfHLc+S3m4xfFi0 qG/UtlLNS+I7q9KPvo9GAS+Uy0yJakJvDyQLx3msLWNQdkcwiHL8QNfguvA5mca9 eN4yq6fOmEdCvaL6tyRsIkHTbvThQtWFjfxHlV8CgYEAhKp5OHbyIX1KKsrXzGmx bpzqP7Hqp+OzxGvqk8Cc7W+i9QIfq+XFXghEt5Y4JL1E4WFt5/hapalrza8Z3Q7G hQGKs9FZEXK3SmAf2oCbCoPZg3woULI/O0PG2JpPEspIdi4RXFslkmYM2SMMS0aT uFXM5DT4Y75O+SeSn+SQeyc= -----END PRIVATE KEY-----` var testKeyN, _ = big.NewInt(0).SetString("00c2906a7da67190085aabbb279ffa9fe290e76d7e565b0911edf5f3d84b25fcd36f1a43c5a83f0e2268db1725e2365e10092b151e09671b3af77961f766276d27d6c19d08be311434e6091acf4be7dc000d3bbbc04a0cdb1016592521f39ce22b0a5b3ff244ee2d43e3e325965ff111c7dbce96d876518a343efac001b26a9278a33757af6063b69e2b1223068f239f81bbab7b1db4172bf5038d14c1d68a772a679ad1325c874ffbefdd8f8c96197debb33c8ed5eac6cec0889e0f440e13eb78bddceee6559d926bae550f4bb98f2733df8d46610962bd0509e2028487899881d25001d9350d53881aa86823e130efd4a3daa6a3f5b2975c2161aa03ee348fbd", 16) var testKeyD, _ = big.NewInt(0).SetString("7ebd5a513d522e93f2947996c75498401b4a55fdfd3cd1fcb6f9077df6fde8e2c635e2bde7ea2347f0ebfef933915cd6af523c3053c9b16848faee011daf4320b77969d4df27d354de20423194ed95570747c8f6ea85d351786d5b8df14659e1d59e395a94e25de42297bdcd65f400342390409d02fdfcbc29e6679af3610ec598fa19b1ef1f0bb4ea248aaf30dda590141dffb79c7d21a20ecc528dd57f6b6804f80b92d5dd3ac9f2e2bbc52e4587bb95ed55131f3834704042fef5206d6473ef5b296a8e9573d7bd2ee85908bdefac044f16ea11b64bb50947857ecd0ff4afa1ff8efc8c1b7c771fcf5aa130667cc0fe027ae2d0197a55a92a5c3d64d1fdb5", 16) var testKeyPrime1, _ = big.NewInt(0).SetString("00cbacdd3a6be98ff4acea8da81d6a977a4e8f2965669c4d77359e391ab59a053f2d1bdce7251a06a80dc28f905e7494b30847eb559e188b6f4fad197b1db1d811c222259d6bc1d98f9a544afd55a093c469040cd3fe2952f748d35c13e503f5afccc830b4ed212446e7a6f1f9101d96be2c8d5a0bd18707733d6a89544cbf4b73", 16) var testKeyPrime2, _ = big.NewInt(0). SetString("00f48c56d95535467c7599cb9d97463af2970b536e695c0ed5b00e48b320a4d5ae6bc1e7204431777552759491a9f3e5dfe3f5a878386a935b5d4c0a74532f867c971392b00964ed63798a098fa1f2e104f9d313cd310647674b8e9086cb7c71dc5c91daf6a7042332a970f8aae5c0f43d8a8940eddfde0c3957f68b2824c34c0f", 16) var testKeyDp, _ = big.NewInt(0).SetString("184268bf52a7292c5d843a8731a9c68d1cda5475ed78e489346bba258247ec24103d675c68c0e6d6117d8118130813c6175069a973013d6a6eb51179e6998e40df3cadfa482178c66ffd6649326fc3e9fb102e6ef9b0f65ed9dfc2f1e3af4b0e3b7804c52811707eb98cdaf96d7ff8ba8217fc07b8573afdf0ecc59ab2e17e1d", 16) var testKeyDq, _ = big.NewInt(0).SetString("48b142cbbdb965676fa9b4cdf67ea8dd1daaff34eddab08a32983c620acd14bb914a8542a122b728c7c72dcf92de6e317c58b4a86fd4b652cd4be23babd28fbe8f46012f94cb4c896a426f0f240bc779ac2d63507647308872fc40d7e0baf03999c6bd78de32aba7ce984742bda2fab7246c2241d36ef4e142d5858dfc47955f", 16) var testKeyQinv, _ = big.NewInt(0).SetString("0084aa793876f2217d4a2acad7cc69b16e9cea3fb1eaa7e3b3c46bea93c09ced6fa2f5021fabe5c55e0844b7963824bd44e1616de7f85aa5a96bcdaf19dd0ec685018ab3d1591172b74a601fda809b0a83d9837c2850b23f3b43c6d89a4f12ca48762e115c5b2592660cd9230c4b4693b855cce434f863be4ef927929fe4907b27", 16) var testKeyValue = &rsa.PrivateKey{ PublicKey: rsa.PublicKey{ N: testKeyN, E: 0x10001, }, D: testKeyD, Primes: []*big.Int{ testKeyPrime1, testKeyPrime2, }, Precomputed: rsa.PrecomputedValues{ Dp: testKeyDp, Dq: testKeyDq, Qinv: testKeyQinv, CRTValues: []rsa.CRTValue{}, }, } const testECKey = `-----BEGIN EC PRIVATE KEY----- MHcCAQEEIABVf3JmY6Fs0MpQwXlUCw0T27BO+hjjr93v/nLT9PrZoAoGCCqGSM49 AwEHoUQDQgAEkDk1q0XGYa6flpAaBVHEI5UStWvdewAHpIbN+PpOlKNgeK1Xu0nw Pv7jwLK+95tYBYbVu1gUnQ+OjWpxyuITaA== -----END EC PRIVATE KEY-----` const testECKeyPKCS8 = `-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgAFV/cmZjoWzQylDB eVQLDRPbsE76GOOv3e/+ctP0+tmhRANCAASQOTWrRcZhrp+WkBoFUcQjlRK1a917 AAekhs34+k6Uo2B4rVe7SfA+/uPAsr73m1gFhtW7WBSdD46NanHK4hNo -----END PRIVATE KEY-----` var testECKeyX, _ = big.NewInt(0).SetString("903935ab45c661ae9f96901a0551c4239512b56bdd7b0007a486cdf8fa4e94a3", 16) var testECKeyY, _ = big.NewInt(0).SetString("6078ad57bb49f03efee3c0b2bef79b580586d5bb58149d0f8e8d6a71cae21368", 16) var testECKeyD, _ = big.NewInt(0).SetString("557f726663a16cd0ca50c179540b0d13dbb04efa18e3afddeffe72d3f4fad9", 16) var testECKeyValue = &ecdsa.PrivateKey{ PublicKey: ecdsa.PublicKey{ Curve: elliptic.P256(), X: testECKeyX, Y: testECKeyY, }, D: testECKeyD, } func TestLoadKey(t *testing.T) { pk, err := LoadPrivateKey([]byte(testKey)) if err != nil { t.Fatalf("failed to load private key: %v", err) } if !reflect.DeepEqual(testKeyValue, pk) { t.Fatalf("key mismatch: %#v %#v", testKeyValue, pk) } pk2, err := LoadPrivateKey([]byte(testKeyPKCS8)) if err != nil { t.Fatalf("failed to load private key: %v", err) } if !reflect.DeepEqual(testKeyValue, pk2) { t.Fatalf("key mismatch: %#v %#v", testKeyValue, pk2) } epk, err := LoadPrivateKey([]byte(testECKey)) if err != nil { t.Fatalf("failed to load EC private key: %v", err) } if !reflect.DeepEqual(epk, testECKeyValue) { t.Fatalf("EC key mismatch: %#v %#v", epk, testECKeyValue) } epk2, err := LoadPrivateKey([]byte(testECKeyPKCS8)) if err != nil { t.Fatalf("failed to load EC private key: %v", err) } if !reflect.DeepEqual(epk2, testECKeyValue) { t.Fatalf("EC key mismatch: %#v %#v", epk2, testECKeyValue) } b := bytes.Buffer{} err = SavePrivateKey(&b, pk) if err != nil { t.Fatalf("%v", err) } pkc, err := LoadPrivateKey(b.Bytes()) if err != nil { t.Fatalf("%v", err) } if !reflect.DeepEqual(pk, pkc) { t.Fatalf("mismatch after save-load") } b = bytes.Buffer{} err = SavePrivateKey(&b, epk) if err != nil { t.Fatalf("%v", err) } epkc, err := LoadPrivateKey(b.Bytes()) if err != nil { t.Fatalf("%v", err) } if !reflect.DeepEqual(epk, epkc) { t.Fatalf("mismatch after save-load") } } const testCSR = `-----BEGIN CERTIFICATE REQUEST----- MIICWzCCAUMCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQDCkGp9pnGQCFqruyef+p/ikOdtflZbCRHt9fPY SyX8028aQ8WoPw4iaNsXJeI2XhAJKxUeCWcbOvd5YfdmJ20n1sGdCL4xFDTmCRrP S+fcAA07u8BKDNsQFlklIfOc4isKWz/yRO4tQ+PjJZZf8RHH286W2HZRijQ++sAB smqSeKM3V69gY7aeKxIjBo8jn4G7q3sdtBcr9QONFMHWincqZ5rRMlyHT/vv3Y+M lhl967M8jtXqxs7AiJ4PRA4T63i93O7mVZ2Sa65VD0u5jycz341GYQlivQUJ4gKE h4mYgdJQAdk1DVOIGqhoI+Ew79Sj2qaj9bKXXCFhqgPuNI+9AgMBAAGgADANBgkq hkiG9w0BAQsFAAOCAQEAGwrSUMmte+rXVUVsYS7cqN0xJFOc18vuMDUezAsAw8Ye UDhivw2wsPTlPBl3zpVavlGSM5ZPWgv4osZtDeS/VeL+ow/7wVShlc2rbyuXTwoq Lh4+Oe4svEEfGwvia5Ui4XA2eFFLSaTVM+FWGZNUXoB++bS831ro2fffoI3jrjDz 0edY1zhsjaV9Fej9k37O/1GhkRfTq0WMAnX/VP6L/2Gjs2ZwvAwCItUpCwPw1B3Q h+/TTT56DupPyRFI1ZmbpX6Rp/A0gL+ykqpjASJj1ai+jkBXiDz/I4LJ2oUhtOxJ vGQkX0wJF929X+IQlURO6rP5ET9tzmF4S9/F/RA1kw== -----END CERTIFICATE REQUEST-----` var testCSRDER = []byte{ 0x30, 0x82, 0x02, 0x5b, 0x30, 0x82, 0x01, 0x43, 0x02, 0x01, 0x00, 0x30, 0x16, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xc2, 0x90, 0x6a, 0x7d, 0xa6, 0x71, 0x90, 0x08, 0x5a, 0xab, 0xbb, 0x27, 0x9f, 0xfa, 0x9f, 0xe2, 0x90, 0xe7, 0x6d, 0x7e, 0x56, 0x5b, 0x09, 0x11, 0xed, 0xf5, 0xf3, 0xd8, 0x4b, 0x25, 0xfc, 0xd3, 0x6f, 0x1a, 0x43, 0xc5, 0xa8, 0x3f, 0x0e, 0x22, 0x68, 0xdb, 0x17, 0x25, 0xe2, 0x36, 0x5e, 0x10, 0x09, 0x2b, 0x15, 0x1e, 0x09, 0x67, 0x1b, 0x3a, 0xf7, 0x79, 0x61, 0xf7, 0x66, 0x27, 0x6d, 0x27, 0xd6, 0xc1, 0x9d, 0x08, 0xbe, 0x31, 0x14, 0x34, 0xe6, 0x09, 0x1a, 0xcf, 0x4b, 0xe7, 0xdc, 0x00, 0x0d, 0x3b, 0xbb, 0xc0, 0x4a, 0x0c, 0xdb, 0x10, 0x16, 0x59, 0x25, 0x21, 0xf3, 0x9c, 0xe2, 0x2b, 0x0a, 0x5b, 0x3f, 0xf2, 0x44, 0xee, 0x2d, 0x43, 0xe3, 0xe3, 0x25, 0x96, 0x5f, 0xf1, 0x11, 0xc7, 0xdb, 0xce, 0x96, 0xd8, 0x76, 0x51, 0x8a, 0x34, 0x3e, 0xfa, 0xc0, 0x01, 0xb2, 0x6a, 0x92, 0x78, 0xa3, 0x37, 0x57, 0xaf, 0x60, 0x63, 0xb6, 0x9e, 0x2b, 0x12, 0x23, 0x06, 0x8f, 0x23, 0x9f, 0x81, 0xbb, 0xab, 0x7b, 0x1d, 0xb4, 0x17, 0x2b, 0xf5, 0x03, 0x8d, 0x14, 0xc1, 0xd6, 0x8a, 0x77, 0x2a, 0x67, 0x9a, 0xd1, 0x32, 0x5c, 0x87, 0x4f, 0xfb, 0xef, 0xdd, 0x8f, 0x8c, 0x96, 0x19, 0x7d, 0xeb, 0xb3, 0x3c, 0x8e, 0xd5, 0xea, 0xc6, 0xce, 0xc0, 0x88, 0x9e, 0x0f, 0x44, 0x0e, 0x13, 0xeb, 0x78, 0xbd, 0xdc, 0xee, 0xe6, 0x55, 0x9d, 0x92, 0x6b, 0xae, 0x55, 0x0f, 0x4b, 0xb9, 0x8f, 0x27, 0x33, 0xdf, 0x8d, 0x46, 0x61, 0x09, 0x62, 0xbd, 0x05, 0x09, 0xe2, 0x02, 0x84, 0x87, 0x89, 0x98, 0x81, 0xd2, 0x50, 0x01, 0xd9, 0x35, 0x0d, 0x53, 0x88, 0x1a, 0xa8, 0x68, 0x23, 0xe1, 0x30, 0xef, 0xd4, 0xa3, 0xda, 0xa6, 0xa3, 0xf5, 0xb2, 0x97, 0x5c, 0x21, 0x61, 0xaa, 0x03, 0xee, 0x34, 0x8f, 0xbd, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa0, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x1b, 0x0a, 0xd2, 0x50, 0xc9, 0xad, 0x7b, 0xea, 0xd7, 0x55, 0x45, 0x6c, 0x61, 0x2e, 0xdc, 0xa8, 0xdd, 0x31, 0x24, 0x53, 0x9c, 0xd7, 0xcb, 0xee, 0x30, 0x35, 0x1e, 0xcc, 0x0b, 0x00, 0xc3, 0xc6, 0x1e, 0x50, 0x38, 0x62, 0xbf, 0x0d, 0xb0, 0xb0, 0xf4, 0xe5, 0x3c, 0x19, 0x77, 0xce, 0x95, 0x5a, 0xbe, 0x51, 0x92, 0x33, 0x96, 0x4f, 0x5a, 0x0b, 0xf8, 0xa2, 0xc6, 0x6d, 0x0d, 0xe4, 0xbf, 0x55, 0xe2, 0xfe, 0xa3, 0x0f, 0xfb, 0xc1, 0x54, 0xa1, 0x95, 0xcd, 0xab, 0x6f, 0x2b, 0x97, 0x4f, 0x0a, 0x2a, 0x2e, 0x1e, 0x3e, 0x39, 0xee, 0x2c, 0xbc, 0x41, 0x1f, 0x1b, 0x0b, 0xe2, 0x6b, 0x95, 0x22, 0xe1, 0x70, 0x36, 0x78, 0x51, 0x4b, 0x49, 0xa4, 0xd5, 0x33, 0xe1, 0x56, 0x19, 0x93, 0x54, 0x5e, 0x80, 0x7e, 0xf9, 0xb4, 0xbc, 0xdf, 0x5a, 0xe8, 0xd9, 0xf7, 0xdf, 0xa0, 0x8d, 0xe3, 0xae, 0x30, 0xf3, 0xd1, 0xe7, 0x58, 0xd7, 0x38, 0x6c, 0x8d, 0xa5, 0x7d, 0x15, 0xe8, 0xfd, 0x93, 0x7e, 0xce, 0xff, 0x51, 0xa1, 0x91, 0x17, 0xd3, 0xab, 0x45, 0x8c, 0x02, 0x75, 0xff, 0x54, 0xfe, 0x8b, 0xff, 0x61, 0xa3, 0xb3, 0x66, 0x70, 0xbc, 0x0c, 0x02, 0x22, 0xd5, 0x29, 0x0b, 0x03, 0xf0, 0xd4, 0x1d, 0xd0, 0x87, 0xef, 0xd3, 0x4d, 0x3e, 0x7a, 0x0e, 0xea, 0x4f, 0xc9, 0x11, 0x48, 0xd5, 0x99, 0x9b, 0xa5, 0x7e, 0x91, 0xa7, 0xf0, 0x34, 0x80, 0xbf, 0xb2, 0x92, 0xaa, 0x63, 0x01, 0x22, 0x63, 0xd5, 0xa8, 0xbe, 0x8e, 0x40, 0x57, 0x88, 0x3c, 0xff, 0x23, 0x82, 0xc9, 0xda, 0x85, 0x21, 0xb4, 0xec, 0x49, 0xbc, 0x64, 0x24, 0x5f, 0x4c, 0x09, 0x17, 0xdd, 0xbd, 0x5f, 0xe2, 0x10, 0x95, 0x44, 0x4e, 0xea, 0xb3, 0xf9, 0x11, 0x3f, 0x6d, 0xce, 0x61, 0x78, 0x4b, 0xdf, 0xc5, 0xfd, 0x10, 0x35, 0x93, } func TestLoadCSR(t *testing.T) { b, err := LoadCSR([]byte(testCSR)) if err != nil { t.Fatalf("load csr") } if !reflect.DeepEqual(b, testCSRDER) { t.Fatalf("bad csr") } } acme-0.0.62/acmeapi/api.go000066400000000000000000000412541321256625400152170ustar00rootroot00000000000000// Package acmeapi provides an API for accessing ACME servers. // // Some methods provided correspond exactly to ACME calls, such as // NewAuthorization, RespondToChallenge, RequestCertificate or Revoke. Others, // such as UpsertRegistration, LoadCertificate or WaitForCertificate, // automatically compose requests to provide a simplified interface. // // For example, LoadCertificate obtains the issuing certificate chain as well. // WaitForCertificate polls until a certificate is available. // UpsertRegistration determines automatically whether an account key is // already registered and registers it if it is not. // // All methods take Contexts so as to support cancellation and timeouts. // // If you have an URI for an authorization, challenge or certificate, you // can load it by constructing such an object and setting the URI field, // then calling the appropriate Load function. (The unexported fields in these // structures are used to track Retry-After times for the WaitLoad* functions and // are not a barrier to you constructing these objects.) // // The following additional packages are likely to be of interest: // // https://godoc.org/github.com/hlandau/acme/acmeapi/acmeendpoints Known providers // https://godoc.org/github.com/hlandau/acme/acmeapi/acmeutils Certificate loading utilities // package acmeapi import ( "crypto" "crypto/ecdsa" "crypto/rsa" "gopkg.in/square/go-jose.v1" denet "github.com/hlandau/goutils/net" "github.com/peterhellberg/link" "golang.org/x/net/context" "golang.org/x/net/context/ctxhttp" "io" "io/ioutil" "net/http" "net/url" "encoding/json" "fmt" "github.com/hlandau/xlog" "runtime" "strings" "sync" "time" ) // Log site. var log, Log = xlog.NewQuiet("acme.api") type directoryInfo struct { NewReg string `json:"new-reg"` RecoverReg string `json:"recover-reg"` NewAuthz string `json:"new-authz"` NewCert string `json:"new-cert"` RevokeCert string `json:"revoke-cert"` } type revokeReq struct { Resource string `json:"resource"` // "revoke-cert" Certificate denet.Base64up `json:"certificate"` } // Returns true if the URL given is (potentially) a valid ACME resource URL. // // The URL must be an HTTPS URL. func ValidURL(u string) bool { ur, err := url.Parse(u) return err == nil && (ur.Scheme == "https" || (TestingAllowHTTP && ur.Scheme == "http")) } // Internal use only. All ACME URLs must use "https" and not "http". However, // for testing purposes, if this is set, "http" URLs will be allowed. This is // useful for testing when a test ACME server doesn't have SSL configured. var TestingAllowHTTP = false // Client for making ACME API calls. // // You must set at least AccountKey and DirectoryURL. type Client struct { // Account private key. Required. AccountKey crypto.PrivateKey // The ACME server directory URL. Required. (However, you can omit this if // you only use the client to load existing resources at known URLs.) DirectoryURL string // Uses http.DefaultClient if nil. HTTPClient *http.Client dir *directoryInfo nonceSource nonceSource nonceReentrant int initOnce sync.Once } // You should set this to a string identifying the code invoking this library. // Optional. var UserAgent string func (c *Client) doReq(method, url string, v, r interface{}, ctx context.Context) (*http.Response, error) { return c.doReqEx(method, url, nil, v, r, ctx) } func algorithmFromKey(key crypto.PrivateKey) (jose.SignatureAlgorithm, error) { switch v := key.(type) { case *rsa.PrivateKey: return jose.RS256, nil case *ecdsa.PrivateKey: name := v.Curve.Params().Name switch name { case "P-256": return jose.ES256, nil case "P-384": return jose.ES384, nil case "P-521": return jose.ES512, nil default: return "", fmt.Errorf("unsupported ECDSA curve: %s", name) } default: return "", fmt.Errorf("unsupported private key type: %T", key) } } 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) } if key == nil { key = c.AccountKey } c.nonceSource.GetNonceFunc = c.obtainNewNonce var rdr io.Reader if v != nil { b, err := json.Marshal(v) if err != nil { return nil, err } if key == nil { return nil, fmt.Errorf("account key must be specified") } kalg, err := algorithmFromKey(key) if err != nil { return nil, err } signer, err := jose.NewSigner(kalg, key) if err != nil { return nil, err } signer.SetNonceSource(c.nonceSource.WithContext(ctx)) sig, err := signer.Sign(b) if err != nil { return nil, err } s := sig.FullSerialize() if err != nil { return nil, err } rdr = strings.NewReader(s) } req, err := http.NewRequest(method, url, rdr) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") if method == "POST" { req.Header.Set("Content-Type", "application/json") } log.Debugf("request: %s", url) res, err := c.doReqActual(req, ctx) log.Debugf("response: %v %v", res, err) if err != nil { return nil, err } if n := res.Header.Get("Replay-Nonce"); n != "" { c.nonceSource.AddNonce(n) } if res.StatusCode >= 400 && res.StatusCode < 600 { defer res.Body.Close() return res, newHTTPError(res) } if r != nil { defer res.Body.Close() if ct := res.Header.Get("Content-Type"); ct != "application/json" { return res, fmt.Errorf("unexpected content type: %#v", ct) } err = json.NewDecoder(res.Body).Decode(r) if err != nil { return nil, err } } return res, nil } func (c *Client) doReqActual(req *http.Request, ctx context.Context) (*http.Response, error) { req.Header.Set("User-Agent", userAgent(UserAgent)) return ctxhttp.Do(ctx, c.HTTPClient, req) } func (c *Client) forceGetDirectory(ctx context.Context) (*directoryInfo, error) { if c.DirectoryURL == "" { return nil, fmt.Errorf("must specify a directory URL") } _, err := c.doReq("GET", c.DirectoryURL, nil, &c.dir, ctx) if err != nil { return nil, err } if !ValidURL(c.dir.NewReg) || !ValidURL(c.dir.NewAuthz) || !ValidURL(c.dir.NewCert) { c.dir = nil return nil, fmt.Errorf("directory does not provide required endpoints") } 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} var updateRegCodes = []int{200, 202} func isStatusCode(res *http.Response, codes []int) bool { for _, c := range codes { if c == res.StatusCode { return true } } return false } // Loads an existing registration. If reg.URI is set, then that registration is // updated, and the operation fails if the registration does not exist. // Otherwise, the registration is created if it does not exist or updated if it // does and the URI is returned. // // Note that this operation requires an account key, since the registration is // private data requiring authentication to access. func (c *Client) UpsertRegistration(reg *Registration, ctx context.Context) error { di, err := c.getDirectory(ctx) if err != nil { return err } // Determine whether we need to get the registration URI. endp := reg.URI resource := "reg" expectCode := updateRegCodes if endp == "" { endp = di.NewReg resource = "new-reg" expectCode = newRegCodes } // Make request. reg.Resource = resource res, err := c.doReq("POST", endp, reg, reg, ctx) if res == nil { return err } // Get TOS URI. lg := link.ParseResponse(res) if tosLink, ok := lg["terms-of-service"]; ok { reg.LatestAgreementURI = tosLink.URI } // Ensure status code is an expected value. if !isStatusCode(res, expectCode) { if err != nil { return err } return fmt.Errorf("unexpected status code: %d: %v", res.StatusCode, endp) } // Process registration URI. loc := res.Header.Get("Location") switch { case resource == "reg": // Updating existing registration, so we already have the URL and // shouldn't be redirected anywhere. if loc != "" { return fmt.Errorf("unexpected Location header: %q", loc) } case !ValidURL(loc): return fmt.Errorf("invalid URL: %q", loc) default: // Save the registration URL. reg.URI = loc } // If conflict occurred, need to issue the request again to update fields. if res.StatusCode == 409 { return c.UpsertRegistration(reg, ctx) } return nil } // This is a higher-level account registration method built on // UpsertRegistration. If a new agreement is required and its URI // is set in agreementURIs, it will be agreed to automatically. Otherwise // AgreementError will be returned. func (c *Client) AgreeRegistration(reg *Registration, agreementURIs map[string]struct{}, ctx context.Context) error { err := c.UpsertRegistration(reg, ctx) if err != nil { return err } if reg.LatestAgreementURI != reg.AgreementURI { _, ok := agreementURIs[reg.LatestAgreementURI] if !ok { return &AgreementError{reg.LatestAgreementURI} } reg.AgreementURI = reg.LatestAgreementURI err = c.UpsertRegistration(reg, ctx) if err != nil { return err } } return nil } // Load or reload the details of an authorization via the URI. // // You can load an authorization from only the URI by creating an Authorization // with the URI set and then calling this. func (c *Client) LoadAuthorization(az *Authorization, ctx context.Context) error { az.Combinations = nil res, err := c.doReq("GET", az.URI, nil, az, ctx) if err != nil { return err } err = az.validate() if err != nil { return err } az.retryAt = retryAtDefault(res.Header, 10*time.Second) return nil } // Like LoadAuthorization, but waits the retry time if this is not the first // attempt to load this authoization. To be used when polling. func (c *Client) WaitLoadAuthorization(az *Authorization, ctx context.Context) error { err := waitUntil(az.retryAt, ctx) if err != nil { return err } return c.LoadAuthorization(az, ctx) } func (az *Authorization) validate() error { if len(az.Challenges) == 0 { return fmt.Errorf("no challenges offered") } if az.Combinations == nil { var is []int for i := 0; i < len(az.Challenges); i++ { is = append(is, i) } az.Combinations = append(az.Combinations, is) } for _, c := range az.Combinations { for _, i := range c { if i >= len(az.Challenges) { return fmt.Errorf("one or more combinations are malformed") } } } return nil } // Load or reload the details of a challenge via the URI. // // You can load a challenge from only the URI by creating a Challenge with the // URI set and then calling this. func (c *Client) LoadChallenge(ch *Challenge, ctx context.Context) error { res, err := c.doReq("GET", ch.URI, nil, ch, ctx) if err != nil { return err } ch.retryAt = retryAtDefault(res.Header, 10*time.Second) return nil } // Like LoadChallenge, but waits the retry time if this is not the first // attempt to load this challenge. To be used when polling. func (c *Client) WaitLoadChallenge(ch *Challenge, ctx context.Context) error { err := waitUntil(ch.retryAt, ctx) if err != nil { return err } return c.LoadChallenge(ch, ctx) } // Create a new authorization for the given hostname. func (c *Client) NewAuthorization(hostname string, ctx context.Context) (*Authorization, error) { di, err := c.getDirectory(ctx) if err != nil { return nil, err } az := &Authorization{ Resource: "new-authz", Identifier: Identifier{ Type: "dns", Value: hostname, }, } res, err := c.doReq("POST", di.NewAuthz, az, az, ctx) if err != nil { return nil, err } loc := res.Header.Get("Location") if res.StatusCode != 201 || !ValidURL(loc) { return nil, fmt.Errorf("expected status code 201 and valid Location header: %#v", res) } az.URI = loc err = az.validate() if err != nil { return nil, err } return az, nil } // Submit a challenge response. Only the challenge URI is required. // // The response message is signed with the given key. // // If responseKey is nil, the account key is used. func (c *Client) RespondToChallenge(ch *Challenge, response json.RawMessage, responseKey crypto.PrivateKey, ctx context.Context) error { _, err := c.doReqEx("POST", ch.URI, responseKey, &response, c, ctx) if err != nil { return err } return nil } // Request a certificate using a CSR in DER form. func (c *Client) RequestCertificate(csrDER []byte, ctx context.Context) (*Certificate, error) { di, err := c.getDirectory(ctx) if err != nil { return nil, err } crt := &Certificate{ Resource: "new-cert", CSR: csrDER, } res, err := c.doReq("POST", di.NewCert, crt, nil, ctx) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != 201 { return nil, fmt.Errorf("unexpected status code: %v", res.StatusCode) } loc := res.Header.Get("Location") if !ValidURL(loc) { return nil, fmt.Errorf("invalid URI: %#v", loc) } crt.URI = loc err = c.loadCertificate(crt, res, ctx) if err != nil { return nil, err } return crt, nil } // Load or reload a certificate. // // You can load a certificate from its URI by creating a Certificate with the // URI set and then calling this. // // Returns nil if the certificate is not yet ready, but the Certificate field // will remain nil. func (c *Client) LoadCertificate(crt *Certificate, ctx context.Context) error { res, err := c.doReq("GET", crt.URI, nil, nil, ctx) if err != nil { return err } return c.loadCertificate(crt, res, ctx) } func (c *Client) loadCertificate(crt *Certificate, res *http.Response, ctx context.Context) error { defer res.Body.Close() ct := res.Header.Get("Content-Type") if ct == "application/pkix-cert" { der, err := ioutil.ReadAll(denet.LimitReader(res.Body, 1*1024*1024)) if err != nil { return err } crt.Certificate = der err = c.loadExtraCertificates(crt, res, ctx) if err != nil { return err } } else if res.StatusCode == 200 { return fmt.Errorf("Certificate returned with unexpected type: %v", ct) } crt.retryAt = retryAtDefault(res.Header, 10*time.Second) return nil } func (c *Client) loadExtraCertificates(crt *Certificate, res *http.Response, ctx context.Context) error { crt.ExtraCertificates = nil for { var err error lg := link.ParseResponse(res) up, ok := lg["up"] if !ok { return nil } crtURI, _ := url.Parse(crt.URI) upURI, _ := url.Parse(up.URI) if crtURI == nil || upURI == nil { return fmt.Errorf("invalid URI") } upURI = crtURI.ResolveReference(upURI) res, err = c.doReq("GET", upURI.String(), nil, nil, ctx) if err != nil { return err } defer res.Body.Close() ct := res.Header.Get("Content-Type") if ct != "application/pkix-cert" { return fmt.Errorf("unexpected certificate type: %v", ct) } der, err := ioutil.ReadAll(denet.LimitReader(res.Body, 1*1024*1024)) if err != nil { return err } res.Body.Close() crt.ExtraCertificates = append(crt.ExtraCertificates, der) } } // Like LoadCertificate, but waits the retry time if this is not the first // attempt to load this certificate. To be used when polling. // // You will almost certainly want WaitForCertificate instead of this. func (c *Client) WaitLoadCertificate(crt *Certificate, ctx context.Context) error { err := waitUntil(crt.retryAt, ctx) if err != nil { return err } return c.LoadCertificate(crt, ctx) } // Wait for a pending certificate to be issued. If the certificate has already // been issued, this is a no-op. Only the URI is required. May be cancelled // using the context. func (c *Client) WaitForCertificate(crt *Certificate, ctx context.Context) error { for { if len(crt.Certificate) > 0 { return nil } err := c.WaitLoadCertificate(crt, ctx) if err != nil { return err } } } // Revoke the given certificate. // // The revocation key may be the key corresponding to the certificate. If it is // nil, the account key is used; in this case, the account must be authorized // for all identifiers in the certificate. func (c *Client) Revoke(certificateDER []byte, revocationKey crypto.PrivateKey, ctx context.Context) error { di, err := c.getDirectory(ctx) if err != nil { return err } if di.RevokeCert == "" { return fmt.Errorf("endpoint does not support revocation") } req := &revokeReq{ Resource: "revoke-cert", Certificate: certificateDER, } res, err := c.doReqEx("POST", di.RevokeCert, revocationKey, req, nil, ctx) if err != nil { return err } defer res.Body.Close() return nil } func userAgent(ua string) string { if ua != "" { ua += " " } return fmt.Sprintf("%sacmeapi Go-http-client/1.1 %s/%s", ua, runtime.GOOS, runtime.GOARCH) } acme-0.0.62/acmeapi/api_ig_test.go000066400000000000000000000026611321256625400167340ustar00rootroot00000000000000// +build integration package acmeapi import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "golang.org/x/net/context" "testing" ) func testAPIWithKey(t *testing.T, pk crypto.PrivateKey) { cl := Client{ DirectoryURL: "http://127.0.0.1:4000/directory", } cl.AccountKey = pk agreementURIs := map[string]struct{}{ "http://boulder:4000/terms/v1": {}, } reg := &Registration{ ContactURIs: []string{ "mailto:nobody@localhost", }, } err := cl.AgreeRegistration(reg, agreementURIs, context.TODO()) if err != nil { t.Fatalf("couldn't upsert registration: %v", err) } auth, err := cl.NewAuthorization("dom1.acmetool-test.devever.net", context.TODO()) if err != nil { t.Fatalf("couldn't create authorization: %v", err) } err = cl.WaitLoadAuthorization(auth, context.TODO()) if err != nil { t.Fatalf("couldn't load authorization") } err = cl.WaitLoadChallenge(auth.Challenges[0], context.TODO()) if err != nil { t.Fatalf("couldn't load challenge") } // TODO //cl.RespondToChallenge //cl.RequestCertificate } func TestAPIIntegration(t *testing.T) { TestingAllowHTTP = true rsaPK, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("couldn't generate RSA key: %v", err) } testAPIWithKey(t, rsaPK) ecdsaPK, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { t.Fatalf("couldn't generate ECDSA key: %v", err) } testAPIWithKey(t, ecdsaPK) } acme-0.0.62/acmeapi/api_test.go000066400000000000000000000206361321256625400162570ustar00rootroot00000000000000package acmeapi import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "github.com/hlandau/goutils/test" "github.com/hlandau/xlog" "gopkg.in/square/go-jose.v1" "golang.org/x/net/context" "io/ioutil" "net/http" "reflect" "testing" "time" ) func TestAPI(t *testing.T) { Log.SetSeverity(xlog.SevDebug) mt := test.HTTPMockTransport{} cl := &Client{ HTTPClient: &http.Client{ Transport: &mt, }, } 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{ StatusCode: 200, Header: http.Header{ "Content-Type": []string{"application/pkix-cert"}, "Link": []string{"; rel=\"up\""}, }, }, []byte("cert-data")) mt.Add("boulder.test/acme/issuer-cert", &http.Response{ StatusCode: 200, Header: http.Header{ "Content-Type": []string{"application/pkix-cert"}, "Link": []string{"; rel=\"up\""}, }, }, []byte("issuer-cert-data")) mt.Add("boulder.test/acme/root-cert", &http.Response{ StatusCode: 200, Header: http.Header{ "Content-Type": []string{"application/pkix-cert"}, //"Replay-Nonce": []string{"some-nonce-root"}, }, }, []byte("root-cert-data")) crt := &Certificate{ URI: "https://boulder.test/acme/cert/some-certificate", } correctCrt := *crt err := cl.WaitForCertificate(crt, context.TODO()) if err != nil { t.Fatalf("%v", err) } someCrt := *crt correctCrt.Certificate = []byte("cert-data") correctCrt.ExtraCertificates = [][]byte{ []byte("issuer-cert-data"), []byte("root-cert-data"), } crt.retryAt = time.Time{} if !reflect.DeepEqual(&correctCrt, crt) { t.Fatalf("%v != %v", &correctCrt, crt) } // Load Authorization mt.Add("boulder.test/acme/authz/some-authz", &http.Response{ StatusCode: 200, Header: http.Header{ "Content-Type": []string{"application/json"}, }, }, []byte(`{"challenges":[ { "type": "http-01", "uri": "https://boulder.test/acme/challenge/some-challenge" } ], "identifier": { "type": "dns", "value": "example.com" }, "status": "pending", "expires": "2015-01-01T18:26:57Z" }`)) az := &Authorization{ URI: "https://boulder.test/acme/authz/some-authz", } correctAZ := *az correctAZ.Combinations = [][]int{[]int{0}} correctAZ.Identifier.Type = "dns" correctAZ.Identifier.Value = "example.com" correctAZ.Status = "pending" correctAZ.Expires = time.Date(2015, 1, 1, 18, 26, 57, 0, time.UTC) correctAZ.Challenges = []*Challenge{ { Type: "http-01", URI: "https://boulder.test/acme/challenge/some-challenge", }, } err = cl.WaitLoadAuthorization(az, context.TODO()) if err != nil { t.Fatalf("%v", err) } az.retryAt = time.Time{} if !reflect.DeepEqual(&correctAZ, az) { t.Fatal("%v != %v", &correctAZ, az) } // Load Challenge mt.Add("boulder.test/acme/challenge/some-challenge", &http.Response{ StatusCode: 200, Header: http.Header{ "Content-Type": []string{"application/json"}, }, }, []byte(`{ "type": "http-01" }`)) ch := &Challenge{ URI: "https://boulder.test/acme/challenge/some-challenge", } err = cl.WaitLoadChallenge(ch, context.TODO()) if err != nil { t.Fatalf("%v", err) } // Request Certificate 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") rw.WriteHeader(201) }) epk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) cl.AccountKey = epk cl.DirectoryURL = "https://boulder.test/directory" crt, err = cl.RequestCertificate([]byte("csr"), context.TODO()) if err != nil { t.Fatalf("%v", err) } err = cl.LoadCertificate(crt, context.TODO()) if err != nil { t.Fatalf("%v", err) } crt.CSR = nil crt.Resource = "" crt.retryAt = someCrt.retryAt if !reflect.DeepEqual(&someCrt, crt) { t.Fatalf("mismatch %#v\n\n%#v", &someCrt, crt) } t.Logf("%v", crt) // Upsert Registration 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", issueNonce()) rw.WriteHeader(409) }) mt.AddHandlerFunc("boulder.test/acme/reg/1", func(rw http.ResponseWriter, req *http.Request) { if req.Method != "POST" { t.Fatal() } if !checkNonce(rw, req) { return } rw.Header().Set("Replay-Nonce", issueNonce()) rw.Header().Set("Content-Type", "application/json") rw.Header().Set("Link", "; rel=\"terms-of-service\"") rw.WriteHeader(200) rw.Write([]byte(`{}`)) }) reg := &Registration{} err = cl.AgreeRegistration(reg, nil, context.TODO()) ae, ok := err.(*AgreementError) if !ok || ae.URI != "urn:some:boulder:terms/of/service" { t.Fatalf("expected agreement error") } agreementURIs := map[string]struct{}{ "urn:some:boulder:terms/of/service": struct{}{}, } err = cl.AgreeRegistration(reg, agreementURIs, context.TODO()) if err != nil { t.Fatalf("%v", err) } // 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("Content-Type", "application/json") 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": [ { "type": "http-01", "uri": "https://boulder.test/acme/challenge/some-challenge2" } ], "identifier": { "type": "dns", "value": "example.com" }, "status": "pending", "expires": "2015-01-01T18:26:57Z" }`)) }) mt.AddHandlerFunc("boulder.test/acme/challenge/some-challenge2", func(rw http.ResponseWriter, req *http.Request) { 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) } err = cl.RespondToChallenge(az.Challenges[0], json.RawMessage(`{}`), nil, context.TODO()) if err != nil { t.Fatalf("%v", err) } mt.AddHandlerFunc("boulder.test/acme/revoke-cert", func(rw http.ResponseWriter, req *http.Request) { if req.Method != "POST" { t.Fatal() } if !checkNonce(rw, req) { return } rw.Header().Set("Replay-Nonce", issueNonce()) rw.Header().Set("Content-Type", "application/json") rw.WriteHeader(200) rw.Write([]byte(`{}`)) }) err = cl.Revoke([]byte("revoke-der"), nil, context.TODO()) if err != nil { t.Fatalf("%v", err) } } acme-0.0.62/acmeapi/nonce.go000066400000000000000000000023331321256625400155430ustar00rootroot00000000000000package acmeapi import ( "errors" "golang.org/x/net/context" ) type nonceSource struct { pool map[string]struct{} GetNonceFunc func(ctx context.Context) error } func (ns *nonceSource) init() { if ns.pool != nil { return } ns.pool = map[string]struct{}{} } func (ns *nonceSource) Nonce(ctx context.Context) (string, error) { ns.init() var k string for k = range ns.pool { break } if k == "" { 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(ctx context.Context) error { if ns.GetNonceFunc == nil { return errors.New("out of nonces - this should never happen") } 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) } acme-0.0.62/acmeapi/nonce_test.go000066400000000000000000000010051321256625400165750ustar00rootroot00000000000000package acmeapi import ( "golang.org/x/net/context" "testing" ) func TestNonce(t *testing.T) { ns := nonceSource{} ns.AddNonce("my-nonce") nsc := ns.WithContext(context.TODO()) n, err := nsc.Nonce() if err != nil { t.Fatal() } if n != "my-nonce" { t.Fatal() } n, err = nsc.Nonce() if err == nil { t.Fatal() } ns.GetNonceFunc = func(ctx context.Context) error { ns.AddNonce("nonce2") return nil } n, err = nsc.Nonce() if err != nil { t.Fatal() } if n != "nonce2" { t.Fatal() } } acme-0.0.62/acmeapi/ocsp.go000066400000000000000000000035661321256625400154160ustar00rootroot00000000000000package acmeapi import ( "crypto/x509" "encoding/base64" "fmt" denet "github.com/hlandau/goutils/net" "golang.org/x/crypto/ocsp" "golang.org/x/net/context" "io/ioutil" "net/http" ) // This is equivalent to calling CheckOCSPRaw, but the raw response is not // returned. Preserved for compatibility; use CheckOCSPRaw instead. func (c *Client) CheckOCSP(crt, issuer *x509.Certificate, ctx context.Context) (*ocsp.Response, error) { res, _, err := c.CheckOCSPRaw(crt, issuer, ctx) return res, err } // Checks OCSP for a certificate. The immediate issuer must be specified. If // the certificate does not support OCSP, (nil, nil) is returned. Uses HTTP // GET rather than POST. The response is verified. The caller must check the // response status. The raw OCSP response is also returned, even if parsing // failed and err is non-nil. func (c *Client) CheckOCSPRaw(crt, issuer *x509.Certificate, ctx context.Context) (parsedResponse *ocsp.Response, rawResponse []byte, err error) { if len(crt.OCSPServer) == 0 { return } b, err := ocsp.CreateRequest(crt, issuer, nil) if err != nil { return } b64 := base64.StdEncoding.EncodeToString(b) path := crt.OCSPServer[0] + "/" + b64 req, err := http.NewRequest("GET", path, nil) if err != nil { return } req.Header.Set("Accept", "application/ocsp-response") res, err := c.doReqActual(req, ctx) if err != nil { return } defer res.Body.Close() if res.StatusCode != 200 { err = fmt.Errorf("OCSP response has status %#v", res.Status) return } if res.Header.Get("Content-Type") != "application/ocsp-response" { err = fmt.Errorf("response to OCSP request had unexpected content type") return } // Read response, limiting response to 1MiB. rawResponse, err = ioutil.ReadAll(denet.LimitReader(res.Body, 1*1024*1024)) if err != nil { return } parsedResponse, err = ocsp.ParseResponse(rawResponse, issuer) return } acme-0.0.62/acmeapi/ocsp_test.go000066400000000000000000000070761321256625400164550ustar00rootroot00000000000000package acmeapi import ( "crypto/x509" "github.com/hlandau/acme/acmeapi/acmeutils" "golang.org/x/crypto/ocsp" "golang.org/x/net/context" "testing" ) const testOCSPCerts = `-----BEGIN CERTIFICATE----- MIIE6DCCA9CgAwIBAgITAPr3OLUNFF72kSERFC+leb00HDANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAxMTcxNjAx MDBaFw0xNjA0MTYxNjAxMDBaMB4xHDAaBgNVBAMTE2FxMS5saGguZGV2ZXZlci5u ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVQT8bs4n6+3QLyehW GseFUI+xMMlAM0Mrkol0rB2ZbC4rWanxfqG9TE6i/ToEe+9dL7NxpBKXrRnD/4jK cpDxHbGy+hqx/XZefmpdLK2E7FtO53sE0rDcQVGZ2r4YweumfS6jNoNeNZsMzJ6/ aAeXoz+j+rPJG73NjgWz2BIWwum7AMquq2YeERp3eu5hXQDsZxk6dlNwJ3XVaho7 EZZojQENm2/BRkpr1oLzq5fMKVc+zRGzuoCJqeYH6yYzWG7oUypW+H477pKDfKLE RGwEoTAAx4SS4HwXYrCftFgfmWw6fFV9L8aqON8ypW9CZ5HCprymypcy+6/n/S7k ruH3AgMBAAGjggIcMIICGDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFN/DSOGyPtfc 1X8rffIJtSocqMzbMB8GA1UdIwQYMBaAFPt4TxL5YBWDLJ8XfzQZsy426kGJMHgG CCsGAQUFBwEBBGwwajAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Auc3RhZ2luZy14 MS5sZXRzZW5jcnlwdC5vcmcvMDMGCCsGAQUFBzAChidodHRwOi8vY2VydC5zdGFn aW5nLXgxLmxldHNlbmNyeXB0Lm9yZy8wHgYDVR0RBBcwFYITYXExLmxoaC5kZXZl dmVyLm5ldDCB/gYDVR0gBIH2MIHzMAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEw gdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggr BgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVk IHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ug d2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0 c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUAA4IBAQAVkT8U oD2AJVjtHogCyt7BkPQ+j6zN1zaN9Bd9nI6a7tpAT6B+j6IqB4o2vCFYawiKaDwR ri06Yi9Ohf1QY50D7P21wzfsRoizHbsmHDPPnlDfFe/R1MzB7jYI1JV4LkjWLpuC OjTQZs3hIoEbTEBA/TIcwAfS9oMFgk+LgL5B4zQUZgqVp0+A4NNy3J1nBhYC2k2T 6qiE0CeU8bCfR2V2MZ6Az2X8nwWkWwovosDQR0oOWDcACDbDnS6OPMuHtZi7Wtqn UeMJ3YfZ7VBWzJTmDRPoDdbP92YI8FqRGbA6GO/XzyJvkOKSnc3CDfJ9Od0IeVeV aC0Q8qLjOhazFhj0 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i 8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8 tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj 7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8 BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7 eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW rFo4Uv1EnkKJm3vJFe50eJGhEKlx -----END CERTIFICATE-----` func TestOCSP(t *testing.T) { b, err := acmeutils.LoadCertificates([]byte(testOCSPCerts)) if err != nil { t.Fatalf("cannot load certificates") } c0, err := x509.ParseCertificate(b[0]) if err != nil { t.Fatalf("cannot parse certificate") } c1, err := x509.ParseCertificate(b[1]) if err != nil { t.Fatalf("cannot parse certificate") } cl := Client{} res, err := cl.CheckOCSP(c0, c1, context.TODO()) if err != nil { t.Fatalf("ocsp error: %v", err) } if res.Status != ocsp.Revoked { t.Fatalf("ocsp status should be revoked (1) but is %v", res.Status) } } acme-0.0.62/acmeapi/types.go000066400000000000000000000113311321256625400156030ustar00rootroot00000000000000package acmeapi import ( "encoding/json" "fmt" "net" "time" denet "github.com/hlandau/goutils/net" jose "gopkg.in/square/go-jose.v1" ) // Represents an account registration. type Registration struct { URI string `json:"-"` // The URI of the registration. Resource string `json:"resource"` // must be "new-reg" or "reg" Key *jose.JsonWebKey `json:"key,omitempty"` // Account Key ContactURIs []string `json:"contact,omitempty"` // Contact URIs AgreementURI string `json:"agreement,omitempty"` // ToS URI AuthorizationsURL string `json:"authorizations,omitempty"` CertificatesURL string `json:"certificates,omitempty"` // This is not actually part of the registration, but it // is provided when loading a registration for convenience // as it is returned in the HTTP headers. It is the URI // of the current agreement required. LatestAgreementURI string `json:"-"` } // Represents an error that may have happened. // https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 type ProblemDetails struct { Type string `json:"type,omitempty"` Detail string `json:"detail,omitempty"` HTTPStatus int `json:"status,omitempty"` } // Represents a single validation attempt. type ValidationRecord struct { Authorities []string `json:",omitempty"` URL string `json:"url,omitempty"` Hostname string `json:"hostname"` Port string `json:"port"` AddressesResolved []net.IP `json:"addressesResolved"` AddressUsed net.IP `json:"addressUsed"` AddressesTried []net.IP `json:"addressesTried"` } // Represents a Challenge which is part of an Authorization. type Challenge struct { URI string `json:"uri"` // The URI of the challenge. Resource string `json:"resource"` // "challenge" Type string `json:"type"` Status Status `json:"status,omitempty"` Validated time.Time `json:"validated,omitempty"` // RFC 3339 Token string `json:"token"` // proofOfPossession Certs []denet.Base64up `json:"certs,omitempty"` Error *ProblemDetails `json:"error,omitempty"` ProvidedKeyAuthorization string `json:"keyAuthorization,omitempty"` ValidationRecord []ValidationRecord `json:"validationRecord,omitempty"` retryAt time.Time } // Represents an authorization. You can construct an authorization from only // the URI; the authorization information will be fetched automatically. type Authorization struct { URI string `json:"-"` // The URI of the authorization. Resource string `json:"resource"` // must be "new-authz" or "authz" Identifier Identifier `json:"identifier"` Status Status `json:"status,omitempty"` Expires time.Time `json:"expires,omitempty"` // RFC 3339 (ISO 8601) Challenges []*Challenge `json:"challenges,omitempty"` Combinations [][]int `json:"combinations,omitempty"` retryAt time.Time } // Represents a certificate which has been, or is about to be, issued. type Certificate struct { URI string `json:"-"` // The URI of the certificate. Resource string `json:"resource"` // "new-cert" // The certificate data. DER. Certificate []byte `json:"-"` // Any required extra certificates, in DER form in the correct order. ExtraCertificates [][]byte `json:"-"` // DER. Consumers of this API will find that this is always nil; it is // used internally when submitting certificate requests. CSR denet.Base64up `json:"csr"` retryAt time.Time } // Represents an identifier for which an authorization is desired. type Identifier struct { Type string `json:"type"` // must be "dns" Value string `json:"value"` // dns: a hostname. } // Represents the status of an authorization or challenge. type Status string const ( StatusUnknown Status = "unknown" // Non-final state... StatusPending = "pending" // Non-final state. StatusProcessing = "processing" // Non-final state. StatusValid = "valid" // Final state. StatusInvalid = "invalid" // Final state. StatusRevoked = "revoked" // Final state. ) // Returns true iff the status is a valid status. func (s Status) Valid() bool { switch s { case "unknown", "pending", "processing", "valid", "invalid", "revoked": return true default: return false } } // Returns true iff the status is a final status. func (s Status) Final() bool { switch s { case "valid", "invalid", "revoked": return true default: return false } } // Implements encoding/json.Unmarshaler. func (s *Status) UnmarshalJSON(data []byte) error { var ss string err := json.Unmarshal(data, &ss) if err != nil { return err } if !Status(ss).Valid() { return fmt.Errorf("not a valid status: %#v", ss) } *s = Status(ss) return nil } acme-0.0.62/acmeapi/types_test.go000066400000000000000000000031271321256625400166460ustar00rootroot00000000000000package acmeapi import ( "bytes" "encoding/json" "net" "testing" ) func TestStatus(t *testing.T) { var s Status err := json.Unmarshal([]byte(`"pending"`), &s) if err != nil { t.Fatalf("%v", err) } if s != "pending" || !s.Valid() || s.Final() { t.Fatal() } err = json.Unmarshal([]byte(`"f9S0"`), &s) if err == nil { t.Fatal() } } func TestChallenge(t *testing.T) { const cJSON = `{ "type": "http-01", "status": "invalid", "error": { "type": "urn:acme:error:caa", "detail": "CAA record for mymonash2021.conference.monash.edu prevents issuance", "status": 403 }, "uri": "https://acme-v01.api.letsencrypt.org/acme/challenge/wL4hNlUUJtGoMp6QeavoaAZjbqmBgJk2FMpOSC1aoIU/2676511905", "token": "GMgoj5xYX7qSIfN9GdmyqhdAHYrCco_Md9kKrT8v0jE", "keyAuthorization": "GMgoj5xYX7qSIfN9GdmyqhdAHYrCco_Md9kKrT8v0jE.QRRvz3cNxWGJObT4gl6G9ZNx-4cXE2eK81kX5lpYzmo", "validationRecord": [ { "url": "http://mysite.foo.com/.well-known/acme-challenge/GMgoj5xYX7qSIfN9GdmyqHdAHYrCco_Md9kKrT8v0jE", "hostname": "mysite.foo.com", "port": "80", "addressesResolved": [ "54.85.70.226", "52.21.26.68", "54.210.179.160", "52.1.9.49" ], "addressUsed": "54.85.70.226", "addressesTried": [] } ] }` var c Challenge if err := json.Unmarshal([]byte(cJSON), &c); err != nil { t.Fatalf("%v", err) } if g, e := c.Error.Type, "urn:acme:error:caa"; g != e { t.Fatalf("%v != %v", g, e) } if g, e := c.ValidationRecord[0].AddressesResolved[1], net.IPv4(52, 21, 26, 68); !bytes.Equal(g, e) { t.Fatalf("%v != %v", g, e) } } acme-0.0.62/acmeapi/util-errors.go000066400000000000000000000024501321256625400167300ustar00rootroot00000000000000package acmeapi import ( "fmt" denet "github.com/hlandau/goutils/net" "io/ioutil" "net/http" ) // Error returned when the account agreement URI does not match the currently required // agreement URI. type AgreementError struct { URI string // The required agreement URI. } func (e *AgreementError) Error() string { return fmt.Sprintf("Registration requires agreement with the following agreement: %#v", e.URI) } // Error returned when an HTTP request results in a valid response, but which // has an unexpected failure status code. Used so that the response can still // be examined if desired. type HTTPError struct { // The HTTP response. Res *http.Response // If the response had an application/problem+json response body, this is // that JSON data. ProblemBody string } // Summarises the response status, headers, and the JSON problem body if // available. func (he *HTTPError) Error() string { return fmt.Sprintf("HTTP error: %v\n%v\n%v", he.Res.Status, he.Res.Header, he.ProblemBody) } func newHTTPError(res *http.Response) error { he := &HTTPError{ Res: res, } if res.Header.Get("Content-Type") == "application/problem+json" { defer res.Body.Close() b, err := ioutil.ReadAll(denet.LimitReader(res.Body, 1*1024*1024)) if err == nil { he.ProblemBody = string(b) } } return he } acme-0.0.62/acmeapi/util-retry.go000066400000000000000000000025701321256625400165640ustar00rootroot00000000000000package acmeapi import ( "github.com/hlandau/goutils/clock" "golang.org/x/net/context" "net/http" "strconv" "time" ) var defaultClock = clock.Real func parseRetryAfter(h http.Header) (t time.Time, ok bool) { v := h.Get("Retry-After") if v == "" { return time.Time{}, false } n, err := strconv.ParseUint(v, 10, 31) if err != nil { t, err = time.Parse(time.RFC1123, v) if err != nil { return time.Time{}, false } return t, true } return defaultClock.Now().Add(time.Duration(n) * time.Second), true } func retryAtDefault(h http.Header, d time.Duration) time.Time { t, ok := parseRetryAfter(h) if ok { return t } return defaultClock.Now().Add(d) } // Wait until time t. If t is before the current time, returns immediately. // Cancellable via ctx, in which case err is passed through. Otherwise returns // nil. func waitUntil(t time.Time, ctx context.Context) error { var ch <-chan time.Time ch = closedChannel now := defaultClock.Now() if t.After(now) { ch = defaultClock.After(t.Sub(now)) } // make sure ctx.Done() is checked here even when we are using closedChannel, // as select doesn't guarantee any particular priority. select { case <-ctx.Done(): return ctx.Err() default: select { case <-ctx.Done(): return ctx.Err() case <-ch: } } return nil } var closedChannel = make(chan time.Time) func init() { close(closedChannel) } acme-0.0.62/acmeapi/util-retry_test.go000066400000000000000000000043101321256625400176150ustar00rootroot00000000000000package acmeapi import ( "github.com/hlandau/goutils/clock" "golang.org/x/net/context" "net/http" "testing" "time" ) func withClock(cl clock.Clock, f func()) { origClock := defaultClock defer func() { defaultClock = origClock }() defaultClock = cl f() } var clk clock.Fake var slowClk clock.Fake func init() { refTime, _ := time.Parse(time.RFC3339, "2009-10-11T11:09:06Z") clk = clock.NewFastAt(refTime) slowClk = clock.NewSlowAt(refTime) } func TestRetryAfter(t *testing.T) { withClock(clk, func() { h := http.Header{} t1, ok := parseRetryAfter(h) if ok { t.Fatal() } h.Set("Retry-After", "Mon, 02 Jan 2006 15:04:05 UTC") t1 = retryAtDefault(h, 1*time.Second) tref, _ := time.Parse("Mon, 02 Jan 2006 15:04:05 UTC", "Mon, 02 Jan 2006 15:04:05 UTC") if t1 != tref || tref.IsZero() { t.Fatal() } t2, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") if t1 != t2 { t.Fatalf("%v %v", t1, t2) } h.Set("Retry-After", "20") t1, ok = parseRetryAfter(h) now := defaultClock.Now() if !ok { t.Fatal() } d := now.Add(20 * time.Second).Sub(t1) if d != 0 { t.Fatalf("%v", d) } h.Set("Retry-After", "Mon 02 Jan 2006 15:04:05 UTC") t1, ok = parseRetryAfter(h) if ok || !t1.IsZero() { t.Fatal() } }) } func TestRetryAfterDefault(t *testing.T) { withClock(clk, func() { h := http.Header{} t1 := retryAtDefault(h, 42*time.Second) now := defaultClock.Now() d := now.Add(42 * time.Second).Sub(t1) if d != 0 { t.Fatalf("%v", d) } }) } func TestWaitUntil(t *testing.T) { withClock(clk, func() { tgt := defaultClock.Now().Add(49828 * time.Millisecond) waitUntil(tgt, context.TODO()) if defaultClock.Now().Sub(tgt) != 0 { t.Fatalf("%v", defaultClock.Now().Sub(tgt)) } }) withClock(slowClk, func() { tgt := defaultClock.Now().Add(49828 * time.Millisecond) ctx, _ := context.WithTimeout(context.TODO(), 10*time.Millisecond) err := waitUntil(tgt, ctx) if err == nil { t.Fatal() } ctx, cancel := context.WithCancel(context.TODO()) cancel() err = waitUntil(tgt, ctx) if err == nil { t.Fatal() } slowClk.Advance(49829 * time.Millisecond) err = waitUntil(tgt, context.TODO()) if err != nil { t.Fatal() } }) } acme-0.0.62/cmd/000077500000000000000000000000001321256625400132555ustar00rootroot00000000000000acme-0.0.62/cmd/acmetool/000077500000000000000000000000001321256625400150605ustar00rootroot00000000000000acme-0.0.62/cmd/acmetool/doc.go000066400000000000000000000046161321256625400161630ustar00rootroot00000000000000package main import ( "fmt" "github.com/hlandau/acme/storage" "gopkg.in/alecthomas/kingpin.v2" ) const manPageTemplate = `{{define "FormatFlags"}}\ {{range .Flags}}\ {{if not .Hidden}}\ .TP \fB{{if .Short}}-{{.Short|Char}}, {{end}}--{{.Name}}{{if not .IsBoolFlag}}={{.FormatPlaceHolder}}{{end}}\\fR {{.Help}} {{end}}\ {{end}}\ {{end}}\ {{define "FormatCommand"}}\ {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}{{if .Default}}*{{end}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ {{end}}\ {{define "FormatCommands"}}\ {{range .FlattenedCommands}}\ {{if not .Hidden}}\ .SS \fB{{.FullCommand}}{{template "FormatCommand" .}}\\fR .PP {{.Help}} {{template "FormatFlags" .}}\ {{end}}\ {{end}}\ {{end}}\ {{define "FormatUsage"}}\ {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}}\\fR {{end}}\ .TH {{.App.Name}} 8 {{.App.Version}} "acmetool" .SH "NAME" {{.App.Name}} - request certificates from ACME servers automatically .SH "SYNOPSIS" .TP \fB{{.App.Name}}{{template "FormatUsage" .App}} .SH "DESCRIPTION" {{.App.Help}} .SH "OPTIONS" {{template "FormatFlags" .App}}\ {{if .App.Commands}}\ .SH "SUBCOMMANDS" {{template "FormatCommands" .App}}\ {{end}}\ .SH "AUTHOR" © 2015 {{.App.Author}} MIT License .SH "SEE ALSO" Documentation: Report bugs at: ` var helpText = fmt.Sprintf(`acmetool is a utility for the automated retrieval, management and renewal of certificates from ACME server such as Let's Encrypt. It emphasises automation, idempotency and the minimisation of state. You use acmetool by configuring targets (typically using the "want") command. acmetool then requests certificates as necessary to satisfy the configured targets. New certificates are requested where existing ones are soon to expire. acmetool stores its state in a state directory. It can be specified on invocation via the --state option; otherwise, the path in ACME_STATE_DIR is used, or, failing that, the path "%s" (recommended). The --xlog options control the logging. The --service options control privilege dropping and daemonization and are applicable only to the redirector subcommand. `, storage.RecommendedPath) func init() { kingpin.CommandLine.Help = helpText kingpin.CommandLine.Author("Hugo Landau") kingpin.ManPageTemplate = manPageTemplate } acme-0.0.62/cmd/acmetool/le-import.go000066400000000000000000000110111321256625400173110ustar00rootroot00000000000000package main import ( "crypto/x509" "fmt" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/acme/acmeapi/acmeendpoints" "github.com/hlandau/acme/acmeapi/acmeutils" "github.com/hlandau/acme/storage" "golang.org/x/net/context" "gopkg.in/square/go-jose.v1" "io/ioutil" "os" "path/filepath" "regexp" "strings" ) func cmdImportLE() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") lePath := *importLEArg accountNames, err := getLEAccountNames(lePath) log.Fatale(err, "cannot inspect accounts directory - do you have permissions to read the Let's Encrypt directory (i.e. are you root)?") // In order to import a Let's Encrypt state directory, we must: // - import the account keys // - import the certificate keys // - import the certificates // Import account keys. durls := map[string]struct{}{} for _, accountName := range accountNames { acct, err := importLEAccount(s, lePath, accountName) log.Fatale(err, "import account") durls[acct.DirectoryURL] = struct{}{} } keyFiles, err := filepath.Glob(filepath.Join(lePath, "keys", "*.pem")) log.Fatale(err) // Import certificate keys. for _, keyFile := range keyFiles { err := importKey(s, keyFile) log.Fatale(err, "import key") } // Import certificates. certFiles, err := filepath.Glob(filepath.Join(lePath, "archive", "*", "cert*.pem")) log.Fatale(err) for _, certFile := range certFiles { err := importCert(s, certFile) log.Fatale(err, "import certificate") } // If there is no default provider set, and we have only one directory URL // imported, set it as the default provider. if len(durls) == 1 && s.DefaultTarget().Request.Provider == "" { for p := range durls { s.DefaultTarget().Request.Provider = p err := s.SaveTarget(s.DefaultTarget()) log.Fatale(err, "couldn't set default provider") break } } } var knownProviderURLs = map[string]struct{}{} func importLEAccount(s storage.Store, lePath, accountName string) (*storage.Account, error) { providerURL, err := getProviderURLFromAccountName(accountName) if err != nil { return nil, err } knownProviderURLs[providerURL] = struct{}{} pkPath := filepath.Join(lePath, "accounts", accountName, "private_key.json") b, err := ioutil.ReadFile(pkPath) if err != nil { return nil, err } k := jose.JsonWebKey{} err = k.UnmarshalJSON(b) if err != nil { return nil, err } acct, err := s.ImportAccount(providerURL, k.Key) if err != nil { return nil, err } return acct, nil } func importKey(s storage.Store, filename string) error { b, err := ioutil.ReadFile(filename) if err != nil { return err } pk, err := acmeutils.LoadPrivateKey(b) if err != nil { return err } _, err = s.ImportKey(pk) return err } func importCert(s storage.Store, filename string) error { certURL, err := determineLECertificateURL(filename) if err != nil { return err } _, err = s.ImportCertificate(certURL) return err } // The Let's Encrypt state directory format keeps certificates but not their // URLs. Since boulder uses the serial number to form the URL, we can // reconstruct the URL. But since not even the provider association is stored, // we have to guess. func determineLECertificateURL(certFilename string) (string, error) { b, err := ioutil.ReadFile(certFilename) if err != nil { return "", err } certs, err := acmeutils.LoadCertificates(b) if err != nil { return "", err } if len(certs) == 0 { return "", fmt.Errorf("no certs") } c, err := x509.ParseCertificate(certs[0]) if err != nil { return "", err } // Don't need directory URL, direct certificate URL load only. cl := acmeapi.Client{} _, certURL, err := acmeendpoints.CertificateToEndpointURL(&cl, c, context.TODO()) if err != nil { return "", err } return certURL, nil } func getProviderURLFromAccountName(accountName string) (string, error) { idx := strings.LastIndexByte(accountName, '/') if idx < 0 || idx != len(accountName)-33 { return "", fmt.Errorf("does not appear to be an account name: %#v", accountName) } return "https://" + accountName[0:idx], nil } func getLEAccountNames(path string) (accountNames []string, err error) { err = filepath.Walk(filepath.Join(path, "accounts"), func(path string, fi os.FileInfo, err error) error { if err != nil { return err } m := re_leAccountPath.FindStringSubmatch(path) if fi.IsDir() && m != nil { accountNames = append(accountNames, m[1]) } return nil }) if err != nil { return nil, err } return accountNames, nil } var re_leAccountPath = regexp.MustCompilePOSIX(`.*/([^/]+/directory/[0-9a-f]{32})$`) acme-0.0.62/cmd/acmetool/main.go000066400000000000000000000337461321256625400163500ustar00rootroot00000000000000// acmetool, an automated certificate acquisition tool for ACME servers. package main import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "syscall" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/acme/acmeapi/acmeutils" "github.com/hlandau/acme/hooks" "github.com/hlandau/acme/interaction" "github.com/hlandau/acme/redirector" "github.com/hlandau/acme/responder" "github.com/hlandau/acme/storage" "github.com/hlandau/acme/storageops" "github.com/hlandau/dexlogconfig" "github.com/hlandau/xlog" "gopkg.in/alecthomas/kingpin.v2" "gopkg.in/hlandau/easyconfig.v1/adaptflag" "gopkg.in/hlandau/service.v2" "gopkg.in/square/go-jose.v1" "gopkg.in/yaml.v2" ) var log, Log = xlog.New("acmetool") var ( stateFlag = kingpin.Flag("state", "Path to the state directory (env: ACME_STATE_DIR)"). Default(storage.RecommendedPath). Envar("ACME_STATE_DIR"). PlaceHolder(storage.RecommendedPath). String() hooksFlag = kingpin.Flag("hooks", "Path to the notification hooks directory (env: ACME_HOOKS_DIR)"). Default(hooks.RecommendedPath). Envar("ACME_HOOKS_DIR"). PlaceHolder(hooks.RecommendedPath). String() batchFlag = kingpin.Flag("batch", "Do not attempt interaction; useful for cron jobs. (acmetool can still obtain responses from a response file, if one was provided.)"). Bool() stdioFlag = kingpin.Flag("stdio", "Don't attempt to use console dialogs; fall back to stdio prompts").Bool() responseFileFlag = kingpin.Flag("response-file", "Read dialog responses from the given file (default: $ACME_STATE_DIR/conf/responses)").ExistingFile() reconcileCmd = kingpin.Command("reconcile", reconcileHelp).Default() cullCmd = kingpin.Command("cull", "Delete expired, unused certificates") cullSimulateFlag = cullCmd.Flag("simulate", "Show which certificates would be deleted without deleting any").Short('n').Bool() statusCmd = kingpin.Command("status", "Show active configuration") wantCmd = kingpin.Command("want", "Add a target with one or more hostnames") wantReconcile = wantCmd.Flag("reconcile", "Specify --no-reconcile to skip reconcile after adding target").Default("1").Bool() wantArg = wantCmd.Arg("hostname", "hostnames for which a certificate should be obtained").Required().Strings() unwantCmd = kingpin.Command("unwant", "Modify targets to remove any mentions of the given hostnames") unwantArg = unwantCmd.Arg("hostname", "hostnames which should be removed from all target files").Required().Strings() 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() 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() redirectorStatusCodeFlag = redirectorCmd.Flag("status-code", "HTTP status code to use when redirecting (default '308')").Default("308").Int() 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() importJWKAccountCmd = kingpin.Command("import-jwk-account", "Import a JWK account key") importJWKURLArg = importJWKAccountCmd.Arg("provider-url", "Provider URL (e.g. https://acme-v01.api.letsencrypt.org/directory)").Required().String() importJWKPathArg = importJWKAccountCmd.Arg("private-key-file", "Path to private_key.json").Required().ExistingFile() importPEMAccountCmd = kingpin.Command("import-pem-account", "Import a PEM account key") importPEMURLArg = importPEMAccountCmd.Arg("provider-url", "Provider URL (e.g. https://acme-v01.api.letsencrypt.org/directory)").Required().String() importPEMPathArg = importPEMAccountCmd.Arg("private-key-file", "Path to private key PEM file").Required().ExistingFile() importKeyCmd = kingpin.Command("import-key", "Import a certificate private key") importKeyArg = importKeyCmd.Arg("private-key-file", "Path to PEM-encoded private key").Required().ExistingFile() importLECmd = kingpin.Command("import-le", "Import a Let's Encrypt client state directory") importLEArg = importLECmd.Arg("le-state-path", "Path to Let's Encrypt state directory").Default("/etc/letsencrypt").ExistingDir() // Arguments we should probably support for revocation: // A certificate ID // A key ID // A path to a PEM-encoded certificate - TODO // A path to a PEM-encoded private key (revoke all known certificates with that key) - TODO // A path to a certificate directory - TODO // A path to a key directory - TODO // A certificate URL - TODO revokeCmd = kingpin.Command("revoke", "Revoke a certificate") revokeArg = revokeCmd.Arg("certificate-id-or-path", "Certificate ID to revoke").String() accountThumbprintCmd = kingpin.Command("account-thumbprint", "Prints account thumbprints") ) const reconcileHelp = `Reconcile ACME state, idempotently requesting and renewing certificates to satisfy configured targets. This is the default command.` func main() { syscall.Umask(0) // make sure webroot files can be world-readable adaptflag.Adapt() cmd := kingpin.Parse() var err error *stateFlag, err = filepath.Abs(*stateFlag) log.Fatale(err, "state directory path") *hooksFlag, err = filepath.Abs(*hooksFlag) log.Fatale(err, "hooks directory path") hooks.DefaultPath = *hooksFlag acmeapi.UserAgent = "acmetool" dexlogconfig.Init() if *batchFlag { interaction.NonInteractive = true } if *stdioFlag { interaction.NoDialog = true } if *responseFileFlag == "" { p := filepath.Join(*stateFlag, "conf/responses") if _, err := os.Stat(p); err == nil { *responseFileFlag = p } } if *responseFileFlag != "" { err := loadResponseFile(*responseFileFlag) log.Errore(err, "cannot load response file, continuing anyway") } switch cmd { case "reconcile": cmdReconcile() case "cull": cmdCull() case "status": cmdStatus() case "account-thumbprint": cmdAccountThumbprint() case "want": cmdWant() if *wantReconcile { cmdReconcile() } case "unwant": cmdUnwant() case "quickstart": cmdQuickstart() case "redirector": cmdRunRedirector() case "test-notify": cmdRunTestNotify() case "import-key": cmdImportKey() case "import-jwk-account": cmdImportJWKAccount() case "import-pem-account": cmdImportPEMAccount() case "import-le": cmdImportLE() cmdReconcile() case "revoke": cmdRevoke() } } func cmdImportJWKAccount() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") f, err := os.Open(*importJWKPathArg) log.Fatale(err, "cannot open private key file") defer f.Close() b, err := ioutil.ReadAll(f) log.Fatale(err, "cannot read file") k := jose.JsonWebKey{} err = k.UnmarshalJSON(b) log.Fatale(err, "cannot unmarshal key") _, err = s.ImportAccount(*importJWKURLArg, k.Key) log.Fatale(err, "cannot import account key") } func cmdImportPEMAccount() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") f, err := os.Open(*importPEMPathArg) log.Fatale(err, "cannot open private key file") defer f.Close() b, err := ioutil.ReadAll(f) log.Fatale(err, "cannot read file") pk, err := acmeutils.LoadPrivateKey(b) log.Fatale(err, "cannot parse private key") _, err = s.ImportAccount(*importPEMURLArg, pk) log.Fatale(err, "cannot import account key") } func cmdImportKey() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") err = importKey(s, *importKeyArg) log.Fatale(err, "import key") } func cmdReconcile() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") err = storageops.Reconcile(s) log.Fatale(err, "reconcile") } func cmdCull() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") err = storageops.Cull(s, *cullSimulateFlag) log.Fatale(err, "cull") } func cmdStatus() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") info := StatusString(s) log.Fatale(err, "status") fmt.Print(info) } func StatusString(s storage.Store) string { var buf bytes.Buffer fmt.Fprintf(&buf, "Settings:\n") fmt.Fprintf(&buf, " ACME_STATE_DIR: %s\n", s.Path()) fmt.Fprintf(&buf, " ACME_HOOKS_DIR: %s\n", hooks.DefaultPath) fmt.Fprintf(&buf, " Default directory URL: %s\n", s.DefaultTarget().Request.Provider) fmt.Fprintf(&buf, " Preferred key type: %v\n", &s.DefaultTarget().Request.Key) fmt.Fprintf(&buf, " Additional webroots:\n") for _, wr := range s.DefaultTarget().Request.Challenge.WebrootPaths { fmt.Fprintf(&buf, " %s\n", wr) } fmt.Fprintf(&buf, "\nAvailable accounts:\n") s.VisitAccounts(func(a *storage.Account) error { fmt.Fprintf(&buf, " %v\n", a) thumbprint, _ := acmeutils.Base64Thumbprint(a.PrivateKey) fmt.Fprintf(&buf, " thumbprint: %s\n", thumbprint) return nil }) fmt.Fprintf(&buf, "\n") s.VisitTargets(func(t *storage.Target) error { fmt.Fprintf(&buf, "%v\n", t) c, err := storageops.FindBestCertificateSatisfying(s, t) if err != nil { fmt.Fprintf(&buf, " error: %v\n", err) return nil // continue } renewStr := "" if storageops.CertificateNeedsRenewing(c) { renewStr = " needs-renewing" } fmt.Fprintf(&buf, " best: %v%s\n", c, renewStr) return nil }) if storageops.HaveUncachedCertificates(s) { fmt.Fprintf(&buf, "\nThere are uncached certificates.\n") } return buf.String() } func cmdAccountThumbprint() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") s.VisitAccounts(func(a *storage.Account) error { thumbprint, _ := acmeutils.Base64Thumbprint(a.PrivateKey) fmt.Printf("%s\t%s\n", thumbprint, a.ID()) return nil }) } 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") alreadyExists := false s.VisitTargets(func(t *storage.Target) error { nm := map[string]struct{}{} for _, n := range t.Satisfy.Names { nm[n] = struct{}{} } for _, w := range hostnames { if _, ok := nm[w]; !ok { return nil } } alreadyExists = true return nil }) if alreadyExists { return } // Add the target. tgt := storage.Target{ Satisfy: storage.TargetSatisfy{ Names: hostnames, }, } err = s.SaveTarget(&tgt) log.Fatale(err, "add target") } func cmdUnwant() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") for _, hn := range *unwantArg { err = storageops.RemoveTargetHostname(s, hn) log.Fatale(err, "remove target hostname ", hn) } } func cmdRunRedirector() { rpath := *redirectorPathFlag if rpath == "" { // redirector process is internet-facing and must never touch private keys storage.Neuter() rpath = determineWebroot() } service.Main(&service.Info{ Name: "acmetool", Description: "acmetool HTTP redirector", DefaultChroot: rpath, NewFunc: func() (service.Runnable, error) { return redirector.New(redirector.Config{ Bind: ":80", ChallengePath: rpath, ChallengeGID: *redirectorGIDFlag, ReadTimeout: *redirectorReadTimeout, WriteTimeout: *redirectorWriteTimeout, StatusCode: *redirectorStatusCodeFlag, }) }, }) } func determineWebroot() string { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") webrootPaths := s.DefaultTarget().Request.Challenge.WebrootPaths if len(webrootPaths) > 0 { return webrootPaths[0] } return responder.StandardWebrootPath } func cmdRunTestNotify() { ctx := &hooks.Context{ HooksDir: *hooksFlag, StateDir: *stateFlag, } err := hooks.NotifyLiveUpdated(ctx, *testNotifyArg) log.Errore(err, "notify") } // YAML response file loading. func loadResponseFile(path string) error { b, err := ioutil.ReadFile(path) if err != nil { return err } m := map[string]interface{}{} err = yaml.Unmarshal(b, &m) if err != nil { return err } for k, v := range m { r, err := parseResponse(v) if err != nil { log.Errore(err, "response for ", k, " invalid") continue } interaction.SetResponse(k, r) } return nil } func parseResponse(v interface{}) (*interaction.Response, error) { switch x := v.(type) { case string: return &interaction.Response{ Value: x, }, nil case int: return &interaction.Response{ Value: fmt.Sprintf("%d", x), }, nil case bool: return &interaction.Response{ Cancelled: !x, }, nil default: return nil, fmt.Errorf("unknown response value") } } func cmdRevoke() { certSpec := *revokeArg f, _ := os.Open(certSpec) //var fi os.FileInfo if f != nil { defer f.Close() //var err error //fi, err = f.Stat() //log.Panice(err) } //u, _ := url.Parse(certSpec) switch { //case f != nil && !fi.IsDir(): // is a file path //case f != nil && fi.IsDir(): // is a directory path // f, _ = os.Open(filepath.Join(certSpec, "cert")) //case u != nil && u.IsAbs() && acmeapi.ValidURL(certSpec): // is an URL case storage.IsWellFormattedCertificateOrKeyID(certSpec): // key or certificate ID revokeByCertificateID(certSpec) default: log.Fatalf("don't understand argument, must be a certificate or key ID: %q", certSpec) } } func revokeByCertificateID(certID string) { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") err = storageops.RevokeByCertificateOrKeyID(s, certID) log.Fatale(err, "revoke") err = storageops.Reconcile(s) log.Fatale(err, "reconcile") } acme-0.0.62/cmd/acmetool/main_ig_test.go000066400000000000000000000043431321256625400200550ustar00rootroot00000000000000// +build integration package main import ( "fmt" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/acme/interaction" "github.com/hlandau/acme/responder" "io/ioutil" "path/filepath" "strings" "testing" ) type interceptor struct { } func (i *interceptor) Prompt(c *interaction.Challenge) (*interaction.Response, error) { switch c.UniqueID { case "acmetool-quickstart-choose-server": return &interaction.Response{Value: "url"}, nil case "acmetool-quickstart-enter-directory-url": return &interaction.Response{Value: "http://127.0.0.1:4000/directory"}, nil case "acmetool-quickstart-choose-method": return &interaction.Response{Value: "redirector"}, nil case "acme-enter-email": return &interaction.Response{Value: "nobody@example.com"}, nil case "acmetool-quickstart-complete": return &interaction.Response{}, nil case "acmetool-quickstart-install-cronjob", "acmetool-quickstart-install-haproxy-script", "acmetool-quickstart-install-redirector-systemd": return &interaction.Response{Cancelled: true}, nil default: if strings.HasPrefix(c.UniqueID, "acme-agreement:") { return &interaction.Response{}, nil } return nil, fmt.Errorf("unsupported challenge for interceptor: %v", c) } } func (i *interceptor) Status(info *interaction.StatusInfo) (interaction.StatusSink, error) { return nil, fmt.Errorf("status not supported") } func TestCLI(t *testing.T) { log.Warnf("This test requires a configured Boulder instance listening at http://127.0.0.1:4000/ and the ability to successfully complete challenges. You must change the Boulder configuration to use ports 80 and 5001. Also change the rate limits per certificate name. Consider ensuring that the user you run these tests as can write to %s and that that directory is served on port 80 /.well-known/acme-challenge/", responder.StandardWebrootPath) acmeapi.TestingAllowHTTP = true interaction.Interceptor = &interceptor{} tmpDir, err := ioutil.TempDir("", "acmetool-test") if err != nil { panic(err) } *stateFlag = filepath.Join(tmpDir, "state") *hooksFlag = filepath.Join(tmpDir, "hooks") responder.InternalTLSSNIPort = 5001 cmdQuickstart() *wantArg = []string{"dom1.acmetool-test.devever.net", "dom2.acmetool-test.devever.net"} cmdWant() cmdReconcile() } acme-0.0.62/cmd/acmetool/quickstart-linux.go000066400000000000000000000061451321256625400207440ustar00rootroot00000000000000// +build linux package main import ( "fmt" sddbus "github.com/coreos/go-systemd/dbus" sdunit "github.com/coreos/go-systemd/unit" "github.com/hlandau/acme/interaction" "gopkg.in/hlandau/svcutils.v1/exepath" "gopkg.in/hlandau/svcutils.v1/systemd" // coreos/go-systemd/util requires cgo "io" "os" ) func promptSystemd() { if !systemd.IsRunningSystemd() { log.Debugf("not running systemd") return } log.Debug("connecting to systemd") conn, err := sddbus.New() if err != nil { log.Errore(err, "connect to systemd") return } defer conn.Close() log.Debug("connected") props, err := conn.GetUnitProperties("acmetool-redirector.service") if err != nil { log.Errore(err, "systemd GetUnitProperties") return } if props["LoadState"].(string) != "not-found" { log.Info("acmetool-redirector.service unit already installed, skipping") return } r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "Install Redirector as systemd Service?", Body: `Would you like acmetool to automatically install the redirector as a systemd service? The service name will be acmetool-redirector.`, ResponseType: interaction.RTYesNo, UniqueID: "acmetool-quickstart-install-redirector-systemd", }) log.Fatale(err, "interaction") if r.Cancelled { return } username, err := determineAppropriateUsername() if err != nil { log.Errore(err, "determine appropriate username") return } f, err := os.OpenFile("/etc/systemd/system/acmetool-redirector.service", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) if err != nil { log.Errore(err, "acmetool-redirector.service unit file already exists?") return } defer f.Close() rdr := sdunit.Serialize([]*sdunit.UnitOption{ sdunit.NewUnitOption("Unit", "Description", "acmetool HTTP redirector"), sdunit.NewUnitOption("Service", "Type", "notify"), sdunit.NewUnitOption("Service", "ExecStart", exepath.Abs+` redirector --service.uid=`+username), sdunit.NewUnitOption("Service", "Restart", "always"), sdunit.NewUnitOption("Service", "RestartSec", "30"), sdunit.NewUnitOption("Install", "WantedBy", "multi-user.target"), }) _, err = io.Copy(f, rdr) if err != nil { log.Errore(err, "cannot write unit file") return } f.Close() err = conn.Reload() // softfail log.Warne(err, "systemctl daemon-reload failed") _, _, err = conn.EnableUnitFiles([]string{"acmetool-redirector.service"}, false, false) log.Errore(err, "failed to enable unit acmetool-redirector.service") _, err = conn.StartUnit("acmetool-redirector.service", "replace", nil) log.Errore(err, "failed to start acmetool-redirector") resultStr := "The acmetool-redirector service was successfully started." if err != nil { resultStr = "The acmetool-redirector service WAS NOT successfully started. You may have a web server listening on port 80. You will need to troubleshoot this yourself." } _, err = interaction.Auto.Prompt(&interaction.Challenge{ Title: "systemd Service Installation Complete", Body: fmt.Sprintf(`acmetool-redirector has been installed as a systemd service. %s`, resultStr), UniqueID: "acmetool-quickstart-complete", }) log.Errore(err, "interaction") } acme-0.0.62/cmd/acmetool/quickstart-nlinux.go000066400000000000000000000000711321256625400211120ustar00rootroot00000000000000// +build !linux package main func promptSystemd() { } acme-0.0.62/cmd/acmetool/quickstart.go000066400000000000000000000577761321256625400176270ustar00rootroot00000000000000package main import ( "bytes" "crypto/rand" "fmt" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/acme/acmeapi/acmeendpoints" "github.com/hlandau/acme/hooks" "github.com/hlandau/acme/interaction" "github.com/hlandau/acme/storage" "github.com/hlandau/acme/storageops" "gopkg.in/hlandau/svcutils.v1/exepath" "gopkg.in/hlandau/svcutils.v1/passwd" "io/ioutil" "os" "os/exec" "path/filepath" "strconv" "strings" ) func cmdQuickstart() { s, err := storage.NewFDB(*stateFlag) log.Fatale(err, "storage") serverURL := promptServerURL() s.DefaultTarget().Request.Provider = serverURL err = s.SaveTarget(s.DefaultTarget()) log.Fatale(err, "set provider URL") // key type keyType := promptKeyType() switch keyType { case "rsa": s.DefaultTarget().Request.Key.Type = "rsa" rsaKeySize := promptRSAKeySize() if rsaKeySize != 0 { s.DefaultTarget().Request.Key.RSASize = rsaKeySize err = s.SaveTarget(s.DefaultTarget()) log.Fatale(err, "set preferred RSA Key size") } case "ecdsa": s.DefaultTarget().Request.Key.Type = "ecdsa" ecdsaCurve := promptECDSACurve() if ecdsaCurve != "" { s.DefaultTarget().Request.Key.ECDSACurve = ecdsaCurve err = s.SaveTarget(s.DefaultTarget()) log.Fatale(err, "set preferred ECDSA curve") } } // hook method method := promptHookMethod() var webroot []string switch method { case "webroot": webroot = []string{promptWebrootDir()} } if len(webroot) != 0 { err = os.MkdirAll(webroot[0], 0755) log.Fatale(err, "couldn't create webroot path") } s.DefaultTarget().Request.Challenge.WebrootPaths = webroot err = s.SaveTarget(s.DefaultTarget()) log.Fatale(err, "set webroot path") prog, err := interaction.Auto.Status(&interaction.StatusInfo{ Title: "Registering account...", }) log.Fatale(err, "status") prog.SetProgress(0, 1) err = storageops.EnsureRegistration(s) log.Fatale(err, "couldn't complete registration") prog.SetProgress(1, 1) prog.Close() if method == "redirector" { promptSystemd() } installDefaultHooks() if promptInstallCombinedHooks() { installCombinedHooks() } promptCron() promptGettingStarted() } const reloadHookFile = `#!/bin/sh ## This file was installed by acmetool. Any updates to this script will ## overwrite changes you make. If you don't want acmetool to manage ## this file, remove the following line. ##!acmetool-managed!## # This file reloads services when the preferred certificate for a hostname # changes. A list of commonly used daemons is preconfigured. You can override # this list by setting $SERVICES in /etc/{default,conf.d}/acme-reload. # # Configuration options: # /etc/{default,conf.d}/acme-reload # Sourced if they exist. Specify variables here. # Please note that most of the time, you don't need to specify anything. # # $SERVICES # Space-separated list of daemons to reload. # Append with SERVICES="$SERVICES mydaemon". ############################################################################### set -e EVENT_NAME="$1" [ "$EVENT_NAME" = "live-updated" ] || exit 42 SERVICES="httpd apache2 apache nginx tengine lighttpd postfix dovecot exim exim4 haproxy hitch quassel quasselcore" [ -e "/etc/default/acme-reload" ] && . /etc/default/acme-reload [ -e "/etc/conf.d/acme-reload" ] && . /etc/conf.d/acme-reload [ -z "$ACME_STATE_DIR" ] && ACME_STATE_DIR="@@ACME_STATE_DIR@@" # Restart services. if which service >/dev/null 2>/dev/null; then for x in $SERVICES; do service "$x" reload >/dev/null 2>/dev/null || true done exit 0 fi if which systemctl >/dev/null 2>/dev/null; then for x in $SERVICES; do [ -e "/lib/systemd/system/$x.service" -o -e "/etc/systemd/system/$x.service" ] && systemctl reload "$x.service" >/dev/null 2>/dev/null || true done exit 0 fi if [ -e "/etc/init.d" ]; then for x in $SERVICES; do /etc/init.d/$x reload >/dev/null 2>/dev/null || true done exit 0 fi` const combinedReloadHookFile = `#!/bin/sh ## This file was installed by acmetool. Any updates to this script will ## overwrite changes you make. If you don't want acmetool to manage ## this file, remove the following line. ##!acmetool-managed!## # This file generates combined certificate+private key files for daemons which # require them. It is called haproxy for legacy reasons, HAProxy being a common # example of such a daemon, but can also be used with other daemons taking the # same input format such as Hitch and Quasselcore. # # This is done outside of acmetool, because it is desired to avoid making # unnecessary copies of private keys except in environments where it is # necessary. This also demonstrates the power and flexibility of the hooks # system. # # The files consist of the private key, followed by the certificate and # certificate chain, followed by any data placed in conf/dhparams. # # This script is a no-op unless a daemon known to require combined files is # found. You can override this by setting $HAPROXY_ALWAYS_GENERATE or # $HAPROXY_DAEMONS in /etc/{default,conf.d}/acme-reload. # # (This file should be executed before 'reload'. So long as it is named # 'haproxy' and reload is named 'reload', that is assured.) # # DEBUGGING NOTE: If you make changes to the configuration this will not # be reflected simply by rerunning 'acmetool', because this script is only # called when a symlink in 'live' is updated. You can force this script to # be rerun by deleting all symlinks in 'live' and running 'acmetool'. # # Output: # $ACME_STATE_DIR/live/$HOSTNAME/haproxy # The combined certificate file for a hostname. # # $ACME_STATE_DIR/haproxy/$HOSTNAME # Symlinked to the combined certificate file. Daemons such as HAProxy # can prefer directories such as these, where each file is a hostname # containing combined data. # # Configuration options: # /etc/{default,conf.d}/acme-reload # Sourced if they exist. Specify variables here. # Please note that most of the time, you don't need to specify anything. # # $HAPROXY_ALWAYS_GENERATE # If non-empty, always generate combined files. # # $HAPROXY_DAEMONS # Space-separated list of binaries to search for in path. If any are found # (or $HAPROXY_ALWAYS_GENERATE is set), generate combined files. # Append with HAPROXY_DAEMONS="$HAPROXY_DAEMONS mydaemon". # Defaults: see below. # # $HAPROXY_DH_PATH # Defaults to "$ACME_STATE_DIR/conf/dhparams". If the file exists, it is # appended verbatim to combined certificate files. Commonly used to attach # custom Diffie-Hellman parameters. # # $HAPROXY_UMASK # Don't change this unless you know what you're doing. # If you change this, you must create a conf/perm file to reconfigure # acmetool's permissions enforcement. See _doc directory in repository. # Override path "certs/*/haproxy". ############################################################################### set -e EVENT_NAME="$1" [ "$EVENT_NAME" = "live-updated" ] || exit 42 # List of services. If any of these are in PATH (or HAPROXY_ALWAYS_GENERATE is # set), assume we need to generate combined files. HAPROXY_DAEMONS="haproxy hitch quasselcore quassel lighttpd" HAPROXY_UMASK="0077" [ -e "/etc/default/acme-reload" ] && . /etc/default/acme-reload [ -e "/etc/conf.d/acme-reload" ] && . /etc/conf.d/acme-reload [ -z "$ACME_STATE_DIR" ] && ACME_STATE_DIR="@@ACME_STATE_DIR@@" [ -z "$HAPROXY_DH_PATH" ] && HAPROXY_DH_PATH="$ACME_STATE_DIR/conf/dhparams" # Don't do anything if no daemon requiring combined files is found. [ -n "$HAPROXY_ALWAYS_GENERATE" ] || { ok= for exe in $HAPROXY_DAEMONS; do which "$exe" >/dev/null 2>/dev/null && ok=1 && break done [ -z "$ok" ] && exit 0 } # Create coalesced files and a haproxy repository. umask 0022 mkdir -p "$ACME_STATE_DIR/haproxy" umask $HAPROXY_UMASK while read name; do certdir="$ACME_STATE_DIR/live/$name" if [ -z "$name" -o ! -e "$certdir" ]; then continue fi if [ -n "$HAPROXY_DH_PATH" -a -e "$HAPROXY_DH_PATH" ]; then cat "$certdir/privkey" "$certdir/fullchain" "$HAPROXY_DH_PATH" > "$certdir/haproxy" else cat "$certdir/privkey" "$certdir/fullchain" > "$certdir/haproxy" fi [ -h "$ACME_STATE_DIR/haproxy/$name" ] || ln -fs "../live/$name/haproxy" "$ACME_STATE_DIR/haproxy/$name" done` func installHook(name, value string) { hooks.Replace(*hooksFlag, name, strings.Replace(value, "@@ACME_STATE_DIR@@", *stateFlag, -1)) // fail silently, allow non-root, makes travis work. } func installDefaultHooks() { installHook("reload", reloadHookFile) } func installCombinedHooks() { installHook("haproxy", combinedReloadHookFile) } var errStop = fmt.Errorf("stop") func isCronjobInstalled() bool { ms, err := filepath.Glob("/etc/cron.*/*acmetool*") log.Fatale(err, "glob") if len(ms) > 0 { return true } installed := false filepath.Walk("/var/spool/cron", func(p string, fi os.FileInfo, err error) error { if err != nil { return nil } if (fi.Mode() & os.ModeType) != 0 { return nil } if strings.Index(fi.Name(), "acmetool") >= 0 { installed = true return errStop } f, err := os.Open(p) if err != nil { return nil } defer f.Close() b, err := ioutil.ReadAll(f) if err != nil { return nil } if bytes.Index(b, []byte("acmetool")) >= 0 { installed = true return errStop } return nil }) return installed } func formulateCron(root bool) string { // Randomise cron time to avoid hammering the ACME server. var b [2]byte _, err := rand.Read(b[:]) log.Panice(err) m := b[0] % 60 h := b[1] % 24 s := "" if root { s = "SHELL=/bin/sh\nPATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin\nMAILTO=root\n" } s += fmt.Sprintf("%d %d * * * ", m, h) if root { s += "root " } s += fmt.Sprintf("%s --batch ", exepath.Abs) if *stateFlag != storage.RecommendedPath { s += fmt.Sprintf(`--state="%s" `, *stateFlag) } if *hooksFlag != hooks.RecommendedPath { s += fmt.Sprintf(`--hooks="%s" `, *hooksFlag) } s += "reconcile\n" return s } func runningAsRoot() bool { return os.Getuid() == 0 } func promptCron() { if isCronjobInstalled() { return } var err error cronString := formulateCron(runningAsRoot()) if runningAsRoot() { _, err = os.Stat("/etc/cron.d") } else { _, err = exec.LookPath("crontab") } if err != nil { log.Warnf("Don't know how to install a cron job on this system, please install the following job:\n%s\n", cronString) return } r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "Install auto-renewal cronjob?", Body: "Would you like to install a cronjob to renew certificates automatically? This is recommended.", ResponseType: interaction.RTYesNo, UniqueID: "acmetool-quickstart-install-cronjob", }) log.Fatale(err, "interaction") if r.Cancelled { return } if runningAsRoot() { f, err := os.OpenFile("/etc/cron.d/acmetool", os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644) if err != nil { log.Errore(err, "failed to install cron job at /etc/cron.d/acmetool (does the file already exist?), wanted to install: ", cronString) return } defer f.Close() f.Write([]byte(cronString)) } else { err := amendUserCron(cronString, "acmetool") if err != nil { log.Errore(err, "failed to amend user crontab to add: ", cronString) return } } } func amendUserCron(cronLine, filterString string) error { b, err := getUserCron() if err != nil { return err } if bytes.Index(b, []byte("acmetool")) >= 0 { return nil } b = append(b, '\n') b = append(b, []byte(cronLine)...) return setUserCron(b) } func getUserCron() ([]byte, error) { errBuf := bytes.Buffer{} listCmd := exec.Command("crontab", "-l") listCmd.Stderr = &errBuf b, err := listCmd.Output() if err == nil { return b, nil } // crontab -l returns 1 if no crontab is installed, grep stderr to identify this condition if bytes.Index(errBuf.Bytes(), []byte("no crontab for")) >= 0 { return nil, nil } return b, nil } func setUserCron(b []byte) error { setCmd := exec.Command("crontab", "-") setCmd.Stdin = bytes.NewReader(b) setCmd.Stdout = os.Stdout setCmd.Stderr = os.Stderr return setCmd.Run() } func promptInstallCombinedHooks() bool { // Always install if the hook is already installed. hooksPath := *hooksFlag if hooksPath == "" { hooksPath = hooks.DefaultPath } if _, err := os.Stat(filepath.Join(hooksPath, "haproxy")); err == nil { return true } // Prompt. r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "Install combined certificate files?", Body: fmt.Sprintf(`By default, acmetool stores certificates and private keys separately. The vast majority of daemons prefer this format. However, some daemons, such as HAProxy, require combined files which contain both certificate and private key. acmetool doesn't create such files by default to avoid creating unnecessary copies of private keys. acmetool can install a notification hook that will automatically generate an additional file called "haproxy" in every certificate directory. (These files are accepted by more than just HAProxy; the name is used for legacy reasons.) This means that you can point such daemons at "%s/live/HOSTNAME/haproxy". These files will also be accessible in a directory of their own, "%s/haproxy/HOSTNAME", for daemons which like a directory of files named after hostnames. If you place a PEM-encoded DH parameter file at "%s/conf/dhparams", those will also be included in each "haproxy" file. This is optional. Examples of daemons requiring combined files include HAProxy, Hitch and Quassel. The hook script will not generate the files unless one of these daemons is detected, or you configure it to always generate combined files. (See the hook script for configuration documentation.) Therefore, installing the script is a no-op on systems without these daemons installed, and it is always safe to say yes here. Do you want to install the combined file generation hook? If in doubt, say yes.`, *stateFlag, *stateFlag, *stateFlag), ResponseType: interaction.RTYesNo, Implicit: !*expertFlag, UniqueID: "acmetool-quickstart-install-haproxy-script", }) if err != nil { // Install by default, since the hook script does nothing unless a service requiring it is // installed. Can still be overriden by --expert or response file. return true } return !r.Cancelled } var usernamesToTry = []string{"daemon", "nobody"} func determineAppropriateUsername() (string, error) { for _, u := range usernamesToTry { _, err := passwd.ParseUID(u) if err == nil { return u, nil } } return "", fmt.Errorf("cannot find appropriate username") } func promptRSAKeySize() int { r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "RSA Key Size", Body: `Please enter the RSA key size to use for keys and account keys. The recommended key size is 2048. Unsupported key sizes will be clamped to the nearest supported value at generation time (the current minimum is 2048; the current maximum is 4096). Leave blank to use the recommended value, currently 2048.`, ResponseType: interaction.RTLineString, UniqueID: "acmetool-quickstart-rsa-key-size", Implicit: !*expertFlag, }) if err != nil { return 0 } if r.Cancelled { os.Exit(1) return 0 } v := strings.TrimSpace(r.Value) if v == "" { return 0 } n, err := strconv.ParseUint(v, 10, 31) if err != nil { interaction.Auto.Prompt(&interaction.Challenge{ Title: "Invalid RSA Key Size", Body: "The RSA key size must be an integer in decimal form.", UniqueID: "acmetool-quickstart-invalid-rsa-key-size", }) return promptRSAKeySize() } return int(n) } func promptKeyType() string { r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "Key Type Selection", Body: `Select the type of keys you want to use for account keys and certificates. If in doubt, select RSA.`, ResponseType: interaction.RTSelect, Options: []interaction.Option{ { Title: "RSA", Value: "rsa", }, { Title: "ECDSA", Value: "ecdsa", }, }, UniqueID: "acmetool-quickstart-key-type", Implicit: !*expertFlag, }) if err != nil { return "rsa" } if r.Cancelled { os.Exit(1) return "" } return r.Value } func promptECDSACurve() string { r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "ECDSA Curve Selection", Body: `Please select the ECDSA curve to use for keys and account keys. NOTE: nistp521 is not as well supported as the others and is not supported by Let's Encrypt.`, ResponseType: interaction.RTSelect, Options: []interaction.Option{ { Title: "NIST P-256 (recommended)", Value: "nistp256", }, { Title: "NIST P-384", Value: "nistp384", }, { Title: "NIST P-521 (limited support)", Value: "nistp521", }, }, UniqueID: "acmetool-quickstart-ecdsa-curve", Implicit: !*expertFlag, }) if err != nil { return "" } if r.Cancelled { os.Exit(1) return "" } return r.Value } func promptWebrootDir() string { r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "Enter Webroot Path", Body: `Please enter the path at which challenges should be stored. If your webroot path is /var/www, you would enter /var/www/.well-known/acme-challenge here. The directory will be created if it does not exist. Webroot paths vary by OS; please consult your web server configuration. `, ResponseType: interaction.RTLineString, UniqueID: "acmetool-quickstart-webroot-path", }) log.Fatale(err, "interaction") if r.Cancelled { os.Exit(1) return "" } path := r.Value path = strings.TrimRight(strings.TrimSpace(path), "/") if !filepath.IsAbs(path) { interaction.Auto.Prompt(&interaction.Challenge{ Title: "Invalid Webroot Path", Body: "The webroot path must be an absolute path.", UniqueID: "acmetool-quickstart-webroot-path-invalid", }) return promptWebrootDir() } if !strings.HasSuffix(path, "/.well-known/acme-challenge") { r1 := r r, err = interaction.Auto.Prompt(&interaction.Challenge{ Title: "Are you sure?", Body: `The webroot path you have entered does not end in "/.well-known/acme-challenge". This path will only work if you have specially configured your webserver to map requests for that path to the specified directory. Do you want to continue? To enter a different webroot path, select No.`, ResponseType: interaction.RTYesNo, Implicit: *batchFlag || r1.Noninteractive, UniqueID: "acmetool-quickstart-webroot-path-unlikely", }) if r != nil && r.Cancelled { return promptWebrootDir() } } err = os.MkdirAll(path, 0755) log.Fatale(err, "could not create directory: ", path) return path } func promptGettingStarted() { if *batchFlag { return } interaction.PrintStderrMessage( "Quickstart Complete", fmt.Sprintf(`The quickstart process is complete. Ensure your chosen challenge conveyance method is configured properly before attempting to request certificates. You can find more information about how to configure your system for each method in the acmetool documentation: https://github.com/hlandau/acme/blob/master/_doc/WSCONFIG.md To request a certificate, run: $ sudo acmetool want example.com www.example.com If the certificate is successfully obtained, it will be placed in %s/live/example.com/{cert,chain,fullchain,privkey}. `, *stateFlag)) } func promptHookMethod() string { r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "Select Challenge Conveyance Method", Body: `acmetool needs to be able to convey challenge responses to the ACME server in order to prove its control of the domains for which you issue certificates. These authorizations expire rapidly, as do ACME-issued certificates (Let's Encrypt certificates have a 90 day lifetime), thus it is essential that the completion of these challenges is a) automated and b) functioning properly. There are several options by which challenges can be facilitated: WEBROOT: The webroot option installs challenge files to a given directory. You must configure your web server so that the files will be available at . For example, if your webroot is "/var/www", specifying a webroot of "/var/www/.well-known/acme-challenge" is likely to work well. The directory will be created automatically if it does not already exist. PROXY: The proxy option requires you to configure your web server to proxy requests for paths under /.well-known/acme-challenge/ to a special web server running on port 402, which will serve challenges appropriately. REDIRECTOR: The redirector option runs a special web server daemon on port 80. This means that you cannot run your own web server on port 80. The redirector redirects all HTTP requests to the equivalent HTTPS URL, so this is useful if you want to enforce use of HTTPS. You will need to configure your web server to not listen on port 80, and you will need to configure your system to run "acmetool redirector" as a daemon. If your system uses systemd, an appropriate unit file can automatically be installed. LISTEN: Directly listen on port 80 or 443, whichever is available, in order to complete challenges. This is useful only for development purposes. STATELESS: Some web servers can be configured to respond to challenges themselves. This removes any need for interaction with acmetool. See documentation for web server support. HOOK: Programmatic challenge provisioning. Advanced users only. Please see documentation.`, ResponseType: interaction.RTSelect, Options: []interaction.Option{ { Title: "WEBROOT - Place challenges in a directory", Value: "webroot", }, {Title: "PROXY - I'll proxy challenge requests to an HTTP server", Value: "proxy", }, {Title: "REDIRECTOR - I want to use acmetool's redirect-to-HTTPS functionality", Value: "redirector", }, {Title: "LISTEN - Listen on port 80 or 443 (only useful for development purposes)", Value: "listen", }, {Title: "STATELESS - I will configure my web server with my account key", Value: "stateless", }, {Title: "HOOK - I will write scripts to provision challenges", Value: "hook", }, }, UniqueID: "acmetool-quickstart-choose-method", }) log.Fatale(err, "interaction") if r.Cancelled { os.Exit(1) return "" } return r.Value } func promptServerURL() string { var options []interaction.Option acmeendpoints.Visit(func(e *acmeendpoints.Endpoint) error { t := e.Title switch e.Code { case "LetsEncryptLive": t += " - I want live certificates" case "LetsEncryptStaging": t += " - I want test certificates" } options = append(options, interaction.Option{ Title: t, Value: e.DirectoryURL, }) return nil }) options = append(options, interaction.Option{ Title: "Enter an ACME server URL", Value: "url", }) r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "Select ACME Server", Body: `Please choose an ACME server from which to request certificates. Your principal choices are the Let's Encrypt Live Server, and the Let's Encrypt Staging Server. You can use the Let's Encrypt Live Server to get real certificates. The Let's Encrypt Staging Server does not issue publically trusted certificates. It is useful for development purposes, as it has far higher rate limits than the live server.`, ResponseType: interaction.RTSelect, Options: options, UniqueID: "acmetool-quickstart-choose-server", }) log.Fatale(err, "interaction") if r.Cancelled { os.Exit(1) return "" } if r.Value == "url" { for { r, err = interaction.Auto.Prompt(&interaction.Challenge{ Title: "Select ACME Server", Body: `Please enter the "Directory URL" of an ACME server. This must be an HTTPS URL pointing to the ACME directory for the server.`, ResponseType: interaction.RTLineString, UniqueID: "acmetool-quickstart-enter-directory-url", }) log.Fatale(err, "interaction") if r.Cancelled { os.Exit(1) return "" } if acmeapi.ValidURL(r.Value) { break } interaction.Auto.Prompt(&interaction.Challenge{ Title: "Invalid ACME URL", Body: "That was not a valid ACME Directory URL. An ACME Directory URL must be a valid HTTPS URL.", ResponseType: interaction.RTAcknowledge, UniqueID: "acmetool-quickstart-invalid-directory-url", }) log.Fatale(err, "interaction") if r.Cancelled { os.Exit(1) return "" } } } return r.Value } acme-0.0.62/fdb/000077500000000000000000000000001321256625400132455ustar00rootroot00000000000000acme-0.0.62/fdb/fdb.go000066400000000000000000000426371321256625400143430ustar00rootroot00000000000000// Package fdb allows for the use of a filesystem directory as a simple // database on UNIX-like systems. package fdb import ( "fmt" deos "github.com/hlandau/goutils/os" "github.com/hlandau/xlog" "gopkg.in/hlandau/svcutils.v1/passwd" "io/ioutil" "os" "path/filepath" "strings" ) var log, Log = xlog.New("fdb") // FDB instance. type DB struct { cfg Config path string extantDirs map[string]struct{} effectivePermissions []Permission } // FDB configuration. type Config struct { Path string Permissions []Permission PermissionsPath string // If not "", allow permissions to be overriden from this file. } // Expresses the permission policy for a given path. The first match is used. type Permission struct { // The path to which the permission applies. May contain wildcards and must // match a collection path, not an object path. e.g. // accounts/*/* // tmp // The directory will receive the DirMode and any objects inside will receive // the FileMode. Since all new files are initially created in tmp, it is // essential that tp have permissions specified which are strictly as strict // or stricter than the permissions of the strictest collection. // The root collection matches the path ".". Path string FileMode os.FileMode DirMode os.FileMode UID string // if not "", user/UID to enforce GID string // if not "", group/GID to enforce } // Merge b into a. func mergePermissions(a, b []Permission) ([]Permission, error) { var r []Permission r = append(r, a...) am := map[string]int{} for i := range a { am[a[i].Path] = i } for i := range b { ai, ok := am[b[i].Path] if ok { r[ai] = b[i] } else { r = append(r, b[i]) } } return r, nil } // Return a copy of a but without any permissions with paths in pathsToErase. func erasePermissionsByPath(a []Permission, pathsToErase map[string]struct{}) []Permission { var r []Permission for i := range a { _, erase := pathsToErase[a[i].Path] if !erase { r = append(r, a[i]) } } return r } // Open a fdb database or create a new database. func Open(cfg Config) (*DB, error) { path, err := filepath.Abs(cfg.Path) if err != nil { return nil, err } db := &DB{ cfg: cfg, path: path, extantDirs: map[string]struct{}{}, } err = db.clearTmp() if err != nil { return nil, err } err = db.Verify() if err != nil { return nil, err } return db, nil } // Closes the database. func (db *DB) Close() error { return nil } // Integrity checks // Verify the consistency and validity of the database (e.g. link targets). // This is called automatically when opening the database so you shouldn't need // to call it. func (db *DB) Verify() error { err := db.loadPermissions() if err != nil { return err } err = db.createDirs() if err != nil { return err } // don't do this until now as EvalSymlinks requires the directory to exist db.path, err = filepath.EvalSymlinks(db.path) if err != nil { return err } err = db.conformPermissions() if err != nil { return err } return nil } func (db *DB) createDirs() error { for _, p := range db.effectivePermissions { if strings.IndexByte(p.Path, '*') >= 0 { continue } uid, gid, err := resolveUIDGID(&p) if err != nil { return err } err = mkdirAllWithOwner(filepath.Join(db.path, p.Path), p.DirMode, uid, gid) if err != nil { return err } } return nil } func (db *DB) loadPermissions() error { db.effectivePermissions = db.cfg.Permissions if db.cfg.PermissionsPath == "" { return nil } r, err := db.Collection("").Open(db.cfg.PermissionsPath) if err != nil { if os.IsNotExist(err) { return nil } return err } defer r.Close() ps, erasePaths, err := parsePermissions(r) if err != nil { return fmt.Errorf("badly formatted permissions file: %v", err) } mergedPermissions, err := mergePermissions(db.cfg.Permissions, ps) if err != nil { return err } mergedPermissions = erasePermissionsByPath(mergedPermissions, erasePaths) db.effectivePermissions = mergedPermissions return nil } // Returns the UID and GID to enforce. If the UID or GID is -1, it is not // to be enforced. Neither or both or either of the UID or GID may be -1. func resolveUIDGID(p *Permission) (uid, gid int, err error) { if p.UID == "$r" { uid = os.Getuid() } else if p.UID != "" { uid, err = passwd.ParseUID(p.UID) if err != nil { return } } else { uid = -1 } if p.GID == "$r" { gid = os.Getgid() } else if p.GID != "" { gid, err = passwd.ParseGID(p.GID) if err != nil { return } } else { gid = -1 } 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 { if err != nil { return err } rpath, err := filepath.Rel(db.path, path) if err != nil { 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: case os.ModeDir: db.extantDirs[rpath] = struct{}{} case os.ModeSymlink: l, err := os.Readlink(path) if err != nil { return err } if filepath.IsAbs(l) { return fmt.Errorf("database symlinks must not have absolute targets: %v: %v", path, l) } ll := filepath.Join(filepath.Dir(path), l) ll, err = filepath.Abs(ll) if err != nil { return err } ok, err := pathIsWithin(ll, db.path) if err != nil { return err } if !ok { 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) err := os.Remove(path) if err != nil { return err } } else if err != nil { log.Errore(err, "stat symlink") return err } default: return fmt.Errorf("unexpected file type in state directory: %s", mode) } perm := db.longestMatching(rpath) if perm == nil { log.Warnf("object without any permissions specified: %v", rpath) } else { correctPerm := perm.FileMode if (mode & os.ModeType) == os.ModeDir { correctPerm = perm.DirMode } if (mode & os.ModeType) != os.ModeSymlink { if fperm := mode.Perm(); fperm != correctPerm { log.Warnf("%#v has wrong mode %v, changing to %v", rpath, fperm, correctPerm) err := os.Chmod(path, correctPerm) if err != nil { return err } } } correctUID, correctGID, err := resolveUIDGID(perm) if err != nil { return err } if correctUID >= 0 || correctGID >= 0 { curUID, err := deos.GetFileUID(info) if err != nil { return err } curGID, err := deos.GetFileGID(info) if err != nil { return err } if correctUID < 0 { correctUID = curUID } if correctGID < 0 { correctGID = curGID } if curUID != correctUID || curGID != correctGID { log.Warnf("%#v has wrong UID/GID %v/%v, changing to %v/%v", rpath, curUID, curGID, correctUID, correctGID) err := os.Lchown(path, correctUID, correctGID) // Can't chown if not root so be a bit forgiving, but always moan log.Errore(err, "could not lchown file ", rpath) } } } return nil }) if err != nil { return err } return nil } func (db *DB) longestMatching(path string) *Permission { pattern := "" var perm *Permission if filepath.IsAbs(path) { panic("do not call longestMatching with an absolute path") } for { for _, p := range db.effectivePermissions { m, err := filepath.Match(p.Path, path) if err != nil { return nil } if m && len(p.Path) > len(pattern) { pattern = p.Path p2 := p perm = &p2 } } if perm != nil { return perm } if path == "." { break } path = filepath.Join(path, "..") } return nil } func pathIsWithin(subject, root string) (bool, error) { return strings.HasPrefix(subject, ensureSeparator(root)), nil } func ensureSeparator(p string) string { if !strings.HasSuffix(p, string(filepath.Separator)) { return p + string(filepath.Separator) } return p } func (db *DB) clearTmp() error { ms, err := filepath.Glob(filepath.Join(db.path, "tmp", "*")) if err != nil { return err } for _, m := range ms { err := os.RemoveAll(m) if err != nil { return err } } return nil } func (db *DB) ensurePath(path string) error { _, ok := db.extantDirs[path] if ok { return nil } mode := os.FileMode(0755) perm := db.longestMatching(path) if perm != nil { mode = perm.DirMode } uid, gid, err := resolveUIDGID(perm) if err != nil { return err } err = mkdirAllWithOwner(filepath.Join(db.path, path), mode, uid, gid) if err != nil { return err } db.extantDirs[path] = struct{}{} return nil } // Database Access // Collection represents a collection of objects in an fdb database. More // accurately, it is a contextual point in the database hierarchy which object // references are interpreted relative to. type Collection struct { db *DB name string ensuredPath bool } // Obtain a collection. The collection will be created automatically if it does // not already exist. Guaranteed to return a non-nil value. func (db *DB) Collection(collectionName string) *Collection { return &Collection{ db: db, name: collectionName, } } // Obtain a collection underneath the given collection. The collection will be // created automatically if it does not already exist. Guaranteed to return a // non-nil value. func (c *Collection) Collection(name string) *Collection { return &Collection{ db: c.db, name: filepath.Join(c.name, name), } } func (c *Collection) ensurePath() error { if c.ensuredPath { return nil } err := c.db.ensurePath(c.name) if err != nil { return err } c.ensuredPath = true return nil } // Stream for reading an object from the database. type ReadStream interface { Close() error Read([]byte) (int, error) Seek(int64, int) (int64, error) } // Stream for writing an object to the database. Changes do not take effect // until the stream is closed, at which case they are applied atomically. type WriteStream interface { ReadStream Write([]byte) (int, error) // Abort writing of the file. The file is not changed. Calling Close or // CloseAbort after calling this has no effect. CloseAbort() error } // A link points to a given name in a given collection. The database ensures // referential integrity. type Link struct { Target string // "collection1/subcollection/etc/objectName" } // Returns the database from which the collection was created. func (c *Collection) DB() *DB { return c.db } // Returns the collection path. func (c *Collection) Name() string { return c.name } // Returns the OS path to the file with the given name inside the collection. // If name is "", returns the OS path to the collection. func (c *Collection) OSPath(name string) string { c.ensurePath() // ignore error return filepath.Join(c.db.path, c.name, name) } // Atomically delete an existing object or link or subcollection in the given // collection with the given name. Returns nil if the object does not exist. func (c *Collection) Delete(name string) error { return os.RemoveAll(filepath.Join(c.db.path, c.name, name)) } // Returned when calling Open() on a symlink. (To open symlinks, use Openl.) var ErrIsLink = fmt.Errorf("cannot open symlink") // Open an existing object in the given collection with the given name. The // object is read-only. Returns an error if the object does not exist or // is a link. func (c *Collection) Open(name string) (ReadStream, error) { return c.open(name, false) } // Like Open(), but follows links automatically. func (c *Collection) Openl(name string) (ReadStream, error) { return c.open(name, true) } func (c *Collection) open(name string, allowSymlinks bool) (ReadStream, error) { fi, err := os.Lstat(filepath.Join(c.db.path, c.name, name)) again: if err != nil { return nil, err } m := fi.Mode() switch m & os.ModeType { case 0: case os.ModeSymlink: if !allowSymlinks { return nil, ErrIsLink } fi, err = os.Stat(filepath.Join(c.db.path, c.name, name)) goto again case os.ModeDir: return nil, fmt.Errorf("cannot open a collection") default: return nil, fmt.Errorf("unknown file type") } f, err := os.Open(filepath.Join(c.db.path, c.name, name)) if err != nil { return nil, err } return f, nil } // Create a new object in the given collection with the given name. If the // object already exists, it will be overwritten atomically. Changes only take // effect once the stream is closed. func (c *Collection) Create(name string) (WriteStream, error) { err := c.ensurePath() if err != nil { return nil, err } f, err := ioutil.TempFile(filepath.Join(c.db.path, "tmp"), "tmp.") if err != nil { return nil, err } return &closeWrapper{ db: c.db, f: f, finalName: filepath.Join(c.db.path, c.name, name), finalNameRel: filepath.Join(c.name, name), writing: true, }, nil } type closeWrapper struct { db *DB f *os.File finalName string finalNameRel string closed bool writing bool } func (cw *closeWrapper) Close() error { if cw.closed { return nil } n := cw.f.Name() err := cw.f.Close() if err != nil { return err } err = os.Rename(n, cw.finalName) if err != nil { return err } if cw.writing { err = cw.db.enforcePermissionsOnFile(cw.finalNameRel, cw.finalNameRel, false) if err != nil { return err } } cw.closed = true return nil } func (db *DB) enforcePermissionsOnFile(rpath, rpathFinal string, symlink bool) error { p := db.longestMatching(rpathFinal) if p == nil { return nil } fpath := filepath.Join(db.path, rpath) // TempFile creates files with mode 0600, so it's OK to chmod/chown it here, race-wise. correctUID, correctGID, err := resolveUIDGID(p) if err != nil { return err } curUID, curGID := os.Getuid(), os.Getgid() if correctUID < 0 { correctUID = curUID } if correctGID < 0 { correctGID = curGID } log.Debugf("enforce permissions: %s %d/%d %d/%d", rpath, curUID, curGID, correctUID, correctGID) if correctUID != curUID || correctGID != curGID { err := os.Lchown(fpath, correctUID, correctGID) // failure is nonfatal, may not be root log.Errore(err, "could not set correct owner for file", fpath) } if !symlink { err = os.Chmod(fpath, p.FileMode) if err != nil { return err } } return nil } func (cw *closeWrapper) CloseAbort() error { if cw.closed { return nil } err := cw.f.Close() if err != nil { return err } err = os.Remove(cw.f.Name()) if err != nil { return err } cw.closed = true return nil } func (cw *closeWrapper) Read(b []byte) (int, error) { return cw.f.Read(b) } func (cw *closeWrapper) Write(b []byte) (int, error) { return cw.f.Write(b) } func (cw *closeWrapper) Seek(p int64, w int) (int64, error) { return cw.f.Seek(p, w) } // Read a link in the given collection with the given name. Returns an error // if the object does not exist or is not a link. func (c *Collection) ReadLink(name string) (Link, error) { fpath := filepath.Join(c.db.path, c.name, name) l, err := os.Readlink(fpath) if err != nil { return Link{}, err } flink := filepath.Join(filepath.Dir(fpath), l) lr, err := filepath.Rel(c.db.path, flink) if err != nil { return Link{}, err } return Link{Target: lr}, nil } // Write a link in the given collection with the given name. Any existing // object or link is overwritten atomically. func (c *Collection) WriteLink(name string, target Link) error { err := c.ensurePath() if err != nil { return err } from := filepath.Join(c.db.path, c.name, name) to := filepath.Join(c.db.path, target.Target) toRel, err := filepath.Rel(filepath.Dir(from), to) if err != nil { return err } tmpName, err := tempSymlink(toRel, filepath.Join(c.db.path, "tmp")) if err != nil { return err } err = c.db.enforcePermissionsOnFile(filepath.Join("tmp", filepath.Base(tmpName)), filepath.Join(c.name, name), true) if err != nil { return err } return os.Rename(tmpName, from) } func (c *Collection) ListAll() ([]string, error) { ms, err := filepath.Glob(filepath.Join(c.db.path, c.name, "*")) if err != nil { return nil, err } var objs []string for _, m := range ms { objs = append(objs, filepath.Base(m)) } return objs, nil } // List the objects and collections in the collection. Filenames beginning with // '.' are hidden. func (c *Collection) List() ([]string, error) { s, err := c.ListAll() if err != nil { return nil, err } s2 := make([]string, 0, len(s)) for _, x := range s { if strings.HasPrefix(x, ".") { continue } s2 = append(s2, x) } return s2, nil } acme-0.0.62/fdb/fdb_test.go000066400000000000000000000067711321256625400154010ustar00rootroot00000000000000package fdb import ( "io/ioutil" "os" "path/filepath" "reflect" "testing" ) func TestFDB(t *testing.T) { dir, err := ioutil.TempDir("", "acmefdbtest") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) const permissionsfile = ` # This is an example permissions file alpha 0604 0705 alpha/foo 0640 0750 ` err = ioutil.WriteFile(filepath.Join(dir, "Permissionsfile"), []byte(permissionsfile), 0644) if err != nil { t.Fatal(err) } db, err := Open(Config{ Path: dir, Permissions: []Permission{ {Path: ".", FileMode: 0644, DirMode: 0755}, {Path: "alpha", FileMode: 0644, DirMode: 0755}, {Path: "beta", FileMode: 0600, DirMode: 0700}, {Path: "tmp", FileMode: 0600, DirMode: 0700}, }, PermissionsPath: "Permissionsfile", }) if err != nil { t.Fatal(err) } defer db.Close() err = db.Verify() if err != nil { t.Fatal(err) } c := db.Collection("alpha/foo/x") if c.DB() != db { panic("...") } if c.Name() != "alpha/foo/x" { panic(c.Name()) } if c.OSPath("") != filepath.Join(dir, "alpha/foo/x") { panic(c.OSPath("")) } if c.OSPath("xyz") != filepath.Join(dir, "alpha/foo/x/xyz") { panic(c.OSPath("xyz")) } cc := db.Collection("alpha").Collection("foo").Collection("x") if cc.OSPath("xyz") != filepath.Join(dir, "alpha/foo/x/xyz") { panic(c.OSPath("xyz")) } b := []byte("\r\n\t 42 \n\n") err = WriteBytes(c, "xyz", b) if err != nil { t.Fatal(err) } n, err := Uint(c, "xyz", 31) if err != nil { t.Fatal(err) } if n != 42 { t.Fatalf("expected 42, got %v", n) } if !Exists(c, "xyz") { t.Fatalf("expected xyz to exist") } if Exists(c, "xyz1") { t.Fatalf("did not expect xyz1 to exist") } fi, err := os.Stat(c.OSPath("xyz")) if err != nil { t.Fatal(err) } if fi.Mode() != 0640 { t.Fatal("unexpected mode") } err = CreateEmpty(db.Collection("alpha"), "nak") if err != nil { t.Fatal(err) } fi, err = os.Stat(db.Collection("alpha").OSPath("nak")) if err != nil { t.Fatal(err) } if fi.Mode() != 0604 { t.Fatal("unexpected mode") } err = CreateEmpty(c, "xyz1") if err != nil { t.Fatal(err) } if !Exists(c, "xyz1") { t.Fatalf("expected xyz1 to exist") } err = c.Delete("xyz1") if err != nil { t.Fatal(err) } f, err := c.Create("xyz") if err != nil { t.Fatal(err) } _, err = f.Write([]byte("blah blah blah.")) if err != nil { t.Fatal(err) } err = f.CloseAbort() if err != nil { t.Fatal(err) } b2, err := Bytes(c.Open("xyz")) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(b, b2) { t.Fatal("mismatch") } s2, err := String(c.Open("xyz")) if err != nil { t.Fatal(err) } if s2 != string(b2) { t.Fatal("mismatch") } err = c.WriteLink("lnk", Link{Target: "alpha/foo/x/xyz"}) if err != nil { t.Fatal(err) } lnk, err := c.ReadLink("lnk") if err != nil { t.Fatal(err) } if lnk.Target != "alpha/foo/x/xyz" { t.Fatal(lnk.Target) } b2, err = Bytes(c.Openl("lnk")) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(b, b2) { t.Fatal("mismatch") } err = db.Verify() if err != nil { t.Fatal(err) } names, err := c.List() if err != nil { t.Fatal(err) } correctNames := []string{"lnk", "xyz"} if !reflect.DeepEqual(names, correctNames) { t.Fatalf("wrong names: %v != %v", names, correctNames) } err = c.Delete("xyz") if err != nil { t.Fatal(err) } err = db.Verify() if err != nil { t.Fatal(err) } _, err = c.Open("lnk") if err == nil { t.Fatal("lnk should have been removed") } err = db.Verify() if err != nil { t.Fatal(err) } } acme-0.0.62/fdb/mkdir.go000066400000000000000000000026001321256625400147000ustar00rootroot00000000000000package fdb import ( "os" "syscall" ) // Like os.MkdirAll but new components created have the given UID and GID. func mkdirAllWithOwner(absPath string, perm os.FileMode, uid, gid int) error { // From os/path.go. // Fast path: if we can tell whether path is a directory or file, stop with success or error. dir, err := os.Stat(absPath) if err == nil { if dir.IsDir() { return nil } return &os.PathError{"mkdir", absPath, syscall.ENOTDIR} } // Slow path: make sure parent exists and then call Mkdir for path. i := len(absPath) for i > 0 && os.IsPathSeparator(absPath[i-1]) { // Skip trailing path separator. i-- } j := i for j > 0 && !os.IsPathSeparator(absPath[j-1]) { // Scan backward over element. j-- } if j > 1 { // Create parent err = mkdirAllWithOwner(absPath[0:j-1], perm, uid, gid) if err != nil { return err } } // Parent now exists; invoke Mkdir and use its result. err = os.Mkdir(absPath, perm) if err != nil { // Handle arguments like "foo/." by double-checking that directory // doesn't exist. dir, err1 := os.Lstat(absPath) if err1 == nil && dir.IsDir() { return nil } return err } if uid >= 0 || gid >= 0 { if uid < 0 { uid = os.Getuid() } if gid < 0 { gid = os.Getgid() } err = os.Lchown(absPath, uid, gid) // ignore errors in case we aren't root log.Errore(err, "cannot chown ", absPath) } return nil } acme-0.0.62/fdb/parseperm.go000066400000000000000000000044451321256625400156010ustar00rootroot00000000000000package fdb import ( "bufio" "fmt" "gopkg.in/hlandau/svcutils.v1/passwd" "io" "os" "path/filepath" "regexp" "strconv" "strings" ) var re_permissionLine = regexp.MustCompile(`^(?P[^\s]+)\s+(?Pinherit|(?P[0-7]{3,4})\s+(?P[0-7]{3,4})(\s+(?P[^\s]+)\s+(?P[^\s]+))?)$`) func parsePermissions(r io.Reader) (ps []Permission, erasePaths map[string]struct{}, err error) { br := bufio.NewReader(r) Lnum := 0 erasePaths = map[string]struct{}{} seenPaths := map[string]struct{}{} for { Lnum++ L, err := br.ReadString('\n') if err == io.EOF { break } if err != nil { return nil, nil, err } L = strings.TrimSpace(L) if L == "" || strings.HasPrefix(L, "#") { continue } // keys/*/privkey 0640 0750 - - m := re_permissionLine.FindStringSubmatch(L) if m == nil { return nil, nil, fmt.Errorf("line %d: badly formatted line: %q", Lnum, L) } path := filepath.Clean(m[1]) if path == ".." || strings.HasPrefix(path, "../") || filepath.IsAbs(path) { return nil, nil, fmt.Errorf("line %d: path must remain within the DB root", Lnum, L) } if _, seen := seenPaths[path]; seen { return nil, nil, fmt.Errorf("line %d: duplicate path entry: %q", Lnum, L) } seenPaths[path] = struct{}{} if m[2] == "inherit" { erasePaths[path] = struct{}{} continue } fileMode, err := strconv.ParseUint(m[3], 8, 12) if err != nil { return nil, nil, fmt.Errorf("line %d: invalid file mode: %q", Lnum, m[3]) } dirMode, err := strconv.ParseUint(m[4], 8, 12) if err != nil { return nil, nil, fmt.Errorf("line %d: invalid dir mode: %q", Lnum, m[4]) } // Validate UID uid := m[6] if uid == "-" { uid = "" } if uid != "" && uid != "$r" { _, err := passwd.ParseUID(uid) if err != nil { return nil, nil, fmt.Errorf("line %d: invalid UID: %q: %v", Lnum, uid, err) } } // Validate GID gid := m[7] if gid == "-" { gid = "" } if gid != "" && gid != "$r" { _, err = passwd.ParseGID(gid) if err != nil { return nil, nil, fmt.Errorf("line %d: invalid GID: %q: %v", Lnum, gid, err) } } // p := Permission{ Path: path, FileMode: os.FileMode(fileMode), DirMode: os.FileMode(dirMode), UID: uid, GID: gid, } ps = append(ps, p) } return ps, erasePaths, nil } acme-0.0.62/fdb/parseperm_test.go000066400000000000000000000026451321256625400166400ustar00rootroot00000000000000// +build cgo package fdb import ( "reflect" "strings" "testing" ) func TestParsePerm(t *testing.T) { var tests = []struct { In string Out []Permission Erase map[string]struct{} }{ {``, nil, map[string]struct{}{}}, {` # this is a comment foo/bar 0644 0755 foo/*/baz 0640 0750 alpha 0644 0755 root daemon beta 0644 0755 42 42 gamma 0644 0755 $r $r delta inherit x 0644 0755 root - y 0644 0755 - daemon `, []Permission{ {Path: "foo/bar", FileMode: 0644, DirMode: 0755}, {Path: "foo/*/baz", FileMode: 0640, DirMode: 0750}, {Path: "alpha", FileMode: 0644, DirMode: 0755, UID: "root", GID: "daemon"}, {Path: "beta", FileMode: 0644, DirMode: 0755, UID: "42", GID: "42"}, {Path: "gamma", FileMode: 0644, DirMode: 0755, UID: "$r", GID: "$r"}, {Path: "x", FileMode: 0644, DirMode: 0755, UID: "root", GID: ""}, {Path: "y", FileMode: 0644, DirMode: 0755, UID: "", GID: "daemon"}, }, map[string]struct{}{"delta": struct{}{}}}, } for _, tst := range tests { ps, erase, err := parsePermissions(strings.NewReader(tst.In)) if err != nil { t.Fatalf("error parsing permissions: %v", err) } if !reflect.DeepEqual(ps, tst.Out) { t.Fatalf("permissions don't match: got %#v, expected %#v", ps, tst.Out) } if !reflect.DeepEqual(erase, tst.Erase) { t.Fatalf("erase list doesn't match: got %v, expected %v", erase, tst.Erase) } } } acme-0.0.62/fdb/tempsymlink.go000066400000000000000000000013731321256625400161540ustar00rootroot00000000000000package fdb import ( "os" "path/filepath" "strconv" "sync" "time" ) var rand uint32 var randmu sync.Mutex func reseed() uint32 { return uint32(time.Now().UnixNano() + int64(os.Getpid())) } func nextSuffix() string { randmu.Lock() r := rand if r == 0 { r = reseed() } r = r*1664525 + 1013904223 rand = r randmu.Unlock() return strconv.Itoa(int(1e9 + r%1e9))[1:] } func tempSymlink(target string, fromDir string) (tmpName string, err error) { nconflict := 0 for i := 0; i < 10000; i++ { tmpName = filepath.Join(fromDir, "symlink."+nextSuffix()) err = os.Symlink(target, tmpName) if os.IsExist(err) { if nconflict++; nconflict > 10 { randmu.Lock() rand = reseed() randmu.Unlock() } continue } break } return } acme-0.0.62/fdb/util.go000066400000000000000000000033041321256625400145510ustar00rootroot00000000000000package fdb import ( "io/ioutil" "strconv" "strings" ) // Read a file as a string. Use like this: // // s, err := String(c.Open("file")) // func String(rs ReadStream, err error) (string, error) { if err != nil { return "", err } defer rs.Close() b, err := ioutil.ReadAll(rs) if err != nil { return "", err } return string(b), nil } // Read a file as []byte. Use like this: // // s, err := Bytes(c.Open("file")) // func Bytes(rs ReadStream, err error) ([]byte, error) { if err != nil { return nil, err } defer rs.Close() b, err := ioutil.ReadAll(rs) if err != nil { return nil, err } return b, nil } // Create an empty file, overwriting it if it exists. func CreateEmpty(c *Collection, name string) error { f, err := c.Create(name) if err != nil { return err } f.Close() return nil } // Determine whether a file exists. func Exists(c *Collection, name string) bool { f, err := c.Open(name) if err != nil { return false } defer f.Close() return true } // Write bytes to a file with the given name in the given collection. // // The byte arrays are concatenated in the given order. func WriteBytes(c *Collection, name string, bs ...[]byte) error { f, err := c.Create(name) if err != nil { return err } defer f.CloseAbort() for _, b := range bs { _, err = f.Write(b) if err != nil { return err } } f.Close() return nil } // Retrieve an unsigned integer in decimal form from a file with the given name // in the given collection. bits is passed to ParseUint. func Uint(c *Collection, name string, bits int) (uint64, error) { s, err := String(c.Open(name)) if err != nil { return 0, err } s = strings.TrimSpace(s) return strconv.ParseUint(s, 10, bits) } acme-0.0.62/hooks/000077500000000000000000000000001321256625400136355ustar00rootroot00000000000000acme-0.0.62/hooks/hooks.go000066400000000000000000000147651321256625400153240ustar00rootroot00000000000000// Package hooks provides functions to invoke a directory of executable hooks, // used to provide arbitrary handling of significant events. package hooks import ( "fmt" deos "github.com/hlandau/goutils/os" "github.com/hlandau/xlog" "os" "os/exec" "path/filepath" "strings" ) // Log site. var log, Log = xlog.New("acme.hooks") // The recommended hook path is the path at which executable hooks are looked // for. On POSIX-like systems, this is usually "/usr/lib/acme/hooks" (or // "/usr/libexec/acme/hooks" if /usr/libexec exists). var RecommendedPath string // The default hook path defaults to the recommended hook path but could be // 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 if p == "" { p = "/usr/lib/acme/hooks" } if _, err := os.Stat("/usr/libexec"); strings.HasPrefix(p, "/usr/lib/") && err == nil { p = "/usr/libexec" + p[8:] } DefaultPath = p RecommendedPath = p } // Notifies hook programs that a live symlink has been updated. // // If hookDirectory is "", DefaultHookPath is used. stateDirectory and // hostnames are passed as information to the hooks. func NotifyLiveUpdated(ctx *Context, hostnames []string) error { if len(hostnames) == 0 { return nil } hostnameList := strings.Join(hostnames, "\n") + "\n" _, err := runParts(ctx, []byte(hostnameList), "live-updated") if err != nil { return err } return nil } // Invokes HTTP challenge start hooks. // // 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(ctx *Context, hostname, targetFileName, token, ka string) (installed bool, err error) { return runParts(ctx, []byte(ka), "challenge-http-start", hostname, targetFileName, token) } 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(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(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(ctx *Context, hostname, targetFileName, body string) (installed bool, err error) { return runParts(ctx, nil, "challenge-dns-start", hostname, targetFileName, body) } 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(ctx *Context, stdinData []byte, args ...string) (anySucceeded bool, err error) { directory := ctx.HooksDir if directory == "" { directory = DefaultPath } fi, err := os.Stat(directory) if err != nil { if os.IsNotExist(err) { // Not an error if the directory doesn't exist; nothing to do. return false, 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 { return false, fmt.Errorf("refusing to execute hooks, directory is world-writable: %s", directory) } ms, err := filepath.Glob(filepath.Join(directory, "*")) if err != nil { return false, err } for _, m := range ms { fi, err := os.Stat(m) if err != nil { log.Errore(err, "hook: ", m) continue } // Ignore 'hidden' files. if strings.HasPrefix(fi.Name(), ".") { continue } mode := fi.Mode() mType := mode & os.ModeType // Make sure it's not a directory, device, socket, pipe, etc. if mType != 0 && mType != os.ModeSymlink { log.Debugf("cannot execute hook, not a file: %s", m) continue } // Yes, this is vulnerable to race conditions; it's just to stop people // from shooting themselves in the foot. if (mode & 02) != 0 { log.Errorf("refusing to execute world-writable hook: %s", m) continue } // This doesn't check which mode bit (user,group,world) is applicable to // us but avoids cluttering the log for non-executable files. if (mode & 0111) == 0 { log.Debugf("cannot execute non-executable hook: %s", m) continue } var cmd *exec.Cmd if shouldSudoFile(m, fi) { log.Debugf("calling hook script (with sudo): %s", m) args2 := []string{"-n", "--", m} args2 = append(args2, args...) cmd = exec.Command("sudo", args2...) } else { log.Debugf("calling hook script: %s", m) cmd = exec.Command(m, args...) } cmd.Dir = "/" cmd.Env = env pipeR, pipeW, err := os.Pipe() if err != nil { return anySucceeded, err } defer pipeR.Close() go func() { defer pipeW.Close() pipeW.Write([]byte(stdinData)) }() cmd.Stdin = pipeR cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() logFailedExecution(m, err) if err == nil { anySucceeded = true } } return anySucceeded, nil } func logFailedExecution(hookPath string, err error) { if err == nil { return } exitCode, err2 := deos.GetExitCode(err) if err2 != nil { // Not an error code. ??? log.Errore(err2, "hook script: ", hookPath) return } switch exitCode { case 42: // Unsupported event type for this hook. Don't log anything; this is OK. default: log.Errore(err, "hook script: ", hookPath) } } acme-0.0.62/hooks/hooks_test.go000066400000000000000000000023461321256625400163530ustar00rootroot00000000000000package hooks import ( "fmt" "io/ioutil" "os" "path/filepath" "testing" ) const fileTpl = `#!/bin/sh %s [ -n "$ACME_STATE_DIR" ] || exit 1 echo NOTIFY-%d >> "$ACME_STATE_DIR/log" while read line; do echo L-$line >> "$ACME_STATE_DIR/log" done` var answer = []string{ `NOTIFY-0 L-a.b L-c.d L-e.f.g NOTIFY-1 L-a.b L-c.d L-e.f.g `, `NOTIFY-0 L-a.b L-c.d L-e.f.g NOTIFY-3 L-a.b L-c.d L-e.f.g `, } func TestNotify(t *testing.T) { dir, err := ioutil.TempDir("", "acme-notify-test") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) notifyDir := filepath.Join(dir, "notify") for i := 0; i < 2; i++ { err = Replace(notifyDir, "alpha", fmt.Sprintf(fileTpl, "", i*2+0)) if err != nil { t.Fatal(err) } err = Replace(notifyDir, "beta", fmt.Sprintf(fileTpl, "#!acmetool-managed!#", i*2+1)) if err != nil { t.Fatal(err) } os.Remove(filepath.Join(dir, "log")) ctx := &Context{ HooksDir: notifyDir, StateDir: dir, } err = NotifyLiveUpdated(ctx, []string{"a.b", "c.d", "e.f.g"}) if err != nil { t.Fatal(err) } b, err := ioutil.ReadFile(filepath.Join(dir, "log")) if err != nil { t.Fatal(err) } s := string(b) if s != answer[i] { t.Fatalf("mismatch: %v != %v", s, answer[i]) } } } acme-0.0.62/hooks/install.go000066400000000000000000000021241321256625400156310ustar00rootroot00000000000000package hooks import ( "bytes" "os" "path/filepath" ) // Installs a hook in the hooks directory. If the file already exists, it is // not overwritten unless it contains the string "#!acmetool-managed!#" in its // first 4096 bytes. func Replace(hookDirectory, name, data string) error { if hookDirectory == "" { hookDirectory = DefaultPath } filename := filepath.Join(hookDirectory, name) isManaged, err := isManagedFile(filename) if os.IsNotExist(err) || (err == nil && isManaged) { return writeHook(filename, data) } return err } func writeHook(filename, data string) error { err := os.MkdirAll(filepath.Dir(filename), 0755) if err != nil { return err } f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return nil } defer f.Close() f.Write([]byte(data)) return nil } func isManagedFile(filename string) (bool, error) { f, err := os.Open(filename) if err != nil { return false, err } defer f.Close() b := make([]byte, 4096) n, _ := f.Read(b) b = b[0:n] return bytes.Index(b, []byte("#!acmetool-managed!#")) >= 0, nil } acme-0.0.62/hooks/os.go000066400000000000000000000020641321256625400146070ustar00rootroot00000000000000package hooks import ( deos "github.com/hlandau/goutils/os" "os" "os/exec" ) func runningAsRoot() bool { return os.Getuid() == 0 } func fileIsScript(fn string) bool { f, err := os.Open(fn) if err != nil { return false } defer f.Close() var b [2]byte n, _ := f.Read(b[:]) if n < 2 { return false } return string(b[:]) == "#!" } // Vulnerable to race conditions, but this is just a check. sudo enforces all // security properties. func shouldSudoFile(fn string, fi os.FileInfo) bool { if runningAsRoot() { return false } _, err := exec.LookPath("sudo") if err != nil { return false } // Only setuid files if the setuid bit is set. if (fi.Mode() & os.ModeSetuid) == 0 { return false } // Don't sudo anything which appears to be setuid'd for a non-root user. // This doesn't really buy us anything security-wise, but it's not what // we're expecting. uid, err := deos.GetFileUID(fi) if err != nil || uid != 0 { return false } // Make sure the file is a script, otherwise we can just execute it directly. return fileIsScript(fn) } acme-0.0.62/interaction/000077500000000000000000000000001321256625400150315ustar00rootroot00000000000000acme-0.0.62/interaction/auto.go000066400000000000000000000042411321256625400163310ustar00rootroot00000000000000package interaction import ( "fmt" "github.com/hlandau/xlog" ) var log, Log = xlog.New("acme.interactor") // Used by Auto. If this is set, only autoresponses can be used. Any challenge // without an autoresponse fails. --batch. var NonInteractive = false type autoInteractor struct{} // Interactor which automatically uses the most suitable challenge method. var Auto Interactor = autoInteractor{} // Used by Auto. If this is non-nil, all challenges are directed to it. There // is no fallback if the interceptor fails. Autoresponses and NonInteractive // take precedence over this. var Interceptor Interactor // Used by Auto. Do not use the Dialog mode. --stdio. var NoDialog = false var responsesReceived = map[string]*Response{} func (ai autoInteractor) Prompt(c *Challenge) (*Response, error) { res, err := ai.prompt(c) if err == nil && c.UniqueID != "" { responsesReceived[c.UniqueID] = res } return res, err } // Returns a map from challenge UniqueIDs to responses received for those // UniqueIDs. Do not mutate the returned map. func ResponsesReceived() map[string]*Response { return responsesReceived } func (autoInteractor) prompt(c *Challenge) (*Response, error) { r, err := Responder.Prompt(c) if err == nil || c.Implicit { return r, err } log.Infoe(err, "interaction auto-responder couldn't give a canned response") if NonInteractive { return nil, fmt.Errorf("cannot prompt the user: currently non-interactive; try running without --batch flag") } if Interceptor != nil { return Interceptor.Prompt(c) } if !NoDialog { r, err := Dialog.Prompt(c) if err == nil { return r, nil } } return Stdio.Prompt(c) } type dummySink struct{} func (dummySink) Close() error { return nil } func (dummySink) SetProgress(n, ofM int) { } func (dummySink) SetStatusLine(status string) { } func (autoInteractor) Status(info *StatusInfo) (StatusSink, error) { if NonInteractive { return dummySink{}, nil } if Interceptor != nil { s, err := Interceptor.Status(info) if err != nil { return dummySink{}, nil } return s, err } if !NoDialog { r, err := Dialog.Status(info) if err == nil { return r, nil } } return Stdio.Status(info) } acme-0.0.62/interaction/dialog.go000066400000000000000000000123571321256625400166270ustar00rootroot00000000000000package interaction import ( "fmt" "io/ioutil" "os" "os/exec" "strings" "sync" "syscall" ) type dialogInteractor struct{} // Invokes a dialog program to create terminal dialog boxes. Fails if no such // program is available. var Dialog Interactor = dialogInteractor{} type dialogStatusSink struct { closeChan chan struct{} closeOnce sync.Once closedChan chan struct{} updateChan chan struct{} pipeW *os.File infoMutex sync.Mutex statusLine string progress int cmd *exec.Cmd } func (ss *dialogStatusSink) Close() error { ss.closeOnce.Do(func() { close(ss.closeChan) }) <-ss.closedChan return nil } func (ss *dialogStatusSink) SetProgress(n, ofM int) { ss.infoMutex.Lock() defer ss.infoMutex.Unlock() ss.progress = int((float64(n) / float64(ofM)) * 100) ss.notify() } func (ss *dialogStatusSink) SetStatusLine(status string) { ss.infoMutex.Lock() defer ss.infoMutex.Unlock() ss.statusLine = status ss.notify() } func (ss *dialogStatusSink) notify() { select { case ss.updateChan <- struct{}{}: default: } } func (ss *dialogStatusSink) loop() { A: for { select { case <-ss.closeChan: break A case <-ss.updateChan: ss.infoMutex.Lock() statusLine := ss.statusLine progress := ss.progress ss.infoMutex.Unlock() fmt.Fprintf(ss.pipeW, "XXX\n%d\n%s\nXXX\n", progress, statusLine) } } ss.pipeW.Close() ss.cmd.Wait() close(ss.closedChan) } func (dialogInteractor) Status(c *StatusInfo) (StatusSink, error) { cmdName, _ := findDialogCommand() if cmdName == "" { return nil, fmt.Errorf("cannot find whiptail or dialog binary in path") } width := "78" height := fmt.Sprintf("%d", strings.Count(c.StatusLine, "\n")+5) var opts []string if c.Title != "" { opts = append(opts, "--backtitle", "ACME", "--title", c.Title) } opts = append(opts, "--gauge", c.StatusLine, height, width, "0") pipeR, pipeW, err := os.Pipe() if err != nil { return nil, err } defer pipeR.Close() cmd := exec.Command(cmdName, opts...) cmd.Stdin = pipeR cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Start() if err != nil { pipeW.Close() return nil, err } ss := &dialogStatusSink{ closeChan: make(chan struct{}), closedChan: make(chan struct{}), updateChan: make(chan struct{}, 10), pipeW: pipeW, cmd: cmd, } go ss.loop() return ss, nil } func (dialogInteractor) Prompt(c *Challenge) (*Response, error) { cmdName, cmdType := findDialogCommand() if cmdName == "" { return nil, fmt.Errorf("cannot find whiptail or dialog binary in path") } width := "78" height := "49" yesLabelArg := "--yes-label" noLabelArg := "--no-label" noTagsArg := "--no-tags" if cmdType == "whiptail" { yesLabelArg = "--yes-button" noLabelArg = "--no-button" noTagsArg = "--notags" } var opts []string if c.Title != "" { opts = append(opts, "--backtitle", "ACME", "--title", c.Title) } var err error var pipeR *os.File var pipeW *os.File switch c.ResponseType { case RTAcknowledge: opts = append(opts, "--msgbox", c.Body, height, width) case RTYesNo: yesLabel := c.YesLabel if yesLabel == "" { yesLabel = "Yes" } noLabel := c.NoLabel if noLabel == "" { noLabel = "No" } opts = append(opts, yesLabelArg, yesLabel, noLabelArg, noLabel, "--yesno", c.Body, height, width) case RTLineString: pipeR, pipeW, err = os.Pipe() if err != nil { return nil, err } defer pipeR.Close() defer pipeW.Close() opts = append(opts, "--output-fd", "3", "--inputbox", c.Body, height, width) case RTSelect: pipeR, pipeW, err = os.Pipe() if err != nil { return nil, err } defer pipeR.Close() defer pipeW.Close() opts = append(opts, "--output-fd", "3", noTagsArg, "--menu", c.Body, height, width, "5") for _, o := range c.Options { opts = append(opts, o.Value, o.Title) } } cmd := exec.Command(cmdName, opts...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if pipeW != nil { cmd.ExtraFiles = append(cmd.ExtraFiles, pipeW) } rc, xerr, err := runCommand(cmd) if err != nil { return nil, err } // If we get error code >1 (particularly 255) the dialog command probably // doesn't support some option we pass it. Return an error, which should make // us fall back to stdio. if rc > 1 { return nil, xerr } res := &Response{} if pipeW != nil { pipeW.Close() } switch c.ResponseType { case RTLineString, RTSelect: b, err := ioutil.ReadAll(pipeR) if err != nil { return nil, err } res.Value = string(b) fallthrough case RTYesNo, RTAcknowledge: if rc != 0 && rc != 1 { return nil, xerr } res.Cancelled = (rc == 1) } return res, nil } var dialogCommand = "" var dialogCommandType = "" func findDialogCommand() (string, string) { if dialogCommand != "" { return dialogCommand, dialogCommandType } // not using whiptail for now, see #18 for _, s := range []string{"dialog"} { p, err := exec.LookPath(s) if err == nil { dialogCommand = p dialogCommandType = s return dialogCommand, dialogCommandType } } return "", "" } func runCommand(cmd *exec.Cmd) (int, error, error) { err := cmd.Run() if err == nil { return 0, nil, nil } if e, ok := err.(*exec.ExitError); ok { if ws, ok := e.Sys().(syscall.WaitStatus); ok { return ws.ExitStatus(), err, nil } } return 255, err, err } acme-0.0.62/interaction/interaction.go000066400000000000000000000055121321256625400177020ustar00rootroot00000000000000// Package interaction provides facilities for asking the user questions, via // dialogs or stdio. package interaction // Interaction mode. Specifies the type of response requested from the user. type ResponseType int const ( // Acknowledgement only. Show notice and require user to acknowledge before // continuing. Response fields ignored. RTAcknowledge ResponseType = iota // Show notice and ask user to agree/disagree. Response has Cancelled set // if user disagreed. RTYesNo // Require user to enter a single-line string, returned as the Value of the // Response. RTLineString // Require user to select from a number of options. RTSelect ) // A challenge prompt to be shown to the user. type Challenge struct { ResponseType ResponseType // The response type. Title string // Title to be used for e.g. a dialog box if shown. Body string // The text to be shown to the user. May be multiple lines. YesLabel string // Label to use for RTYesNo 'Yes' label. NoLabel string // Label to use for RTYesNo 'No' label. // Prompt line used for stdio prompts. For RTAcknowledge, defaults to 'Press // Return to continue.' or similar. For RTYesNo, defaults to 'Agree? [Yn]' // or similar. Prompt string // Challenge type unique identifier. This identifies the meaning of the // dialog and can be used to respond automatically to known dialogs. // Optional. UniqueID string // Specifies the options for RTSelect. Options []Option // An implicit challenge will never be shown to the user but may be provided // by a response file. Implicit bool } // An option in an RTSelect challenge. type Option struct { Title string // Option title. Value string // Internal value that the option represents. } // A user's response to a prompt. type Response struct { Cancelled bool // Set this to true if the user cancelled the challenge. Value string // Value the user entered, if applicable. Noninteractive bool // Set to true if the response came from a noninteractive source. } // Specifies the initial parameters for a status dialog. type StatusInfo struct { Title string // Title to be used for the status dialog. StatusLine string // The status line. This may contain multiple lines if desired. } // Used to control a status dialog. type StatusSink interface { // Close the dialog and wait for it to terminate. Close() error // Set progress = (n/ofM)%. SetProgress(n, ofM int) // Set the status line(s). You cannot specify a number of lines that exceeds // the number of lines specified in the initial StatusLine. SetStatusLine(status string) } // An Interactor facilitates interaction with the user. type Interactor interface { // Synchronously present a prompt to the user. Prompt(*Challenge) (*Response, error) // Asynchronously present status information to the user. Status(*StatusInfo) (StatusSink, error) } acme-0.0.62/interaction/responder.go000066400000000000000000000014741321256625400173670ustar00rootroot00000000000000package interaction import "fmt" type responder struct{} var responses = map[string]*Response{} // Auto-responder. Provides canned responses if they have been set. Otherwise, // fails. var Responder Interactor = responder{} func (responder) Status(c *StatusInfo) (StatusSink, error) { return nil, fmt.Errorf("not supported") } func (responder) Prompt(c *Challenge) (*Response, error) { if c.UniqueID == "" { return nil, fmt.Errorf("cannot auto-respond to a challenge without a unique ID") } res := responses[c.UniqueID] if res == nil { return nil, fmt.Errorf("unknown unique ID, cannot respond: %#v", c.UniqueID) } return res, nil } // Configures a canned response for the given interaction UniqueID. func SetResponse(uniqueID string, res *Response) { res.Noninteractive = true responses[uniqueID] = res } acme-0.0.62/interaction/stdio.go000066400000000000000000000104161321256625400165040ustar00rootroot00000000000000package interaction import ( "bufio" "fmt" "github.com/hlandau/goutils/text" "github.com/mitchellh/go-wordwrap" "gopkg.in/cheggaaa/pb.v1" "os" "strconv" "strings" "sync" ) type stdioInteractor struct{} // Interactor which uses un-fancy stdio prompts. var Stdio Interactor = stdioInteractor{} type stdioStatusSink struct { closeChan chan struct{} closeOnce sync.Once closedChan chan struct{} updateChan chan struct{} infoMutex sync.Mutex statusLine string progress int } func (ss *stdioStatusSink) Close() error { ss.closeOnce.Do(func() { close(ss.closeChan) }) <-ss.closedChan return nil } func (ss *stdioStatusSink) SetProgress(n, ofM int) { ss.infoMutex.Lock() defer ss.infoMutex.Unlock() ss.progress = int((float64(n) / float64(ofM)) * 100) ss.notify() } func (ss *stdioStatusSink) SetStatusLine(status string) { ss.infoMutex.Lock() defer ss.infoMutex.Unlock() ss.statusLine = status ss.notify() } func (ss *stdioStatusSink) notify() { select { case ss.updateChan <- struct{}{}: default: } } func (ss *stdioStatusSink) loop() { bar := pb.StartNew(100) bar.ShowSpeed = false bar.ShowCounters = false bar.ShowTimeLeft = false bar.SetMaxWidth(lineLength) A: for { select { case <-ss.closeChan: break A case <-ss.updateChan: ss.infoMutex.Lock() statusLine := ss.statusLine idx := strings.IndexByte(statusLine, '\n') if idx >= 0 { statusLine = statusLine[0:idx] } progress := ss.progress ss.infoMutex.Unlock() bar.Set(progress) bar.Postfix(" " + statusLine) } } //bar.Update() bar.Finish() close(ss.closedChan) } func (stdioInteractor) Status(c *StatusInfo) (StatusSink, error) { ss := &stdioStatusSink{ closeChan: make(chan struct{}), closedChan: make(chan struct{}), updateChan: make(chan struct{}, 10), statusLine: c.StatusLine, } ss.updateChan <- struct{}{} go ss.loop() return ss, nil } func (stdioInteractor) Prompt(c *Challenge) (*Response, error) { switch c.ResponseType { case RTAcknowledge: return stdioAcknowledge(c) case RTYesNo: return stdioYesNo(c) case RTLineString: return stdioLineString(c) case RTSelect: return stdioSelect(c) default: return nil, fmt.Errorf("unsupported challenge type") } } func stdioAcknowledge(c *Challenge) (*Response, error) { p := c.Prompt if p == "" { p = "Press Return to continue." } PrintStderrMessage(c.Title, fmt.Sprintf("%s\n\n%s", c.Body, p)) waitReturn() return &Response{}, nil } func PrintStderrMessage(title, body string) { fmt.Fprintf(os.Stderr, "%s\n%s\n", titleLine(title), wordwrap.WrapString(body, lineLength)) } func stdioYesNo(c *Challenge) (*Response, error) { p := c.Prompt if p == "" { p = "Continue?" } fmt.Fprintf(os.Stderr, `%s %s `, titleLine(c.Title), c.Body) yes := waitYN(p) return &Response{Cancelled: !yes}, nil } func stdioLineString(c *Challenge) (*Response, error) { p := c.Prompt if p == "" { p = ">" } PrintStderrMessage(c.Title, fmt.Sprintf("%s\n\n%s", c.Body, p)) v := waitLine() return &Response{Value: v}, nil } func stdioSelect(c *Challenge) (*Response, error) { p := c.Prompt if p == "" { p = ">" } PrintStderrMessage(c.Title, fmt.Sprintf("%s\n\n", c.Body)) for i, o := range c.Options { t := o.Title if t == "" { t = o.Value } fmt.Fprintf(os.Stderr, " %v) %s\n", i+1, t) } fmt.Fprintf(os.Stderr, "%s ", p) v := strings.TrimSpace(waitLine()) n, err := strconv.ParseUint(v, 10, 31) if err != nil || n == 0 || int(n-1) >= len(c.Options) { return stdioSelect(c) } return &Response{Value: c.Options[int(n-1)].Value}, nil } func waitReturn() { waitLine() } func waitLine() string { s, _ := bufio.NewReader(os.Stdin).ReadString('\n') return strings.TrimRight(s, "\r\n") } func waitYN(prompt string) bool { r := bufio.NewReader(os.Stdin) for { fmt.Fprintf(os.Stderr, "%s [Yn] ", prompt) s, _ := r.ReadString('\n') if v, ok := text.ParseBoolUserDefaultYes(s); ok { return v } } } const lineLength = 70 func repeat(n int) string { return "--------------------------------------------------------------------------------"[80-n:] } func titleLine(title string) string { if title != "" { title = " " + title + " " } n := lineLength/2 - len(title)/2 s := "\n\n" + repeat(n) + title if len(s) < lineLength { s += repeat(lineLength - len(s)) } return s } acme-0.0.62/redirector/000077500000000000000000000000001321256625400146545ustar00rootroot00000000000000acme-0.0.62/redirector/redirector.go000066400000000000000000000157231321256625400173550ustar00rootroot00000000000000// Package redirector provides a basic HTTP server for redirecting HTTP // requests to HTTPS requests and serving ACME HTTP challenge values. package redirector import ( "errors" "fmt" deos "github.com/hlandau/goutils/os" "github.com/hlandau/xlog" "gopkg.in/hlandau/svcutils.v1/chroot" "gopkg.in/hlandau/svcutils.v1/passwd" "gopkg.in/tylerb/graceful.v1" "html" "net" "net/http" "os" "sync/atomic" "time" ) var log, Log = xlog.New("acme.redirector") // 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)"` 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"` StatusCode int `default:"308" usage:"HTTP redirect status code"` } // Simple HTTP to HTTPS redirector. type Redirector struct { cfg Config httpServer graceful.Server httpListener net.Listener stopping uint32 } // Instantiate an HTTP to HTTPS redirector. func New(cfg Config) (*Redirector, error) { r := &Redirector{ cfg: cfg, httpServer: graceful.Server{ Timeout: 100 * time.Millisecond, NoSignalHandling: true, Server: &http.Server{ Addr: cfg.Bind, ReadTimeout: cfg.ReadTimeout, WriteTimeout: cfg.WriteTimeout, }, }, } // Try and make the challenge path if it doesn't exist. err := os.MkdirAll(r.cfg.ChallengePath, 0755) if err != nil { return nil, err } if r.cfg.ChallengeGID != "" { err := enforceGID(r.cfg.ChallengeGID, r.cfg.ChallengePath) if err != nil { return nil, err } } l, err := net.Listen("tcp", r.httpServer.Server.Addr) if err != nil { return nil, err } r.httpListener = l return r, nil } func enforceGID(gid, path string) error { newGID, err := passwd.ParseGID(gid) if err != nil { return err } // So this is a surprisingly complicated dance if we want to be free of // potentially hazardous race conditions. We have a path. We can't assume // anything about its ownership, or mode, whether it's a symlink, etc. // // The big risk is that someone is able to create a symlink pointing to // something they want to illicitly access. Note that since /var/run will // commonly be used and because this directory is world-writeable, ala /tmp, // this is a real risk. // // So we have to make sure we don't follow symlinks. Assume we are running // as root (necessary, since we're chowning), and that nothing running as // root is malicious. // // We open the directory as a file so we can modify it using that reference // without worrying about the resolution of the path changing under us. But // we need to make sure we don't follow symlinks. This requires special OS // support, alas. dir, err := deos.OpenNoSymlinks(path) if err != nil { return err } defer dir.Close() fi, err := dir.Stat() if err != nil { return err } // Attributes of the directory can still change, but its type certainly // can't. This guarantee is enough for our purposes. if (fi.Mode() & os.ModeType) != os.ModeDir { return fmt.Errorf("challenge path %#v is not a directory", path) } curUID, err := deos.GetFileUID(fi) if err != nil { return err } dir.Chmod((fi.Mode() | 0070) & ^os.ModeType) // Ignore errors. dir.Chown(curUID, newGID) // Ignore errors. return nil } func (r *Redirector) commonHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("Server", "acmetool-redirector") rw.Header().Set("Content-Security-Policy", "default-src 'none'") h.ServeHTTP(rw, req) }) } // Start the redirector. func (r *Redirector) Start() error { serveMux := http.NewServeMux() r.httpServer.Handler = r.commonHandler(serveMux) challengePath, ok := chroot.Rel(r.cfg.ChallengePath) if !ok { return fmt.Errorf("challenge path is not addressible inside chroot: %s", r.cfg.ChallengePath) } serveMux.HandleFunc("/", r.handleRedirect) serveMux.Handle("/.well-known/acme-challenge/", http.StripPrefix("/.well-known/acme-challenge/", http.FileServer(nolsDir(challengePath)))) go func() { err := r.httpServer.Serve(r.httpListener) if atomic.LoadUint32(&r.stopping) == 0 { log.Fatale(err, "serve") } }() log.Debugf("redirector running") return nil } // Stop the redirector. func (r *Redirector) Stop() error { atomic.StoreUint32(&r.stopping, 1) r.httpServer.Stop(r.httpServer.Timeout) <-r.httpServer.StopChan() return nil } // Respond to a request with a redirect. func (r *Redirector) handleRedirect(rw http.ResponseWriter, req *http.Request) { // Redirect. u := *req.URL u.Scheme = "https" if u.Host == "" { u.Host = req.Host } if u.Host == "" { rw.WriteHeader(400) return } us := u.String() rw.Header().Set("Location", us) // If we are receiving any cookies, these must be insecure cookies, ergo // cookies aren't being set securely properly. This is a security issue. // Deleting cookies after the fact doesn't change the fact that they were // sent in cleartext and are thus forever untrustworthy. But it increases // the probability of somebody noticing something is up. // // ... However, the HTTP specification makes it impossible to delete a cookie // unless we know its domain and path, which aren't transmitted in requests. if req.Method == "GET" { rw.Header().Set("Cache-Control", "public; max-age=31536000") rw.Header().Set("Content-Type", "text/html; charset=utf-8") } // This is a permanent redirect and the request method should be preserved. // It's unfortunate if the client has transmitted information in cleartext // via POST, etc., but there's nothing we can do about it at this stage. rw.WriteHeader(r.cfg.StatusCode) if req.Method == "GET" { // Redirects issued in response to GET SHOULD have a body pointing to the // new URL for clients which don't support redirects. (Whether the set of // clients supporting acceptably modern versions of TLS and not supporting // HTTP redirects is non-empty is another matter.) ue := html.EscapeString(us) rw.Write([]byte(fmt.Sprintf(redirBody, ue, ue))) } } const redirBody = ` Permanently Moved

Permanently Moved

This resource has moved permanently to %s.

` // Like http.Dir, but doesn't allow directory listings. type nolsDir string var errNoListing = errors.New("http: directory listing not allowed") func (d nolsDir) Open(name string) (http.File, error) { f, err := http.Dir(d).Open(name) if err != nil { return nil, err } fi, err := f.Stat() if err != nil { f.Close() return nil, err } if fi.IsDir() { f.Close() return nil, os.ErrNotExist } return f, nil } acme-0.0.62/redirector/redirector_test.go000066400000000000000000000025051321256625400204060ustar00rootroot00000000000000package redirector import ( denet "github.com/hlandau/goutils/net" "io/ioutil" "net/http" "os" "path/filepath" "testing" ) func TestRedirector(t *testing.T) { dir, err := ioutil.TempDir("", "acme-redirector-test") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) r, err := New(Config{ Bind: ":9847", ChallengePath: dir, }) if err != nil { t.Fatal(err) } err = r.Start() if err != nil { t.Fatal(err) } defer r.Stop() req, err := http.NewRequest("FROBNICATE", "http://127.0.0.1:9847/foo/bar?alpha=beta", nil) if err != nil { t.Fatal(err) } res, err := http.DefaultTransport.RoundTrip(req) if err != nil { t.Fatal(err) } defer res.Body.Close() loc := res.Header.Get("Location") if loc != "https://127.0.0.1:9847/foo/bar?alpha=beta" { t.Fatalf("wrong Location: %v", loc) } err = ioutil.WriteFile(filepath.Join(dir, "foo"), []byte("bar"), 0644) if err != nil { t.Fatal(err) } req, err = http.NewRequest("GET", "http://127.0.0.1:9847/.well-known/acme-challenge/foo", nil) if err != nil { t.Fatal(err) } res, err = http.DefaultTransport.RoundTrip(req) if err != nil { t.Fatal(err) } defer res.Body.Close() b, err := ioutil.ReadAll(denet.LimitReader(res.Body, 1*1024*1024)) if err != nil { t.Fatal(err) } if string(b) != "bar" { t.Fatal("wrong response") } } acme-0.0.62/responder/000077500000000000000000000000001321256625400145135ustar00rootroot00000000000000acme-0.0.62/responder/dns.go000066400000000000000000000031121321256625400156230ustar00rootroot00000000000000package responder import ( "crypto" "encoding/json" "fmt" "github.com/hlandau/acme/acmeapi/acmeutils" ) type DNSChallengeInfo struct { Body string } type dnsResponder struct { rcfg Config validation []byte dnsString string } func newDNSResponder(rcfg Config) (Responder, error) { s := &dnsResponder{ rcfg: rcfg, } var err error s.validation, err = acmeutils.ChallengeResponseJSON(rcfg.AccountKey, rcfg.Token, "dns-01") if err != nil { return nil, err } s.dnsString, err = acmeutils.DNSKeyAuthorization(rcfg.AccountKey, rcfg.Token) if err != nil { return nil, err } return s, nil } // Start is a no-op for the DNS method. func (s *dnsResponder) Start() error { // Try hooks. if startFunc := s.rcfg.ChallengeConfig.StartHookFunc; startFunc != nil { err := startFunc(&DNSChallengeInfo{ Body: s.dnsString, }) return err } return fmt.Errorf("DNS challenge not supported") } // Stop is a no-op for the DNS method. func (s *dnsResponder) Stop() error { // Try hooks. if stopFunc := s.rcfg.ChallengeConfig.StopHookFunc; stopFunc != nil { err := stopFunc(&DNSChallengeInfo{ Body: s.dnsString, }) log.Warne(err, "failed to uninstall DNS challenge via hook (ignoring)") return nil } return fmt.Errorf("DNS challenge not supported") } func (s *dnsResponder) RequestDetectedChan() <-chan struct{} { return nil } func (s *dnsResponder) Validation() json.RawMessage { return json.RawMessage(s.validation) } func (s *dnsResponder) ValidationSigningKey() crypto.PrivateKey { return nil } func init() { RegisterResponder("dns-01", newDNSResponder) } acme-0.0.62/responder/http.go000066400000000000000000000232271321256625400160270ustar00rootroot00000000000000package responder import ( "bytes" "crypto" "crypto/tls" "encoding/json" "fmt" "github.com/hlandau/acme/acmeapi/acmeutils" denet "github.com/hlandau/goutils/net" deos "github.com/hlandau/goutils/os" "gopkg.in/tylerb/graceful.v1" "io/ioutil" "net" "net/http" "net/url" "os" "path/filepath" "sort" "strconv" "strings" "sync" "time" ) type HTTPChallengeInfo struct { Filename string Body string } type httpResponder struct { rcfg Config serveMux *http.ServeMux response []byte requestDetectedChan chan struct{} stopFuncs []func() ka []byte validation []byte filePath string notifySupported bool // is notify supported? listening bool } func newHTTP(rcfg Config) (Responder, error) { s := &httpResponder{ rcfg: rcfg, serveMux: http.NewServeMux(), requestDetectedChan: make(chan struct{}, 1), notifySupported: true, } // Configure the HTTP server s.serveMux.HandleFunc("/.well-known/acme-challenge/"+rcfg.Token, s.handle) ka, err := acmeutils.KeyAuthorization(rcfg.AccountKey, rcfg.Token) if err != nil { return nil, err } s.ka = []byte(ka) s.validation, err = acmeutils.ChallengeResponseJSON(rcfg.AccountKey, rcfg.Token, "http-01") if err != nil { return nil, err } return s, nil } // HTTP handler. func (s *httpResponder) handle(rw http.ResponseWriter, req *http.Request) { // Send the precomputed response. rw.Header().Set("Content-Type", "text/plain") rw.Write(s.ka) s.notify() } func (s *httpResponder) notify() { // Notify callers that a request has been detected. select { case s.requestDetectedChan <- struct{}{}: default: } } // Start handling HTTP requests. func (s *httpResponder) Start() error { err := s.startActual() if err != nil { return err } if !s.rcfg.ChallengeConfig.HTTPNoSelfTest { log.Debug("http-01 self test") err = s.selfTest() if err != nil { log.Infoe(err, "http-01 self test failed") s.Stop() return err } } log.Debug("http-01 started") return nil } // This is currently the validation timeout used by Let's Encrypt, so let's // use the same value here. var selfTestTimeout = 5 * time.Second // Test that the challenge is reachable at the given hostname. If a hostname // was not provided, this test is skipped. func (s *httpResponder) selfTest() error { if s.rcfg.Hostname == "" { return nil } u := url.URL{ Scheme: "http", Host: s.rcfg.Hostname, Path: "/.well-known/acme-challenge/" + s.rcfg.Token, } trans := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, DisableKeepAlives: true, } client := &http.Client{ Transport: trans, Timeout: selfTestTimeout, } res, err := client.Get(u.String()) if err != nil { return err } defer res.Body.Close() if res.StatusCode != 200 { return fmt.Errorf("non-200 status code when doing self-test") } b, err := ioutil.ReadAll(denet.LimitReader(res.Body, 1*1024*1024)) if err != nil { return err } b = bytes.TrimSpace(b) if !bytes.Equal(b, s.ka) { return fmt.Errorf("got 200 response when doing self-test, but with the wrong data") } // If we detected a request, we support notifications, otherwise we don't. select { case <-s.requestDetectedChan: default: s.notifySupported = false } // Drain the notification channel in case we somehow made several requests. L: for { select { case <-s.requestDetectedChan: default: break L } } return nil } // Tries to write a challenge file to each of the directories. func webrootWriteChallenge(webroots map[string]struct{}, token string, ka []byte) { log.Debugf("writing %d webroot challenge files", len(webroots)) for wr := range webroots { os.MkdirAll(wr, 0755) // ignore errors fn := filepath.Join(wr, token) log.Debugf("writing webroot file %s", fn) // Because /var/run/acme/acme-challenge may not exist due to /var/run // possibly being a tmpfs, and because that tmpfs is likely to be world // writable, there is a risk of following a maliciously crafted symlink to // cause a file to be overwritten as root. Open the file using a // no-symlinks flag if the OS supports it, but only for /var/run paths; we // want to support symlinks for other paths, which are presumably properly // controlled. // // Unfortunately earlier components in the pathname will still be followed // if they are symlinks, but it looks like this is the best we can do. var f *os.File var err error if strings.HasPrefix(wr, "/var/run/") { f, err = deos.OpenFileNoSymlinks(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) } else { f, err = os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) } if err != nil { log.Infoe(err, "failed to open webroot file ", fn) continue } f.Write(ka) f.Close() } } // Tries to remove a challenge file from each of the directories. func webrootRemoveChallenge(webroots map[string]struct{}, token string) { for wr := range webroots { fn := filepath.Join(wr, token) log.Debugf("removing webroot file %s", fn) os.Remove(fn) // ignore errors } } // The standard webroot path, into which the responder always tries to install // challenges, not necessarily successfully. This is intended to be a standard, // system-wide path to look for challenges at. On POSIX-like systems, it is // usually "/var/run/acme/acme-challenge". var StandardWebrootPath string func init() { if StandardWebrootPath == "" { StandardWebrootPath = "/var/run/acme/acme-challenge" } } func (s *httpResponder) getWebroots() map[string]struct{} { webroots := map[string]struct{}{} for _, p := range s.rcfg.ChallengeConfig.WebPaths { if p != "" { webroots[strings.TrimRight(p, "/")] = struct{}{} } } // The webroot and redirector models both require us to drop the challenge at // a given path. If a webroot is not specified in the configuration, use an // ephemeral default that the redirector might be using anyway. webroots[StandardWebrootPath] = struct{}{} return webroots } func parseListenAddrs(addrs []string) map[string]struct{} { m := map[string]struct{}{} for _, s := range addrs { n, err := strconv.ParseUint(s, 10, 16) if err == nil { m[fmt.Sprintf("[::1]:%d", n)] = struct{}{} m[fmt.Sprintf("127.0.0.1:%d", n)] = struct{}{} continue } ta, err := net.ResolveTCPAddr("tcp", s) if err != nil { log.Warnf("invalid listen addr: %q: %v", s, err) continue } m[ta.String()] = struct{}{} } return m } func addrWeight(x string) int { host, _, err := net.SplitHostPort(x) if err != nil { return 0 } if host == "" { return -1 } ip := net.ParseIP(host) if ip != nil && ip.IsUnspecified() { if ip.To4() != nil { return -1 } return -2 } return 0 } type addrSorter []string func (a addrSorter) Len() int { return len(a) } func (a addrSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a addrSorter) Less(i, j int) bool { return addrWeight(a[i]) < addrWeight(a[j]) } func determineListenAddrs(userAddrs []string) []string { // Here's our brute force method: listen on everything that might work. addrs := parseListenAddrs(userAddrs) addrs["[::]:80"] = struct{}{} // OpenBSD addrs[":80"] = struct{}{} addrs["[::1]:402"] = struct{}{} addrs["127.0.0.1:402"] = struct{}{} addrs["[::1]:4402"] = struct{}{} addrs["127.0.0.1:4402"] = struct{}{} // Sort the strings so that 'all interfaces' addresses appear first, so that // they are not blocked by more specific entries such as the ones above, // which are always attempted. var addrsl []string for k := range addrs { addrsl = append(addrsl, k) } sort.Stable(addrSorter(addrsl)) return addrsl } func (s *httpResponder) startActual() error { // Determine and listen on sorted list of addresses. addrs := determineListenAddrs(s.rcfg.ChallengeConfig.HTTPPorts) for _, a := range addrs { s.startListener(a) } // Even if none of the listeners managed to start, the webroot or redirector // methods might work. webrootWriteChallenge(s.getWebroots(), s.rcfg.Token, s.ka) // Try hooks. if startFunc := s.rcfg.ChallengeConfig.StartHookFunc; startFunc != nil { err := startFunc(&HTTPChallengeInfo{ Filename: s.rcfg.Token, Body: string(s.ka), }) log.Errore(err, "start challenge hook") } return nil } func (s *httpResponder) startListener(addr string) error { svr := &graceful.Server{ NoSignalHandling: true, Server: &http.Server{ Addr: addr, Handler: s.serveMux, }, } l, err := net.Listen("tcp", svr.Addr) if err != nil { log.Debuge(err, "failed to listen on ", svr.Addr) return err } log.Debugf("listening on %v", svr.Addr) go func() { defer l.Close() svr.Serve(l) }() stopFunc := func() { svr.Stop(10 * time.Millisecond) <-svr.StopChan() } s.stopFuncs = append(s.stopFuncs, stopFunc) return nil } // Stop handling HTTP requests. func (s *httpResponder) Stop() error { var wg sync.WaitGroup wg.Add(len(s.stopFuncs)) call := func(f func()) { defer wg.Done() f() } for _, f := range s.stopFuncs { go call(f) } wg.Wait() s.stopFuncs = nil // Try and remove challenges. webrootRemoveChallenge(s.getWebroots(), s.rcfg.Token) // Try and stop hooks. if stopFunc := s.rcfg.ChallengeConfig.StopHookFunc; stopFunc != nil { err := stopFunc(&HTTPChallengeInfo{ Filename: s.rcfg.Token, Body: string(s.ka), }) log.Errore(err, "stop challenge hook") } return nil } func (s *httpResponder) RequestDetectedChan() <-chan struct{} { if !s.notifySupported { return nil } return s.requestDetectedChan } func (s *httpResponder) Validation() json.RawMessage { return json.RawMessage(s.validation) } func (s *httpResponder) ValidationSigningKey() crypto.PrivateKey { return nil } func init() { RegisterResponder("http-01", newHTTP) } acme-0.0.62/responder/possession.go000066400000000000000000000036271321256625400172570ustar00rootroot00000000000000package responder import ( "crypto" "crypto/x509" "encoding/json" "fmt" "github.com/hlandau/acme/acmeapi" "gopkg.in/square/go-jose.v1" ) type proofOfPossessionResponder struct { validation []byte pk crypto.PrivateKey } func (rcfg *Config) findAcceptablePrivateKey() (crypto.PrivateKey, error) { for _, der := range rcfg.AcceptableCertificates { crt, err := x509.ParseCertificate(der) if err != nil { continue } if rcfg.ChallengeConfig.PriorKeyFunc == nil { continue } pk, err := rcfg.ChallengeConfig.PriorKeyFunc(crt.PublicKey) if err != nil { return nil, err } if pk != nil { return pk, nil } } return nil, nil } func newProofOfPossessionResponder(rcfg Config) (Responder, error) { if rcfg.Hostname == "" { return nil, fmt.Errorf("hostname is required for proofOfPossession") } pk, err := rcfg.findAcceptablePrivateKey() if err != nil { return nil, err } if pk == nil { return nil, fmt.Errorf("no acceptable private keys could be found") } r := &proofOfPossessionResponder{ pk: pk, } info := map[string]interface{}{ "resource": "challenge", "type": "proofOfPossession", "identifiers": []acmeapi.Identifier{ { Type: "dns", Value: rcfg.Hostname, }, }, "accountKey": &jose.JsonWebKey{ Key: rcfg.AccountKey, }, } r.validation, err = json.Marshal(&info) if err != nil { return nil, err } return r, nil } func (r *proofOfPossessionResponder) Start() error { return nil } func (r *proofOfPossessionResponder) Stop() error { return nil } func (r *proofOfPossessionResponder) RequestDetectedChan() <-chan struct{} { return nil } func (r *proofOfPossessionResponder) Validation() json.RawMessage { return json.RawMessage(r.validation) } func (r *proofOfPossessionResponder) ValidationSigningKey() crypto.PrivateKey { return r.pk } func init() { RegisterResponder("proofOfPossession", newProofOfPossessionResponder) } acme-0.0.62/responder/responder.go000066400000000000000000000074501321256625400170510ustar00rootroot00000000000000// Package responder implements the various ACME challenge types. package responder import ( "crypto" "encoding/json" "fmt" "github.com/hlandau/xlog" ) // Log site. var log, Log = xlog.New("acme.responder") // A Responder implements a challenge type. // // After successfully instantiating a responder, you should call Start. // // You should then use the return values of Validation() and // ValidationSigningKey() to submit the challenge response. // // Once the challenge has been completed, as determined by polling, you must // call Stop. If RequestDetectedChan() is non-nil, it provides a hint as to // when polling may be fruitful. type Responder interface { // Become ready to be interrogated by the ACME server. Start() error // Stop responding to any queries by the ACME server. Stop() error // This channel is sent to when a request to the responder is detected, // which may indicates completion of the challenge is imminent. // // Returning nil indicates that request detection is not supported. RequestDetectedChan() <-chan struct{} // Return the validation object the signature for which was delivered. If // nil is returned, no validation object is submitted. Validation() json.RawMessage // Key which must sign validation object. If nil, account key is used. ValidationSigningKey() crypto.PrivateKey } // Used to instantiate a responder. type Config struct { // Information about the challenge to be completed. Type string // The responder type to be used. e.g. "http-01". AccountKey crypto.PrivateKey // The account private key. Token string // The challenge token. // "http-01", "proofOfPossession": The hostname being verified. May be used // for pre-initiation self-testing. Optional. Required for // proofOfPossession. Hostname string // "proofOfPossession": The certificates which are acceptable. Each entry is // a DER X.509 certificate. AcceptableCertificates [][]byte ChallengeConfig ChallengeConfig } // Information used to complete challenges, other than information provided by // the ACME server. type ChallengeConfig struct { // "http-01": The http responder may attempt to place challenges in these // locations. Optional. WebPaths []string // "http-01": The http responder may attempt to listen on these addresses. // Optional. HTTPPorts []string // Do not perform self test, but assume challenge is completable. HTTPNoSelfTest bool // "proofOfPossession": Function which returns the private key for a given // public key. This may be called multiple times for a given challenge as // multiple public keys may be permitted. If a private key for the given // public key cannot be found, return nil and do not return an error. // Returning an error short circuits. // // If not specified, proofOfPossession challenges always fail. PriorKeyFunc PriorKeyFunc StartHookFunc HookFunc StopHookFunc HookFunc } // Returns the private key corresponding to the given public key, if it can be // found. If a corresponding private key cannot be found, return nil; do not // return an error. Returning an error short circuits. type PriorKeyFunc func(crypto.PublicKey) (crypto.PrivateKey, error) type HookFunc func(challengeInfo interface{}) error var responderTypes = map[string]func(Config) (Responder, error){} // Try and instantiate a responder using the given configuration. func New(rcfg Config) (Responder, error) { f, ok := responderTypes[rcfg.Type] if !ok { return nil, fmt.Errorf("challenge type not supported") } return f(rcfg) } // Register a responder type. Allows types other than those innately supported // by this package to be supported. Overrides any previously registered // responder of the same type. func RegisterResponder(typeName string, createFunc func(Config) (Responder, error)) { responderTypes[typeName] = createFunc } acme-0.0.62/responder/tlssni.go000066400000000000000000000115441321256625400163630ustar00rootroot00000000000000package responder import ( "crypto" "crypto/tls" "encoding/json" "fmt" "github.com/hlandau/acme/acmeapi/acmeutils" "net" "strings" ) type TLSSNIChallengeInfo struct { Hostname1, Hostname2 string // must appear in certificate Certificate []byte Key crypto.PrivateKey } type tlssniResponder struct { requestDetectedChan chan struct{} notifySupported bool rcfg Config stoppedChan chan struct{} cfg *tls.Config l net.Listener validation []byte validationHostname string cert []byte privateKey crypto.PrivateKey } func newTLSSNIResponder(rcfg Config) (Responder, error) { r := &tlssniResponder{ rcfg: rcfg, requestDetectedChan: make(chan struct{}, 1), stoppedChan: make(chan struct{}), notifySupported: true, } // Validation hostname. var err error r.validationHostname, err = acmeutils.TLSSNIHostname(rcfg.AccountKey, rcfg.Token) if err != nil { return nil, err } // Certificate and private key. r.cert, r.privateKey, err = acmeutils.CreateTLSSNICertificate(r.validationHostname) if err != nil { return nil, err } c := &tls.Certificate{ Certificate: [][]byte{r.cert}, PrivateKey: r.privateKey, } r.cfg = &tls.Config{ Certificates: []tls.Certificate{*c}, } // Validation response. r.validation, err = acmeutils.ChallengeResponseJSON(rcfg.AccountKey, rcfg.Token, "tls-sni-01") if err != nil { return nil, err } return r, nil } // Internal use only. This can be used to change the port the TLSSNI responder // listens on for development purposes. var InternalTLSSNIPort uint16 = 443 func (r *tlssniResponder) Start() error { listenErr := r.startListener() log.Debuge(listenErr, "failed to start TLS-SNI listener") // Try hooks. var hookErr error if startFunc := r.rcfg.ChallengeConfig.StartHookFunc; startFunc != nil { hookErr = startFunc(&TLSSNIChallengeInfo{ Hostname1: r.validationHostname, Hostname2: r.validationHostname, Certificate: r.cert, Key: r.privateKey, }) log.Debuge(hookErr, "failed to install TLS-SNI challenge via hook") } if listenErr != nil && hookErr != nil { return listenErr } err := r.selfTest() if err != nil { log.Debuge(err, "tls-sni-01 self-test failed") r.Stop() return err } return nil } func (r *tlssniResponder) startListener() error { l, err := tls.Listen("tcp", fmt.Sprintf(":%d", InternalTLSSNIPort), r.cfg) if err != nil { return err } r.l = l go func() { defer close(r.stoppedChan) defer l.Close() for { c, err := l.Accept() if err != nil { break } c.(*tls.Conn).Handshake() // Ignore error c.Close() r.notify() } }() return nil } func (r *tlssniResponder) Stop() error { if r.l != nil { r.l.Close() <-r.stoppedChan r.l = nil } // Try hooks. if stopFunc := r.rcfg.ChallengeConfig.StopHookFunc; stopFunc != nil { err := stopFunc(&TLSSNIChallengeInfo{ Hostname1: r.validationHostname, Hostname2: r.validationHostname, Certificate: r.cert, Key: r.privateKey, }) log.Errore(err, "failed to uninstall TLS-SNI challenge via hook") } return nil } func containsHostname(hostname string, hostnames []string) bool { for _, x := range hostnames { if strings.TrimSuffix(strings.ToLower(x), ".") == hostname { return true } } return false } func (r *tlssniResponder) selfTest() error { if r.rcfg.Hostname == "" { return nil } conn, err := tls.Dial("tcp", net.JoinHostPort(r.rcfg.Hostname, fmt.Sprintf("%d", InternalTLSSNIPort)), &tls.Config{ ServerName: r.validationHostname, InsecureSkipVerify: true, }) if err != nil { return err } defer conn.Close() err = conn.Handshake() if err != nil { return err } certs := conn.ConnectionState().PeerCertificates if len(certs) != 1 { return fmt.Errorf("when doing self-test, got %d certificates, expected 1", len(certs)) } if !containsHostname(r.validationHostname, certs[0].DNSNames) { return fmt.Errorf("certificate does not contain expected challenge name") } // If we detected a request, we support notifications, otherwise we don't. select { case <-r.requestDetectedChan: default: r.notifySupported = false } // Drain the notification channel in case we somehow made several requests. L: for { select { case <-r.requestDetectedChan: default: break L } } return nil } func (r *tlssniResponder) notify() { select { case r.requestDetectedChan <- struct{}{}: default: } } func (r *tlssniResponder) RequestDetectedChan() <-chan struct{} { if !r.notifySupported { return nil } return r.requestDetectedChan } func (r *tlssniResponder) Validation() json.RawMessage { return json.RawMessage(r.validation) } func (r *tlssniResponder) ValidationSigningKey() crypto.PrivateKey { return nil } func init() { RegisterResponder("tls-sni-01", newTLSSNIResponder) } acme-0.0.62/solver/000077500000000000000000000000001321256625400140245ustar00rootroot00000000000000acme-0.0.62/solver/preference.go000066400000000000000000000054321321256625400164750ustar00rootroot00000000000000package solver import ( "github.com/hlandau/acme/acmeapi" "sort" ) // Any challenge having a preference at or below this value will never be used. const NonviableThreshold int32 = -1000000 // Sorter. type sorter struct { authz *acmeapi.Authorization preferencer Preferencer } func (s *sorter) Len() int { return len(s.authz.Combinations) } func (s *sorter) Swap(i, j int) { s.authz.Combinations[i], s.authz.Combinations[j] = s.authz.Combinations[j], s.authz.Combinations[i] } func (s *sorter) Less(i, j int) bool { pi := s.preference(s.authz.Combinations[i]...) pj := s.preference(s.authz.Combinations[j]...) return pi < pj } func (s *sorter) preference(idx ...int) int32 { p := int32(0) for _, i := range idx { if i >= len(s.authz.Challenges) || p <= NonviableThreshold { return NonviableThreshold } v := s.preferencer.Preference(s.authz.Challenges[i]) p = satAdd(p, v) } return p } func satAdd(x, y int32) int32 { v := int64(x) + int64(y) if v > int64(-NonviableThreshold) { return -NonviableThreshold } if v < int64(NonviableThreshold) { return NonviableThreshold } return int32(v) } // TypePreferencer returns a preference according to the type of the challenge. // // Unknown challenge types are nonviable. type TypePreferencer map[string]int32 // Implements Preferencer. func (p TypePreferencer) Preference(ch *acmeapi.Challenge) int32 { v, ok := p[ch.Type] if !ok { return NonviableThreshold } return v } // Returns a copy of TypePreferencer, so that it can be mutated without // changing the original. func (p TypePreferencer) Copy() TypePreferencer { tp := TypePreferencer{} for k, v := range p { tp[k] = v } return tp } // PreferFast prefers fast types. var PreferFast = TypePreferencer{ "tls-sni-01": 1, "http-01": 0, // Disable DNS challenges for now. They're practically unusable and the Let's // Encrypt live server doesn't support them at this time anyway. "dns-01": -10, // Avoid unless necessary. In future we might want to determine whether we // have a key and prefer this accordingly. "proofOfPossession:": -40, } // Determines the degree to which a challenge is preferred. Higher values are // more preferred. Any value <= NonviableThreshold will never be used. type Preferencer interface { // Get the preference for the given challenge. Preference(ch *acmeapi.Challenge) int32 } // Sort authorization combinations by preference. Crops Combinations to viable // combinations. func SortCombinations(authz *acmeapi.Authorization, preferencer Preferencer) { s := sorter{ authz: authz, preferencer: preferencer, } sort.Stable(sort.Reverse(&s)) for i := range authz.Combinations { pi := s.preference(authz.Combinations[i]...) if pi <= NonviableThreshold { authz.Combinations = authz.Combinations[0:i] return } } } acme-0.0.62/solver/register.go000066400000000000000000000055121321256625400162020ustar00rootroot00000000000000package solver import ( "fmt" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/acme/interaction" "golang.org/x/net/context" "net/mail" ) // Using the given client and interactor (or interaction.Auto if nil), register // the client account if it does not already exist. // // The interactor is used to prompt for terms of service agreement, if // agreement has not already been obtained. An e. mail address is prompted for. func AssistedUpsertRegistration(cl *acmeapi.Client, interactor interaction.Interactor, ctx context.Context) error { interactor = defaultInteraction(interactor) email := "" reg := &acmeapi.Registration{} agreementURIs := map[string]struct{}{} for { err := cl.AgreeRegistration(reg, agreementURIs, ctx) if err != nil { if e, ok := err.(*acmeapi.AgreementError); ok { res, err := interactor.Prompt(&interaction.Challenge{ Title: "Terms of Service Agreement Required", YesLabel: "I Agree", NoLabel: "Cancel", ResponseType: interaction.RTYesNo, UniqueID: "acme-agreement:" + e.URI, Prompt: "Do you agree to the Terms of Service?", Body: fmt.Sprintf(`You must agree to the terms of service at the following URL to continue: %s Do you agree to the terms of service set out in the above document?`, e.URI), }) if err != nil { return err } if !res.Cancelled { if email == "" { email, err = getEmail(interactor) if err != nil { return err } if email == "-" { return fmt.Errorf("e. mail input cancelled") } } reg.AgreementURI = e.URI agreementURIs[e.URI] = struct{}{} if email != "" { reg.ContactURIs = []string{"mailto:" + email} } continue } } } return err } } func getEmail(interactor interaction.Interactor) (string, error) { for { res, err := interactor.Prompt(&interaction.Challenge{ Title: "E. Mail Address Required", ResponseType: interaction.RTLineString, Prompt: "E. mail address: ", Body: `Please enter an e. mail address where you can be reached. Although entering an e. mail address is optional, it is highly recommended.`, UniqueID: "acme-enter-email", }) if err != nil { return "", err } if res.Value == "" { return "", nil } if res.Cancelled { return "-", nil } addr, err := mail.ParseAddress(res.Value) if err != nil { if res.Noninteractive { // If the e. mail address specified was invalid but we received it from // a noninteractive source, don't loop or we will loop forever. Instead // just act like one wasn't specified. return "", nil } continue } return addr.Address, nil } } func defaultInteraction(interactor interaction.Interactor) interaction.Interactor { if interactor == nil { return interaction.Auto } return interactor } acme-0.0.62/solver/respond.go000066400000000000000000000111641321256625400160300ustar00rootroot00000000000000// Package solver figures out how to complete authorizations and completes them // by instantiating responders. package solver import ( "fmt" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/acme/responder" denet "github.com/hlandau/goutils/net" "github.com/hlandau/xlog" "golang.org/x/net/context" "time" ) var log, Log = xlog.New("acme.solver") // Returned if all combinations fail. var ErrFailedAllCombinations = fmt.Errorf("failed all combinations") type authState struct { c *acmeapi.Client dnsName string ccfg responder.ChallengeConfig ctx context.Context pref TypePreferencer webPaths []string listenAddrs []string priorKeyFunc responder.PriorKeyFunc } // Attempts to authorize a hostname using the given client. webPaths and // priorKeyFunc are passed to responders. Returns the successfully validated // authorization on success. func Authorize(c *acmeapi.Client, dnsName string, ccfg responder.ChallengeConfig, ctx context.Context) (*acmeapi.Authorization, error) { as := authState{ c: c, dnsName: dnsName, ctx: ctx, pref: PreferFast.Copy(), ccfg: ccfg, } for { az, fatal, err := as.authorize() if err == nil { return az, nil } if fatal { return nil, err } } } func (as *authState) authorize() (az *acmeapi.Authorization, fatal bool, err error) { az, err = as.c.NewAuthorization(as.dnsName, as.ctx) if err != nil { return nil, true, err } SortCombinations(az, as.pref) for _, com := range az.Combinations { invalidated, err := as.attemptCombination(az, com) if err != nil { if !invalidated { continue } // The combination failed and failed challenge types have been removed // from the preference map. Assume that a newly created authorization // will offer the same combinations. So, if we still don't have a // viable combination, we fail here rather than creating an authorization // that won't get used. if !as.haveAnyViableCombinations(az) { break } return nil, false, err } return az, false, nil } return nil, true, ErrFailedAllCombinations } func (as *authState) haveAnyViableCombinations(az *acmeapi.Authorization) bool { for _, com := range az.Combinations { for _, i := range com { ch := az.Challenges[i] p, ok := as.pref[ch.Type] if ok && p > NonviableThreshold { return true } } } return false } func (as *authState) attemptCombination(az *acmeapi.Authorization, combination []int) (invalidated bool, err error) { for _, i := range combination { ch := az.Challenges[i] invalidated, err := CompleteChallenge(as.c, ch, as.dnsName, as.ccfg, as.ctx) if err != nil { delete(as.pref, ch.Type) return invalidated, err } } return false, nil } // Completes a given challenge, polling it until it is complete. Can be // cancelled using ctx. // // dnsName is the hostname which is being authorized. webPaths and priorKeyFunc // are passed to responders. // // The return value indicates whether the whole authorization has been invalidated // (set to "failed" status) as a result of an error. In this case a new authorization // must be created. func CompleteChallenge(c *acmeapi.Client, ch *acmeapi.Challenge, dnsName string, ccfg responder.ChallengeConfig, ctx context.Context) (invalidated bool, err error) { log.Debugf("attempting challenge type %s", ch.Type) var certs [][]byte for _, c := range ch.Certs { certs = append(certs, c) } r, err := responder.New(responder.Config{ Type: ch.Type, Token: ch.Token, AccountKey: c.AccountKey, Hostname: dnsName, AcceptableCertificates: certs, ChallengeConfig: ccfg, }) if err != nil { log.Debuge(err, "challenge instantiation failed") return false, err } err = r.Start() if err != nil { log.Debuge(err, "challenge start failed") return false, err } defer r.Stop() err = c.RespondToChallenge(ch, r.Validation(), r.ValidationSigningKey(), ctx) if err != nil { return false /* ??? */, err } b := denet.Backoff{ InitialDelay: 5 * time.Second, MaxDelay: 30 * time.Second, } for { log.Debug("waiting to poll challenge") select { case <-ctx.Done(): return true, ctx.Err() case <-r.RequestDetectedChan(): log.Debug("request detected") case <-time.After(b.NextDelay()): } log.Debug("querying challenge status") err := c.WaitLoadChallenge(ch, ctx) if err != nil { return false, err } if ch.Status.Final() { log.Debug("challenge now in final state") break } } if ch.Status != "valid" { return true, fmt.Errorf("challenge failed with status %#v", ch.Status) } return false, nil } acme-0.0.62/storage/000077500000000000000000000000001321256625400141565ustar00rootroot00000000000000acme-0.0.62/storage/abs.go000066400000000000000000000041301321256625400152500ustar00rootroot00000000000000package storage import ( "crypto" "errors" ) // Abstract storage interface. type Store interface { Close() error // Closes the database. Reload() error // Reloads the database from disk. Path() string // ACME state directory path. // These methods find an object by its identifier. Returns nil if the object // is not found. AccountByID(accountID string) *Account AccountByDirectoryURL(directoryURL string) *Account CertificateByID(certificateID string) *Certificate KeyByID(keyID string) *Key TargetByFilename(filename string) *Target DefaultTarget() *Target // Returns the default target. PreferredCertificateForHostname(hostname string) (*Certificate, error) VisitPreferredCertificates(func(hostname string, c *Certificate) error) error // The Visit methods call the given function for each known object of the // given type. Returning an error short-circuits. VisitAccounts(func(*Account) error) error VisitCertificates(func(*Certificate) error) error VisitKeys(func(*Key) error) error VisitTargets(func(*Target) error) error // Mutators. SaveTarget(*Target) error // Saves a target. RemoveTarget(filename string) error // Remove a target from the database. SaveCertificate(*Certificate) error // Saves certificate information. SaveAccount(*Account) error // Save account information. // Erase a whole certificate directory including URL, certificates, etc. RemoveCertificate(certificateID string) error // Erase a private key directory. RemoveKey(keyID string) error ImportKey(privateKey crypto.PrivateKey) (*Key, error) // Imports the key if it isn't already imported. ImportAccount(directoryURL string, privateKey crypto.PrivateKey) (*Account, error) // Imports an account key if it isn't already imported. ImportCertificate(url string) (*Certificate, error) // Imports a certificate if it isn't already imported. SetPreferredCertificateForHostname(hostname string, c *Certificate) error WriteMiscellaneousConfFile(filename string, data []byte) error } var StopVisiting = errors.New("[stop visiting]") acme-0.0.62/storage/config.go000066400000000000000000000040161321256625400157530ustar00rootroot00000000000000package storage import ( "crypto/elliptic" "github.com/hlandau/acme/fdb" "strings" ) // Legacy Configuration func (s *fdbStore) loadWebrootPaths() { if len(s.defaultTarget.Request.Challenge.WebrootPaths) != 0 { // Path list in default target file takes precedence. return } webrootPath, _ := fdb.String(s.db.Collection("conf").Open("webroot-path")) // ignore errors webrootPath = strings.TrimSpace(webrootPath) webrootPaths := strings.Split(webrootPath, "\n") for i := range webrootPaths { webrootPaths[i] = strings.TrimSpace(webrootPaths[i]) } if len(webrootPaths) == 1 && webrootPaths[0] == "" { webrootPaths = nil } s.defaultTarget.Request.Challenge.WebrootPaths = webrootPaths } func (s *fdbStore) loadRSAKeySize() { if s.defaultTarget.Request.Key.RSASize != 0 { // setting in default target file takes precedence return } n, err := fdb.Uint(s.db.Collection("conf"), "rsa-key-size", 31) if err != nil { return } s.defaultTarget.Request.Key.RSASize = int(n) if nn := clampRSAKeySize(int(n)); nn != int(n) { log.Warnf("An RSA key size of %d is not supported; must have %d <= size <= %d; clamping at %d", n, minRSASize, maxRSASize, nn) } } // Key Parameters const ( minRSASize = 2048 defaultRSASize = 2048 maxRSASize = 4096 ) func clampRSAKeySize(sz int) int { if sz == 0 { return defaultRSASize } if sz < minRSASize { return minRSASize } if sz > maxRSASize { return maxRSASize } return sz } const defaultCurve = "nistp256" // Make sure the curve name is valid and use a default curve name. "clamp" is // not the sanest name here but is consistent with clampRSAKeySize. func clampECDSACurve(curveName string) string { switch curveName { case "nistp256", "nistp384", "nistp521": return curveName default: return defaultCurve } } func getECDSACurve(curveName string) elliptic.Curve { switch clampECDSACurve(curveName) { case "nistp256": return elliptic.P256() case "nistp384": return elliptic.P384() case "nistp521": return elliptic.P521() default: return nil } } acme-0.0.62/storage/neuter.go000066400000000000000000000030461321256625400160120ustar00rootroot00000000000000package storage // In some cases it is desirable to load configuration information such as the // default target file, but very undesirable to load sensitive information such // as private keys. For example, the HTTP to HTTPS redirector is a public-facing // service and as such, is run privilege-dropped and chrooted for mitigation // purposes in the unlikely event that a vulnerability is identified in this // program or its dependencies, each written in a memory-safe language. // However, this could all be for nought if extremely valuable data such as // private keys is kept in process memory after dropping privileges. It is // therefore essential that private keys NEVER touch the memory of an acmetool // process launched to serve as a redirector. // // Hence this function. Calling this function neuters the storage package. // Neuter does two things: // // - It panics if a storage instance has ever been created in this process // before the first call to Neuter. // // - It changes the behaviour of the storage package so that all future loads // of state directories load configuration information, but no private keys. // // Thus, once Neuter has returned, this is essentially a guarantee that no // private keys ever have been or ever will be loaded into the process. A call // to Neuter cannot be reversed except by starting a new process. func Neuter() { if hasTouchedSensitiveData { panic("cannot neuter storage package after it has already been used") } isNeutered = true } var isNeutered = false var hasTouchedSensitiveData = false acme-0.0.62/storage/storage-fdb.go000066400000000000000000000460551321256625400167140ustar00rootroot00000000000000// Package storage implements the state directory specification, providing // a logical API access layer. package storage import ( "crypto" "crypto/x509" "fmt" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/acme/acmeapi/acmeutils" "github.com/hlandau/acme/fdb" "github.com/hlandau/xlog" "gopkg.in/yaml.v2" "io" "io/ioutil" "os" "strings" "time" ) var log, Log = xlog.New("acme.storage") // ACME client store. {{{1 type fdbStore struct { db *fdb.DB path string certs map[string]*Certificate // key: certificate ID accounts map[string]*Account // key: account ID keys map[string]*Key // key: key ID targets map[string]*Target // key: target filename preferred map[string]*Certificate // key: hostname defaultTarget *Target // from conf } func (s *fdbStore) WriteMiscellaneousConfFile(filename string, data []byte) error { return fdb.WriteBytes(s.db.Collection("conf"), filename, data) } // Trivial accessors. {{{1 func (s *fdbStore) AccountByID(accountID string) *Account { return s.accounts[accountID] } func (s *fdbStore) AccountByDirectoryURL(directoryURL string) *Account { for _, a := range s.accounts { if a.MatchesURL(directoryURL) { return a } } return nil } func (s *fdbStore) VisitAccounts(f func(a *Account) error) error { for _, a := range s.accounts { err := f(a) if err != nil { return err } } return nil } func (s *fdbStore) CertificateByID(certificateID string) *Certificate { return s.certs[certificateID] } func (s *fdbStore) VisitCertificates(f func(c *Certificate) error) error { for _, c := range s.certs { err := f(c) if err != nil { return err } } return nil } func (s *fdbStore) TargetByFilename(filename string) *Target { return s.targets[filename] } func (s *fdbStore) VisitTargets(f func(t *Target) error) error { for _, t := range s.targets { err := f(t) if err != nil { return err } } return nil } // Return the default target. Persist changes to the default target by calling SaveTarget. func (s *fdbStore) DefaultTarget() *Target { return s.defaultTarget } func (s *fdbStore) KeyByID(keyID string) *Key { return s.keys[keyID] } func (s *fdbStore) VisitKeys(f func(k *Key) error) error { for _, k := range s.keys { err := f(k) if err != nil { return err } } return nil } func (s *fdbStore) loadPreferred() error { s.preferred = map[string]*Certificate{} c := s.db.Collection("live") links, err := c.List() if err != nil { return err } for _, linkName := range links { link, err := c.ReadLink(linkName) if err != nil { return err } certID := link.Target[6:] cert := s.CertificateByID(certID) if cert == nil { // This should never happen because fdb checks symlinks, though maybe if // there was an empty certificate directory... return fmt.Errorf("unknown certificate: %q", certID) } s.preferred[linkName] = cert } return nil } func (s *fdbStore) VisitPreferredCertificates(f func(hostname string, c *Certificate) error) error { for hostname, c := range s.preferred { err := f(hostname, c) if err != nil { return err } } return nil } func (s *fdbStore) PreferredCertificateForHostname(hostname string) (*Certificate, error) { c := s.preferred[hostname] if c == nil { return nil, fmt.Errorf("not found: %q", hostname) } return c, nil } func (s *fdbStore) SetPreferredCertificateForHostname(hostname string, c *Certificate) error { err := s.db.Collection("live").WriteLink(hostname, fdb.Link{Target: "certs/" + c.ID()}) if err != nil { return err } s.preferred[hostname] = c return nil } // Default paths and permissions. {{{1 // The recommended path is the hardcoded, default, recommended path to be used // for a system-wide state storage directory. It may vary by system and // platform. On most POSIX-like systems, it is "/var/lib/acme". Specific builds // might customise it. var RecommendedPath string func init() { // Allow the path to be overridden at build time. if RecommendedPath == "" { RecommendedPath = "/var/lib/acme" } } var storePermissions = []fdb.Permission{ {Path: ".", DirMode: 0755, FileMode: 0644}, {Path: "accounts", DirMode: 0700, FileMode: 0600}, {Path: "desired", DirMode: 0755, FileMode: 0644}, {Path: "live", DirMode: 0755, FileMode: 0644}, {Path: "certs", DirMode: 0755, FileMode: 0644}, {Path: "certs/*/haproxy", DirMode: 0700, FileMode: 0600}, // hack for HAProxy {Path: "keys", DirMode: 0700, FileMode: 0600}, {Path: "conf", DirMode: 0755, FileMode: 0644}, {Path: "tmp", DirMode: 0700, FileMode: 0600}, } // Initialization and loading. {{{1 // Create a new client store using the given path. func NewFDB(path string) (Store, error) { if path == "" { path = RecommendedPath } db, err := fdb.Open(fdb.Config{ Path: path, Permissions: storePermissions, PermissionsPath: "conf/perm", }) if err != nil { return nil, err } s := &fdbStore{ db: db, path: path, } err = s.Reload() if err != nil { return nil, err } return s, nil } // Close the store. func (s *fdbStore) Close() error { return nil } // State directory path. func (s *fdbStore) Path() string { return s.path } // Reload from disk. func (s *fdbStore) Reload() error { if !isNeutered { hasTouchedSensitiveData = true err := s.loadAccounts() if err != nil { return err } err = s.loadKeys() if err != nil { return err } err = s.loadCerts() if err != nil { return err } } err := s.loadTargets() if err != nil { return err } if !isNeutered { err = s.loadPreferred() if err != nil { return err } } return nil } func (s *fdbStore) loadAccounts() error { c := s.db.Collection("accounts") serverNames, err := c.List() if err != nil { return err } s.accounts = map[string]*Account{} for _, serverName := range serverNames { sc := c.Collection(serverName) accountNames, err := sc.List() log.Errore(err, "failed to list accounts for server ", serverName) if err != nil { return err } for _, accountName := range accountNames { ac := sc.Collection(accountName) err := s.validateAccount(serverName, accountName, ac) log.Errore(err, "failed to load account ", accountName) if err != nil && IsWellFormattedCertificateOrKeyID(accountName) { // If the account name is not a well-formatted key ID and it fails to // load, ignore errors. return err } } } return nil } func (s *fdbStore) validateAccount(serverName, accountName string, c *fdb.Collection) error { f, err := c.Open("privkey") if err != nil { return err } defer f.Close() b, err := ioutil.ReadAll(f) if err != nil { return err } pk, err := acmeutils.LoadPrivateKey(b) if err != nil { return err } f.Close() directoryURL, err := decodeAccountURLPart(serverName) if err != nil { return err } account := &Account{ PrivateKey: pk, DirectoryURL: directoryURL, Authorizations: map[string]*Authorization{}, } accountID := account.ID() actualAccountID := serverName + "/" + accountName if accountID != actualAccountID { return fmt.Errorf("account ID mismatch: %#v != %#v", accountID, actualAccountID) } s.accounts[accountID] = account err = s.validateAuthorizations(account, c) if err != nil { return err } return nil } func (s *fdbStore) validateAuthorizations(account *Account, c *fdb.Collection) error { ac := c.Collection("authorizations") auths, err := ac.List() if err != nil { return err } for _, auth := range auths { auc := ac.Collection(auth) err := s.validateAuthorization(account, auth, auc) log.Errore(err, "failed to load authorization, ignoring: ", auth) } return nil } func (s *fdbStore) validateAuthorization(account *Account, authName string, c *fdb.Collection) error { ss, err := fdb.String(c.Open("expiry")) if err != nil { return err } expiry, err := time.Parse(time.RFC3339, strings.TrimSpace(ss)) if err != nil { return err } azURL, _ := fdb.String(c.Open("url")) if !acmeapi.ValidURL(azURL) { azURL = "" } az := &Authorization{ Name: authName, URL: strings.TrimSpace(azURL), Expires: expiry, } account.Authorizations[authName] = az return nil } func (s *fdbStore) loadKeys() error { s.keys = map[string]*Key{} c := s.db.Collection("keys") keyIDs, err := c.List() if err != nil { return err } for _, keyID := range keyIDs { kc := c.Collection(keyID) err := s.validateKey(keyID, kc) log.Errore(err, "failed to load key ", keyID) if err != nil && IsWellFormattedCertificateOrKeyID(keyID) { // If the key fails to load and it has an invalid key ID, ignore errors. return err } } return nil } func (s *fdbStore) validateKey(keyID string, kc *fdb.Collection) error { f, err := kc.Open("privkey") if err != nil { return err } defer f.Close() b, err := ioutil.ReadAll(f) if err != nil { return err } pk, err := acmeutils.LoadPrivateKey(b) if err != nil { return err } actualKeyID, err := determineKeyIDFromKey(pk) if err != nil { return err } if actualKeyID != keyID { return fmt.Errorf("key ID mismatch: %#v != %#v", keyID, actualKeyID) } k := &Key{ ID: actualKeyID, PrivateKey: pk, } s.keys[actualKeyID] = k return nil } func (s *fdbStore) loadCerts() error { s.certs = map[string]*Certificate{} c := s.db.Collection("certs") certIDs, err := c.List() if err != nil { return err } for _, certID := range certIDs { kc := c.Collection(certID) err := s.validateCert(certID, kc) log.Errore(err, "failed to load certificate ", certID) if err != nil && IsWellFormattedCertificateOrKeyID(certID) { // If the certificate fails to load and it has an invalid cert ID, // ignore errors. return err } } return nil } func (s *fdbStore) validateCert(certID string, c *fdb.Collection) error { ss, err := fdb.String(c.Open("url")) if err != nil { return err } ss = strings.TrimSpace(ss) if !acmeapi.ValidURL(ss) { return fmt.Errorf("certificate has invalid URI") } actualCertID := determineCertificateID(ss) if certID != actualCertID { return fmt.Errorf("cert ID mismatch: %#v != %#v", certID, actualCertID) } crt := &Certificate{ URL: ss, Certificates: nil, Cached: false, RevocationDesired: fdb.Exists(c, "revoke"), Revoked: fdb.Exists(c, "revoked"), } fullchain, err := fdb.Bytes(c.Open("fullchain")) if err == nil { certs, err := acmeutils.LoadCertificates(fullchain) if err != nil { return err } xcrt, err := x509.ParseCertificate(certs[0]) if err != nil { return err } keyID := determineKeyIDFromCert(xcrt) crt.Key = s.keys[keyID] if crt.Key != nil { err := c.WriteLink("privkey", fdb.Link{Target: "keys/" + keyID + "/privkey"}) if err != nil { return err } } crt.Certificates = certs crt.Cached = true } s.certs[certID] = crt return nil } func (s *fdbStore) loadTargets() error { s.targets = map[string]*Target{} // default target confc := s.db.Collection("conf") dtgt, err := s.validateTargetInner("target", confc, true) if err == nil { dtgt.genericise() s.defaultTarget = dtgt } else { if !os.IsNotExist(err) { log.Errore(err, "error loading default target file") } s.defaultTarget = &Target{} } // Legacy support. We have to do this here so that these defaults get copied // across to the targets. s.loadWebrootPaths() s.loadRSAKeySize() // targets c := s.db.Collection("desired") desiredKeys, err := c.List() if err != nil { return err } for _, desiredKey := range desiredKeys { err := s.validateTarget(desiredKey, c) log.Errore(err, "failed to load target ", desiredKey) // Ignore errors, best effort. } return nil } func (s *fdbStore) validateTarget(desiredKey string, c *fdb.Collection) error { tgt, err := s.validateTargetInner(desiredKey, c, false) if err != nil { return err } s.targets[desiredKey] = tgt return nil } func (s *fdbStore) validateTargetInner(desiredKey string, c *fdb.Collection, loadingDefault bool) (*Target, error) { b, err := fdb.Bytes(c.Open(desiredKey)) if err != nil { return nil, err } var tgt *Target if loadingDefault { tgt = &Target{} } else { tgt = s.defaultTarget.CopyGeneric() } tgt.Filename = desiredKey err = yaml.Unmarshal(b, tgt) if err != nil { return nil, err } if len(tgt.Satisfy.Names) == 0 { if len(tgt.LegacyNames) > 0 { tgt.Satisfy.Names = tgt.LegacyNames } else { tgt.Satisfy.Names = []string{desiredKey} } } if tgt.Request.Provider == "" { tgt.Request.Provider = tgt.LegacyProvider } err = normalizeNames(tgt.Satisfy.Names) if err != nil { return nil, fmt.Errorf("invalid target: %s: %v", desiredKey, err) } if len(tgt.Request.Names) == 0 { tgt.Request.Names = tgt.Satisfy.Names tgt.Request.implicitNames = true } // tgt.Request.Account is not set; it is for use by other code. return tgt, nil } // Saving {{{1 // Serializes the target to disk. Call after changing any settings. func (s *fdbStore) SaveTarget(t *Target) error { // Some basic validation. err := t.Validate() if err != nil { return err } if t != s.defaultTarget { t.ensureFilename() } tcopy := *t if t == s.defaultTarget { tcopy.genericise() } // don't serialize default request names list if tcopy.Request.implicitNames { tcopy.Request.Names = nil } b, err := yaml.Marshal(&tcopy) if err != nil { return err } // Save. if t == s.defaultTarget { return fdb.WriteBytes(s.db.Collection("conf"), "target", b) } return fdb.WriteBytes(s.db.Collection("desired"), t.Filename, b) } func (s *fdbStore) RemoveTarget(filename string) error { return s.db.Collection("desired").Delete(filename) } func (s *fdbStore) SaveCertificate(cert *Certificate) error { c := s.db.Collection("certs/" + cert.ID()) if cert.RevocationDesired { err := fdb.CreateEmpty(c, "revoke") if err != nil { return err } } if cert.Revoked { err := fdb.CreateEmpty(c, "revoked") if err != nil { return err } } if len(cert.Certificates) == 0 { return nil } fcert, err := c.Create("cert") if err != nil { return err } defer fcert.CloseAbort() fchain, err := c.Create("chain") if err != nil { return err } defer fchain.CloseAbort() ffullchain, err := c.Create("fullchain") if err != nil { return err } defer ffullchain.CloseAbort() err = acmeutils.SaveCertificates(io.MultiWriter(fcert, ffullchain), cert.Certificates[0]) if err != nil { return err } for _, ec := range cert.Certificates[1:] { err = acmeutils.SaveCertificates(io.MultiWriter(fchain, ffullchain), ec) if err != nil { return err } } fcert.Close() fchain.Close() ffullchain.Close() return nil } func (s *fdbStore) SaveAccount(a *Account) error { coll := s.db.Collection("accounts/" + a.ID()) w, err := coll.Create("privkey") if err != nil { return err } defer w.CloseAbort() err = acmeutils.SavePrivateKey(w, a.PrivateKey) if err != nil { return err } w.Close() for _, auth := range a.Authorizations { c := coll.Collection("authorizations/" + auth.Name) err := fdb.WriteBytes(c, "expiry", []byte(auth.Expires.Format(time.RFC3339))) if err != nil { return err } err = fdb.WriteBytes(c, "url", []byte(auth.URL)) if err != nil { return err } } return nil } // Removal {{{1 func (s *fdbStore) RemoveCertificate(certificateID string) error { _, ok := s.certs[certificateID] if !ok { return fmt.Errorf("certificate does not exist: %s", certificateID) } err := s.db.Collection("certs").Delete(certificateID) if err != nil { return err } delete(s.certs, certificateID) return nil } func (s *fdbStore) RemoveKey(keyID string) error { _, ok := s.keys[keyID] if !ok { return fmt.Errorf("key does not exist: %s", keyID) } err := s.db.Collection("keys").Delete(keyID) if err != nil { return err } delete(s.keys, keyID) return nil } // Importing {{{1 // Give a PEM-encoded key file, imports the key into the store. If the key is // already installed, returns nil. func (s *fdbStore) ImportKey(privateKey crypto.PrivateKey) (*Key, error) { keyID, err := determineKeyIDFromKey(privateKey) if err != nil { return nil, err } k, ok := s.keys[keyID] if ok { return k, nil } c := s.db.Collection("keys/" + keyID) err = s.saveKey(c, privateKey) if err != nil { return nil, err } k = &Key{ PrivateKey: privateKey, ID: keyID, } s.keys[keyID] = k return k, nil } // Given a certificate URL, imports the certificate into the store. The // certificate will be retrirved on the next reconcile. If a certificate with // that URL already exists, this is a no-op and returns nil. func (s *fdbStore) ImportCertificate(url string) (*Certificate, error) { certID := determineCertificateID(url) c, ok := s.certs[certID] if ok { return c, nil } err := fdb.WriteBytes(s.db.Collection("certs/"+certID), "url", []byte(url)) if err != nil { return nil, err } c = &Certificate{ URL: url, } s.certs[certID] = c return c, nil } // Given an account private key and the provider directory URL, imports that account key. // If the account already exists and has a private key, this is a no-op and returns nil. func (s *fdbStore) ImportAccount(directoryURL string, privateKey crypto.PrivateKey) (*Account, error) { accountID, err := determineAccountID(directoryURL, privateKey) if err != nil { return nil, err } a, ok := s.accounts[accountID] if ok { return a, nil } err = s.saveKey(s.db.Collection("accounts/"+accountID), privateKey) if err != nil { return nil, err } a = &Account{ PrivateKey: privateKey, DirectoryURL: directoryURL, } s.accounts[accountID] = a return a, nil } // Saves a key as a file named "privkey" inside the given collection. func (s *fdbStore) saveKey(c *fdb.Collection, privateKey crypto.PrivateKey) error { f, err := c.Create("privkey") if err != nil { return err } defer f.CloseAbort() err = acmeutils.SavePrivateKey(f, privateKey) if err != nil { return err } return f.Close() } // Save a private key inside a key ID collection under the given collection. func (s *fdbStore) saveKeyUnderID(c *fdb.Collection, privateKey crypto.PrivateKey) (keyID string, err error) { keyID, err = determineKeyIDFromKey(privateKey) if err != nil { return } err = s.saveKey(c.Collection(keyID), privateKey) return } // Revocation marking {{{1 // Try to revoke the certificate with the given certificate ID. // If a key ID is given, revoke all certificates with using key ID. func (s *fdbStore) RevokeByCertificateOrKeyID(certID string) error { c, ok := s.certs[certID] if !ok { return s.revokeByKeyID(certID) } if c.Revoked { log.Warnf("%v already revoked", c) return nil } col := s.db.Collection("certs/" + c.ID()) err := fdb.CreateEmpty(col, "revoke") if err != nil { return err } c.RevocationDesired = true return nil } func (s *fdbStore) revokeByKeyID(keyID string) error { k, ok := s.keys[keyID] if !ok { return fmt.Errorf("cannot find certificate or key with given ID: %q", keyID) } var merr MultiError for _, c := range s.certs { if c.Key != k { continue } err := s.RevokeByCertificateOrKeyID(c.ID()) if err != nil { merr = append(merr, fmt.Errorf("failed to mark %v for revocation: %v", c, err)) } } if len(merr) > 0 { return merr } return nil } acme-0.0.62/storage/types.go000066400000000000000000000177641321256625400156700ustar00rootroot00000000000000package storage import ( "crypto" "encoding/base32" "fmt" "github.com/hlandau/acme/acmeapi" "github.com/jmhodges/clock" "github.com/satori/go.uuid" "strings" "time" ) // Represents stored account data. type Account struct { // N. Account private key. PrivateKey crypto.PrivateKey // N. Server directory URL. DirectoryURL string // Disposable. Authorizations. Authorizations map[string]*Authorization // ID: determined from DirectoryURL and PrivateKey. // Path: formed from ID. // Registration URL: can be recovered automatically. } // Returns the account ID (server URL/key ID). func (a *Account) ID() string { accountID, err := determineAccountID(a.DirectoryURL, a.PrivateKey) log.Panice(err) return accountID } // Returns true iff the account is for a given provider URL. func (a *Account) MatchesURL(p string) bool { return p == a.DirectoryURL } func (a *Account) String() string { return fmt.Sprintf("Account(%v)", a.ID()) } // Represents an authorization. type Authorization struct { // N. The authorized hostname. Name string // N. The authorization URL. URL string // D. Can be derived from the URL. The authorization expiry time. Expires time.Time } // Returns true iff the authorization is unexpired. func (a *Authorization) IsValid(clock clock.Clock) bool { return clock.Now().Before(a.Expires) } // Represents the "satisfy" section of a target file. type TargetSatisfy struct { // N. List of SANs required to satisfy this target. May include hostnames // (and maybe one day SRV-IDs). May include wildcard hostnames, but ACME // doesn't support those yet. Names []string `yaml:"names,omitempty"` // D. Reduced name set, after disjunction operation. Derived from Names. ReducedNames []string `yaml:"-"` } // Represents the "request" section of a target file. type TargetRequest struct { // N/d. List of SANs to place on any obtained certificate. Defaults to the // names in the satisfy section. Names []string `yaml:"names,omitempty"` // Used to track whether Names was explicitly specified, for reserialization purposes. implicitNames bool // N. Currently, this is the provider directory URL. An account matching it // will be used. At some point, a way to specify a particular account should // probably be added. Provider string `yaml:"provider,omitempty"` // D. Account to use. The storage package does not set this; it is for the // convenience of consuming code. To be determined via Provider string. Account *Account `yaml:"-"` // Settings relating to the creation of new keys used to request // corresponding certificates. Key TargetRequestKey `yaml:"key,omitempty"` // Settings relating to the completion of challenges. Challenge TargetRequestChallenge `yaml:"challenge,omitempty"` // N. Request OCSP Must Staple in CSRs? OCSPMustStaple bool `yaml:"ocsp-must-staple,omitempty"` } // Settings for keys generated as part of certificate requests. type TargetRequestKey struct { // N. Key type to use in making a request. "rsa" or "ecdsa". Default "rsa". Type string `yaml:"type,omitempty"` // N. RSA key size to use for new RSA keys. Defaults to 2048 bits. RSASize int `yaml:"rsa-size,omitempty"` // N. ECDSA curve. "nistp256" (default), "nistp384" or "nistp521". ECDSACurve string `yaml:"ecdsa-curve,omitempty"` // N. The key ID of an existing key to use for the purposes of making // requests. If not set, always generate a new key. ID string `yaml:"id,omitempty"` } func (k *TargetRequestKey) String() string { switch k.Type { case "", "rsa": return fmt.Sprintf("rsa-%d", clampRSAKeySize(k.RSASize)) case "ecdsa": return fmt.Sprintf("ecdsa-%s", clampECDSACurve(k.ECDSACurve)) default: return k.Type // ... } } // Settings relating to the completion of challenges. type TargetRequestChallenge struct { // N. Webroot paths to use when completing challenges. WebrootPaths []string `yaml:"webroot-paths,omitempty"` // N. Ports to listen on when completing challenges. HTTPPorts []string `yaml:"http-ports,omitempty"` // 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. type Target struct { // Specifies conditions which must be met. Satisfy TargetSatisfy `yaml:"satisfy,omitempty"` // Specifies parameters used when requesting certificates. Request TargetRequest `yaml:"request,omitempty"` // N. Priority. See state storage specification. Priority int `yaml:"priority,omitempty"` // LEGACY. Names to be satisfied. Moved to Satisfy.Names. LegacyNames []string `yaml:"names,omitempty"` // LEGACY. Provider URL to used. Moved to Request.Provider. LegacyProvider string `yaml:"provider,omitempty"` // Internal use. The filename under which the target is stored. Filename string `yaml:"-"` } func (t *Target) String() string { return fmt.Sprintf("Target(%s;%s;%d)", strings.Join(t.Satisfy.Names, ","), t.Request.Provider, t.Priority) } // Validates a target for basic sanity. Returns the first error found or nil. func (t *Target) Validate() error { if t.Request.Provider != "" && !acmeapi.ValidURL(t.Request.Provider) { return fmt.Errorf("invalid provider URL: %q", t.Request.Provider) } return nil } func (tgt *Target) ensureFilename() { if tgt.Filename != "" { return } // Unfortunately we can't really check if the first hostname exists as a filename // and use another name instead as this would create all sorts of race conditions. // We have to use a random name. nprefix := "" if len(tgt.Satisfy.Names) > 0 { nprefix = tgt.Satisfy.Names[0] + "-" } b := uuid.NewV4().Bytes() str := strings.ToLower(strings.TrimRight(base32.StdEncoding.EncodeToString(b), "=")) tgt.Filename = nprefix + str } // Returns a copy of the target. func (t *Target) Copy() *Target { // A Target contains no pointers to part of the target which should be copied. // i.e. all pointers point to other things not part of the copy. Thus we can // 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 } // Returns a copy of the target, but zeroes any very specific fields // like names. func (t *Target) CopyGeneric() *Target { tt := t.Copy() tt.genericise() return tt } func (t *Target) genericise() { t.Satisfy.Names = nil t.Satisfy.ReducedNames = nil t.Request.Names = nil t.LegacyNames = nil } // Represents stored certificate information. type Certificate struct { // N. URL from which the certificate can be retrieved. URL string // N. Whether this certificate should be revoked. RevocationDesired bool // N (for now). Whether this certificate has been revoked. Revoked bool // D. Certificate data retrieved from URL, plus chained certificates. // The end certificate comes first, the root last, etc. Certificates [][]byte // D. True if the certificate has been downloaded. Cached bool // D. The private key for the certificate. Key *Key // D. ID: formed from hash of certificate URL. // D. Path: formed from ID. } // Returns a string summary of the certificate. func (c *Certificate) String() string { return fmt.Sprintf("Certificate(%v)", c.ID()) } // Returns the certificate ID. func (c *Certificate) ID() string { return determineCertificateID(c.URL) } // Represents a stored key. type Key struct { // N. The key. PrivateKey crypto.PrivateKey // D. ID: Derived from the key itself. ID string // D. Path: formed from ID. } // Returns a string summary of the key. func (k *Key) String() string { return fmt.Sprintf("Key(%v)", k.ID) } acme-0.0.62/storage/util.go000066400000000000000000000132541321256625400154670ustar00rootroot00000000000000package storage import ( "crypto" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base32" "fmt" "github.com/hlandau/acme/acmeapi/acmeutils" "io" "math/big" "net/url" "path/filepath" "regexp" "strings" ) func decodeAccountURLPart(part string) (string, error) { scheme := "https" if strings.HasPrefix(part, "http:") { scheme = "http" part = part[5:] } unesc, err := url.QueryUnescape(part) if err != nil { return "", err } p := scheme + "://" + unesc u, err := url.Parse(p) if err != nil { return "", err } if u.Path == "" { u.Path = "/" } return u.String(), nil } func accountURLPart(directoryURL string) (string, error) { u, err := url.Parse(directoryURL) if err != nil { return "", err } if u.Scheme != "https" && u.Scheme != "http" { return "", fmt.Errorf("scheme must be HTTPS (or HTTP)") } directoryURL = u.String() s := directoryURL[strings.IndexByte(directoryURL, ':')+3:] if u.Path == "/" { s = s[0 : len(s)-1] } s = lowerEscapes(url.QueryEscape(s)) if u.Scheme == "http" { s = "http:" + s } return s, nil } func lowerEscapes(s string) string { b := []byte(s) state := 0 for i := 0; i < len(b); i++ { switch state { case 0: if b[i] == '%' { state = 1 } case 1: if b[i] == '%' { state = 0 } else { state = 2 } b[i] = lowerChar(b[i]) case 2: state = 0 b[i] = lowerChar(b[i]) } } return string(b) } func lowerChar(c byte) byte { if c >= 'A' && c <= 'F' { return c - 'A' + 'a' } return c } // 'root' must be an absolute path. func pathIsWithin(subject, root string) (bool, error) { os := subject subject, err := filepath.EvalSymlinks(subject) if err != nil { log.Errore(err, "eval symlinks: ", os, " ", root) return false, err } subject, err = filepath.Abs(subject) if err != nil { return false, err } return strings.HasPrefix(subject, ensureSeparator(root)), nil } func ensureSeparator(p string) string { if !strings.HasSuffix(p, string(filepath.Separator)) { return p + string(filepath.Separator) } return p } func determineKeyIDFromCert(c *x509.Certificate) string { h := sha256.New() h.Write(c.RawSubjectPublicKeyInfo) return strings.ToLower(strings.TrimRight(base32.StdEncoding.EncodeToString(h.Sum(nil)), "=")) } func getPublicKey(pk crypto.PrivateKey) crypto.PublicKey { switch pkv := pk.(type) { case *rsa.PrivateKey: return &pkv.PublicKey case *ecdsa.PrivateKey: return &pkv.PublicKey default: panic("unsupported key type") } } func determineKeyIDFromKey(pk crypto.PrivateKey) (string, error) { return determineKeyIDFromKeyIntl(getPublicKey(pk), pk) } func determineKeyIDFromKeyIntl(pubk crypto.PublicKey, pk crypto.PrivateKey) (string, error) { cc := &x509.Certificate{ SerialNumber: big.NewInt(1), } cb, err := x509.CreateCertificate(rand.Reader, cc, cc, pubk, pk) if err != nil { return "", err } c, err := x509.ParseCertificate(cb) if err != nil { return "", err } return determineKeyIDFromCert(c), nil } type psuedoPrivateKey struct { pk crypto.PublicKey } func (ppk *psuedoPrivateKey) Public() crypto.PublicKey { return ppk.pk } func (ppk *psuedoPrivateKey) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) { return []byte{0}, nil } func DetermineKeyIDFromPublicKey(pubk crypto.PublicKey) (string, error) { // Trick crypto/x509 into creating a certificate so we can grab the // subjectPublicKeyInfo by giving it a fake private key generating an invalid // signature. ParseCertificate doesn't verify the signature so this will // work. // // Yes, this is very hacky, but avoids having to duplicate code in crypto/x509. determineKeyIDFromKeyIntl(pubk, psuedoPrivateKey{}) cc := &x509.Certificate{ SerialNumber: big.NewInt(1), } cb, err := x509.CreateCertificate(rand.Reader, cc, cc, pubk, &psuedoPrivateKey{pubk}) if err != nil { return "", err } c, err := x509.ParseCertificate(cb) if err != nil { return "", err } return determineKeyIDFromCert(c), nil } func determineAccountID(providerURL string, privateKey interface{}) (string, error) { u, err := accountURLPart(providerURL) if err != nil { return "", err } keyID, err := determineKeyIDFromKey(privateKey) if err != nil { return "", err } return u + "/" + keyID, nil } func determineCertificateID(url string) string { h := sha256.New() h.Write([]byte(url)) b := h.Sum(nil) return strings.ToLower(strings.TrimRight(base32.StdEncoding.EncodeToString(b), "=")) } var re_certID = regexp.MustCompile(`^[a-z0-9]{52}$`) // Returns true iff the given string could (possibly) be a valid certificate // (or key) ID. func IsWellFormattedCertificateOrKeyID(certificateID string) bool { return re_certID.MatchString(certificateID) } func targetGt(a *Target, b *Target) bool { if a == nil && b == nil { return false // equal } else if b == nil { return true // a > nil } else if a == nil { return false // nil < a } if a.Priority > b.Priority { return true } else if a.Priority < b.Priority { return false } return len(a.Satisfy.Names) > len(b.Satisfy.Names) } func containsName(names []string, name string) bool { for _, n := range names { if n == name { return true } } return false } func normalizeNames(names []string) error { for i := range names { n, err := acmeutils.NormalizeHostname(names[i]) if err != nil { return err } names[i] = n } return nil } // Used to return multiple errors, for example when several targets cannot be // reconciled. This prevents one failing target from blocking others. type MultiError []error func (merr MultiError) Error() string { s := "" for _, e := range merr { if s != "" { s += "; \n" } s += e.Error() } return "the following errors occurred:\n" + s } acme-0.0.62/storage/util_test.go000066400000000000000000000011161321256625400165200ustar00rootroot00000000000000package storage import ( "crypto/rand" "crypto/rsa" "testing" ) // Make sure the determineKeyIDFromKey and determineKeyIDFromPublicKey // functions produce the same result. func TestKeyID(t *testing.T) { pk, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("error: %v", err) } keyID, err := determineKeyIDFromKey(pk) if err != nil { t.Fatalf("error: %v", err) } keyID2, err := DetermineKeyIDFromPublicKey(&pk.PublicKey) if err != nil { t.Fatalf("error: %v", err) } if keyID != keyID2 { t.Fatalf("key ID mismatch: %#v != %#v", keyID, keyID2) } } acme-0.0.62/storageops/000077500000000000000000000000001321256625400147005ustar00rootroot00000000000000acme-0.0.62/storageops/config.go000066400000000000000000000016101321256625400164720ustar00rootroot00000000000000package storageops import "github.com/hlandau/acme/storage" // Update targets to remove any mention of hostname from all targets. The // targets are resaved to disk. func RemoveTargetHostname(s storage.Store, hostname string) error { return s.VisitTargets(func(t *storage.Target) error { if !containsName(t.Satisfy.Names, hostname) { return nil // continue } t.Satisfy.Names = removeStringFromList(t.Satisfy.Names, hostname) t.Request.Names = removeStringFromList(t.Request.Names, hostname) if len(t.Satisfy.Names) == 0 { return s.RemoveTarget(t.Filename) } return s.SaveTarget(t) }) } func containsName(names []string, name string) bool { for _, n := range names { if n == name { return true } } return false } func removeStringFromList(ss []string, s string) []string { var r []string for _, x := range ss { if x != s { r = append(r, x) } } return r } acme-0.0.62/storageops/cull.go000066400000000000000000000017441321256625400161740ustar00rootroot00000000000000package storageops import "github.com/hlandau/acme/storage" func Cull(s storage.Store, simulate bool) error { certificatesToCull := map[string]*storage.Certificate{} // Relink before culling. err := Relink(s) if err != nil { return err } // Select all certificates. s.VisitCertificates(func(c *storage.Certificate) error { certificatesToCull[c.ID()] = c return nil }) // Unselect any certificate which is currently referenced. s.VisitPreferredCertificates(func(hostname string, c *storage.Certificate) error { delete(certificatesToCull, c.ID()) return nil }) // Now delete any certificate which is not generally valid. for certID, c := range certificatesToCull { if CertificateGenerallyValid(c) { continue } if simulate { log.Noticef("would delete certificate %s", certID) } else { log.Noticef("deleting certificate %s", certID) err := s.RemoveCertificate(certID) log.Errore(err, "failed to delete certificate ", certID) } } return nil } acme-0.0.62/storageops/keysize.go000066400000000000000000000016041321256625400167130ustar00rootroot00000000000000package storageops import "crypto/elliptic" const ( minRSASize = 2048 defaultRSASize = 2048 maxRSASize = 4096 ) func clampRSAKeySize(sz int) int { if sz == 0 { return defaultRSASize } if sz < minRSASize { return minRSASize } if sz > maxRSASize { return maxRSASize } return sz } const defaultCurve = "nistp256" // Make sure the curve name is valid and use a default curve name. "clamp" is // not the sanest name here but is consistent with clampRSAKeySize. func clampECDSACurve(curveName string) string { switch curveName { case "nistp256", "nistp384", "nistp521": return curveName default: return defaultCurve } } func getECDSACurve(curveName string) elliptic.Curve { switch clampECDSACurve(curveName) { case "nistp256": return elliptic.P256() case "nistp384": return elliptic.P384() case "nistp521": return elliptic.P521() default: return nil } } acme-0.0.62/storageops/reconcile.go000066400000000000000000000547151321256625400172060ustar00rootroot00000000000000// Package storageops implements operations on the state directory. package storageops import ( "bytes" "crypto" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "fmt" "github.com/hlandau/acme/acmeapi" "github.com/hlandau/acme/acmeapi/acmeendpoints" "github.com/hlandau/acme/acmeapi/acmeutils" "github.com/hlandau/acme/hooks" "github.com/hlandau/acme/responder" "github.com/hlandau/acme/solver" "github.com/hlandau/acme/storage" "github.com/hlandau/xlog" "github.com/jmhodges/clock" "golang.org/x/net/context" "sort" "strings" ) var log, Log = xlog.New("acme.storageops") // Internal use only. Used for testing purposes. Do not change. var InternalClock = clock.Default() type reconcile struct { store storage.Store // Cache of account clients to avoid duplicated directory lookups. accountClients map[*storage.Account]*acmeapi.Client } func makeReconcile(store storage.Store) *reconcile { return &reconcile{ store: store, accountClients: map[*storage.Account]*acmeapi.Client{}, } } func EnsureRegistration(store storage.Store) error { r := makeReconcile(store) return r.EnsureRegistration() } func (r *reconcile) EnsureRegistration() error { a, err := r.getAccountByDirectoryURL("") if err != nil { return err } cl := r.getClientForAccount(a) return solver.AssistedUpsertRegistration(cl, nil, context.TODO()) } // Runs the reconcilation operation. func Reconcile(store storage.Store) error { r := makeReconcile(store) reconcileErr := r.Reconcile() log.Errore(reconcileErr, "failed to reconcile") reloadErr := r.store.Reload() log.Errore(reloadErr, "failed to reload after reconcilation") relinkErr := r.Relink() log.Errore(relinkErr, "failed to relink after reconcilation") err := reconcileErr if err == nil { err = reloadErr } if err == nil { err = relinkErr } return err } // Runs the relink operation without running the reconcile operation. func Relink(store storage.Store) error { r := makeReconcile(store) err := r.Relink() log.Errore(err, "failed to relink") return err } func (r *reconcile) Relink() error { hostnameTargetMapping, err := r.disjoinTargets() if err != nil { return err } var updatedHostnames []string for name, tgt := range hostnameTargetMapping { c, err := FindBestCertificateSatisfying(r.store, tgt) if err != nil { log.Debugf("could not find certificate satisfying %v: %v", tgt, err) continue } log.Tracef("relink: best certificate satisfying %v is %v", tgt, c) cprev, err := r.store.PreferredCertificateForHostname(name) if c != cprev || err != nil { log.Debugf("relinking: %v -> %v (was %v)", name, c, cprev) updatedHostnames = append(updatedHostnames, name) err = r.store.SetPreferredCertificateForHostname(name, c) log.Errore(err, "failed to set preferred certificate for hostname") } } 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 } func (r *reconcile) disjoinTargets() (hostnameTargetMapping map[string]*storage.Target, err error) { var targets []*storage.Target r.store.VisitTargets(func(t *storage.Target) error { targets = append(targets, t) return nil }) sort.Stable(sort.Reverse(targetSorter(targets))) // Hostname-target mapping. hostnameTargetMapping = map[string]*storage.Target{} for _, tgt := range targets { tgt.Satisfy.ReducedNames = nil for _, name := range tgt.Satisfy.Names { _, exists := hostnameTargetMapping[name] if !exists { hostnameTargetMapping[name] = tgt tgt.Satisfy.ReducedNames = append(tgt.Satisfy.ReducedNames, name) } } } // Debugging information. for name, tgt := range hostnameTargetMapping { log.Debugf("disjoint hostname mapping: %s -> %v", name, tgt) } return } func (r *reconcile) Reconcile() error { err := r.processUncachedCertificates() if err != nil { return err } err = r.processPendingRevocations() log.Errore(err, "could not process pending revocations") err = r.processTargets() log.Errore(err, "error while processing targets") if err != nil { return err } return nil } func (r *reconcile) processUncachedCertificates() error { if !HaveUncachedCertificates(r.store) { return nil } log.Debug("there are uncached certificates - downloading them") err := r.downloadUncachedCertificates() if err != nil { log.Errore(err, "error while downloading uncached certificates") return err } log.Debug("reloading after downloading uncached certificates") err = r.store.Reload() if err != nil { log.Errore(err, "failed to reload after downloading uncached certificates") return err } log.Debug("finished reloading after downloading uncached certificates") if HaveUncachedCertificates(r.store) { log.Error("failed to download all uncached certificates") return fmt.Errorf("cannot obtain one or more uncached certificates") } return nil } func HaveUncachedCertificates(s storage.Store) bool { haveUncached := false s.VisitCertificates(func(c *storage.Certificate) error { if !c.Cached { haveUncached = true } return nil }) return haveUncached } func (r *reconcile) downloadUncachedCertificates() error { return r.store.VisitCertificates(func(c *storage.Certificate) error { if c.Cached { return nil } return r.downloadCertificate(c) }) } func (r *reconcile) downloadCertificate(c *storage.Certificate) error { log.Debugf("downloading certificate %v", c) cl := r.getGenericClient() crt := acmeapi.Certificate{ URI: c.URL, } err := cl.WaitForCertificate(&crt, context.TODO()) if err != nil { return err } if len(crt.Certificate) == 0 { return fmt.Errorf("nil certificate?") } c.Certificates = [][]byte{crt.Certificate} c.Certificates = append(c.Certificates, crt.ExtraCertificates...) c.Cached = true err = r.store.SaveCertificate(c) if err != nil { log.Errore(err, "failed to save certificate after retrieval: %v", c) return err } return nil } func (r *reconcile) processPendingRevocations() error { var me storage.MultiError r.store.VisitCertificates(func(c *storage.Certificate) error { if c.Revoked || !c.RevocationDesired { return nil } err := r.revokeCertificate(c) if err != nil { me = append(me, fmt.Errorf("failed to revoke %v: %v", c, err)) // keep processing revocations } return nil }) if len(me) > 0 { return me } return nil } func (r *reconcile) revokeCertificate(c *storage.Certificate) error { err := r.revokeCertificateInner(c) if err != nil { return err } c.Revoked = true err = r.store.SaveCertificate(c) if err != nil { log.Errore(err, "failed to save certificate after revocation: ", c) return err } return nil } func (r *reconcile) revokeCertificateInner(c *storage.Certificate) error { if len(c.Certificates) == 0 { return fmt.Errorf("no certificates in certificate to revoke: %v", c) } endCertificate := c.Certificates[0] crt, err := x509.ParseCertificate(endCertificate) if err != nil { return err } // Get the endpoint which issued the certificate. endpoint, err := acmeendpoints.CertificateToEndpoint(r.getGenericClient(), crt, context.TODO()) if err != nil { return fmt.Errorf("could not map certificate %v to endpoint: %v", c, err) } // In order to revoke a certificate, one needs either the private key of the // certificate, or the account key with authorizations for all names on the // certificate. Try and find the private key first. var client *acmeapi.Client var revocationKey crypto.PrivateKey if c.Key != nil { revocationKey = c.Key.PrivateKey client = r.getClientForDirectoryURL(endpoint.DirectoryURL) } if revocationKey == nil { acct, err := r.getAccountByDirectoryURL(endpoint.DirectoryURL) if err != nil { return err } client = r.getClientForAccount(acct) // If we have no private key for the certificate, obtain all necessary // authorizations. err = r.getRevocationAuthorizations(acct, crt) if err != nil { return err } } return client.Revoke(endCertificate, revocationKey, context.TODO()) } func (r *reconcile) getGenericClient() *acmeapi.Client { return &acmeapi.Client{} } func (r *reconcile) getClientForDirectoryURL(directoryURL string) *acmeapi.Client { cl := r.getGenericClient() cl.DirectoryURL = directoryURL return cl } func (r *reconcile) getClientForAccount(a *storage.Account) *acmeapi.Client { cl := r.accountClients[a] if cl == nil { cl = r.getClientForDirectoryURL(a.DirectoryURL) cl.AccountKey = a.PrivateKey r.accountClients[a] = cl } return cl } func (r *reconcile) getRevocationAuthorizations(acct *storage.Account, crt *x509.Certificate) error { log.Debugf("obtaining authorizations needed to facilitate revocation") return r.obtainNecessaryAuthorizations(crt.DNSNames, acct, "", &r.store.DefaultTarget().Request.Challenge) } func (r *reconcile) obtainNecessaryAuthorizations(names []string, a *storage.Account, targetFilename string, ccfg *storage.TargetRequestChallenge) error { authsNeeded := r.determineNecessaryAuthorizations(names, a) for _, name := range authsNeeded { log.Debugf("trying to obtain authorization for %q", name) err := r.obtainAuthorization(name, a, targetFilename, ccfg) if err != nil { log.Errore(err, "could not obtain authorization for ", name) return err } } return nil } func (r *reconcile) determineNecessaryAuthorizations(names []string, a *storage.Account) []string { needed := map[string]struct{}{} for _, n := range names { needed[n] = struct{}{} } for _, auth := range a.Authorizations { if auth.IsValid(InternalClock) { delete(needed, auth.Name) } } // Preserve the order of the names in case the user considers that important. var neededs []string for _, name := range names { if _, ok := needed[name]; ok { neededs = append(neededs, name) } } return neededs } func generateHookPEM(info *responder.TLSSNIChallengeInfo) (string, error) { b := bytes.Buffer{} err := acmeutils.SaveCertificates(&b, info.Certificate) if err != nil { return "", err } err = acmeutils.SavePrivateKey(&b, info.Key) if err != nil { return "", err } return b.String(), nil } 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(ctx, name, targetFilename, v.Filename, v.Body) return err case *responder.TLSSNIChallengeInfo: hookPEM, err := generateHookPEM(v) if err != nil { return err } _, err = hooks.ChallengeTLSSNIStart(ctx, name, targetFilename, v.Hostname1, v.Hostname2, hookPEM) return err case *responder.DNSChallengeInfo: installed, err := hooks.ChallengeDNSStart(ctx, name, targetFilename, v.Body) if err == nil && !installed { return fmt.Errorf("could not install DNS challenge, no hooks succeeded") } return err default: return nil } } stopHookFunc := func(challengeInfo interface{}) error { switch v := challengeInfo.(type) { case *responder.HTTPChallengeInfo: 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(ctx, name, targetFilename, v.Hostname1, v.Hostname2, hookPEM) return err case *responder.DNSChallengeInfo: uninstalled, err := hooks.ChallengeDNSStop(ctx, name, targetFilename, v.Body) if err == nil && !uninstalled { return fmt.Errorf("could not uninstall DNS challenge, no hooks succeeded") } return err default: return nil } } httpSelfTest := true if trc.HTTPSelfTest != nil { httpSelfTest = *trc.HTTPSelfTest } ccfg := responder.ChallengeConfig{ WebPaths: trc.WebrootPaths, HTTPPorts: trc.HTTPPorts, HTTPNoSelfTest: !httpSelfTest, PriorKeyFunc: r.getPriorKey, StartHookFunc: startHookFunc, StopHookFunc: stopHookFunc, } az, err := solver.Authorize(cl, name, ccfg, context.TODO()) if err != nil { return err } err = cl.LoadAuthorization(az, context.TODO()) if err != nil { // Try proceeding anyway. return nil } if a.Authorizations == nil { a.Authorizations = map[string]*storage.Authorization{} } a.Authorizations[az.Identifier.Value] = &storage.Authorization{ URL: az.URI, Name: az.Identifier.Value, Expires: az.Expires, } err = r.store.SaveAccount(a) if err != nil { return err } return nil } func (r *reconcile) getPriorKey(publicKey crypto.PublicKey) (crypto.PrivateKey, error) { // Returning an error here short circuits. If any errors occur here, return (nil,nil). keyID, err := storage.DetermineKeyIDFromPublicKey(publicKey) if err != nil { log.Errore(err, "failed to get key ID from public key") return nil, nil } k := r.store.KeyByID(keyID) if k == nil { log.Infof("failed to find key ID wanted by proofOfPossession: %s", keyID) return nil, nil // unknown key } return k.PrivateKey, nil } func (r *reconcile) getAccountByDirectoryURL(directoryURL string) (*storage.Account, error) { if directoryURL == "" { directoryURL = r.store.DefaultTarget().Request.Provider } if directoryURL == "" { directoryURL = acmeendpoints.DefaultEndpoint.DirectoryURL } if !acmeapi.ValidURL(directoryURL) { return nil, fmt.Errorf("directory URL is not a valid HTTPS URL") } ma := r.store.AccountByDirectoryURL(directoryURL) if ma != nil { return ma, nil } return r.createNewAccount(directoryURL) } func (r *reconcile) createNewAccount(directoryURL string) (*storage.Account, error) { pk, err := generateKey(&r.store.DefaultTarget().Request.Key) if err != nil { return nil, err } a := &storage.Account{ PrivateKey: pk, DirectoryURL: directoryURL, } err = r.store.SaveAccount(a) if err != nil { log.Errore(err, "failed to save account") return nil, err } return a, nil } func (r *reconcile) processTargets() error { var merr storage.MultiError r.store.VisitTargets(func(t *storage.Target) error { c, err := FindBestCertificateSatisfying(r.store, t) log.Debugf("%v: best certificate satisfying is %v, err=%v", t, c, err) if err == nil && !CertificateNeedsRenewing(c) { log.Debugf("%v: have best certificate which does not need renewing, skipping target", t) return nil // continue } log.Debugf("%v: requesting certificate", t) err = r.requestCertificateForTarget(t) log.Errore(err, t, ": failed to request certificate") if err != nil { // Do not block satisfaction of other targets just because one fails; // collect errors and return them as one. merr = append(merr, &TargetSpecificError{ Target: t, Err: err, }) } return nil }) log.Debugf("done processing targets, reconciliation complete, %d errors occurred", len(merr)) if len(merr) != 0 { return merr } return nil } func (r *reconcile) getRequestAccount(tr *storage.TargetRequest) (*storage.Account, error) { if tr.Account != nil { return tr.Account, nil } // This will create the account if it doesn't exist. acct, err := r.getAccountByDirectoryURL(tr.Provider) if err != nil { return nil, err } return acct, nil } // Returns the strings in ys not contained in xs. func stringsNotIn(xs, ys []string) []string { m := map[string]struct{}{} for _, x := range xs { m[x] = struct{}{} } var zs []string for _, y := range ys { _, ok := m[y] if !ok { zs = append(zs, y) } } return zs } func ensureConceivablySatisfiable(t *storage.Target) { // We ensure that every stipulation in the satisfy section can be met by the request // parameters. excludedNames := stringsNotIn(t.Request.Names, t.Satisfy.Names) if len(excludedNames) > 0 { log.Warnf("%v can never be satisfied because names to be requested are not a superset of the names to be satisfied; adding names automatically to render target satisfiable", t) } for _, n := range excludedNames { t.Request.Names = append(t.Request.Names, n) } } func (r *reconcile) requestCertificateForTarget(t *storage.Target) error { //return fmt.Errorf("not requesting certificate") // debugging neuter ensureConceivablySatisfiable(t) acct, err := r.getRequestAccount(&t.Request) if err != nil { return err } cl := r.getClientForAccount(acct) err = solver.AssistedUpsertRegistration(cl, nil, context.TODO()) if err != nil { return err } err = r.obtainNecessaryAuthorizations(t.Request.Names, acct, t.Filename, &t.Request.Challenge) if err != nil { return err } csr, err := r.createCSR(t) if err != nil { return err } log.Debugf("%v: requesting certificate", t) acrt, err := cl.RequestCertificate(csr, context.TODO()) if err != nil { log.Errore(err, "could not request certificate") return err } c, err := r.store.ImportCertificate(acrt.URI) if err != nil { log.Errore(err, "could not import certificate") return err } err = r.downloadCertificate(c) if err != nil { log.Errore(err, "failed to download certificate") return err } return nil } var ( oidTLSFeature = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} mustStapleFeatureValue = []byte{0x30, 0x03, 0x02, 0x01, 0x05} ) func (r *reconcile) createCSR(t *storage.Target) ([]byte, error) { if len(t.Request.Names) == 0 { return nil, fmt.Errorf("cannot request a certificate with no names") } csr := &x509.CertificateRequest{ DNSNames: t.Request.Names, Subject: pkix.Name{ CommonName: t.Request.Names[0], }, } if t.Request.OCSPMustStaple { csr.ExtraExtensions = append(csr.ExtraExtensions, pkix.Extension{ Id: oidTLSFeature, Value: mustStapleFeatureValue, }) } pk, err := r.generateOrGetKey(&t.Request.Key) if err != nil { log.Errore(err, "could not generate key while generating CSR for %v", t) return nil, err } _, err = r.store.ImportKey(pk) if err != nil { log.Errore(err, "could not import freshly generated key while generating CSR for %v", t) return nil, err } csr.SignatureAlgorithm, err = signatureAlgorithmFromKey(pk) if err != nil { return nil, err } return x509.CreateCertificateRequest(rand.Reader, csr, pk) } func (r *reconcile) generateOrGetKey(trk *storage.TargetRequestKey) (crypto.PrivateKey, error) { if trk.ID != "" { k := r.store.KeyByID(strings.TrimSpace(strings.ToLower(trk.ID))) if k != nil { return k.PrivateKey, nil } log.Warnf("target requests specific key %q but it cannot be found, generating a new key", trk.ID) } return generateKey(trk) } func DoesCertificateSatisfy(c *storage.Certificate, t *storage.Target) bool { if c.Revoked { log.Debugf("%v cannot satisfy %v because it is revoked", c, t) return false } if len(c.Certificates) == 0 { log.Debugf("%v cannot satisfy %v because it has no actual certificates", c, t) return false } if c.Key == nil { // A certificate we don't have the key for is unusable. log.Debugf("%v cannot satisfy %v because we do not have a key for it", c, t) return false } cc, err := x509.ParseCertificate(c.Certificates[0]) if err != nil { log.Debugf("%v cannot satisfy %v because we cannot parse it: %v", c, t, err) return false } names := map[string]struct{}{} for _, name := range cc.DNSNames { names[name] = struct{}{} } for _, name := range t.Satisfy.Names { _, ok := names[name] if !ok { log.Debugf("%v cannot satisfy %v because required hostname %q is not listed on it: %#v", c, t, name, cc.DNSNames) return false } } log.Debugf("%v satisfies %v", c, t) return true } func FindBestCertificateSatisfying(s storage.Store, t *storage.Target) (*storage.Certificate, error) { var bestCert *storage.Certificate err := s.VisitCertificates(func(c *storage.Certificate) error { if DoesCertificateSatisfy(c, t) { isBetterThan, err := CertificateBetterThan(c, bestCert) if err != nil { return err } if isBetterThan { log.Tracef("findBestCertificateSatisfying: %v > %v", c, bestCert) bestCert = c } else { log.Tracef("findBestCertificateSatisfying: %v <= %v", c, bestCert) } } return nil }) if err != nil { return nil, err } if bestCert == nil { return nil, fmt.Errorf("%v: no certificate satisfies this target", t) } return bestCert, nil } func CertificateBetterThan(a, b *storage.Certificate) (bool, error) { if b == nil || a == nil { return (b == nil && a != nil), nil } if len(a.Certificates) == 0 || len(b.Certificates) == 0 { return false, fmt.Errorf("need two certificates to compare") } ac, err := x509.ParseCertificate(a.Certificates[0]) bc, err2 := x509.ParseCertificate(b.Certificates[0]) if err != nil || err2 != nil { if err == nil && err2 != nil { log.Tracef("certBetterThan: parseable certificate is better than unparseable certificate") return true, nil } return false, nil } isAfter := ac.NotAfter.After(bc.NotAfter) log.Tracef("certBetterThan: (%v > %v)=%v", ac.NotAfter, bc.NotAfter, isAfter) return isAfter, nil } func CertificateNeedsRenewing(c *storage.Certificate) bool { if len(c.Certificates) == 0 { log.Debugf("%v: not renewing because it has no actual certificates (???)", c) return false } cc, err := x509.ParseCertificate(c.Certificates[0]) if err != nil { log.Debugf("%v: not renewing because its end certificate is unparseable", c) return false } renewTime := renewTime(cc.NotBefore, cc.NotAfter) needsRenewing := !InternalClock.Now().Before(renewTime) log.Debugf("%v needsRenewing=%v notAfter=%v", c, needsRenewing, cc.NotAfter) return needsRenewing } // This is used to detertmine whether to cull certificates. func CertificateGenerallyValid(c *storage.Certificate) bool { // This function is very conservative because if we return false // the certificate will get deleted. Revocation and expiry are // good reasons to delete. We already know the certificate is // unreferenced. if c.Revoked { log.Debugf("%v not generally valid because it is revoked", c) return false } if len(c.Certificates) == 0 { // If we have no actual certificates, give the benefit of the doubt. // Maybe the certificate is undownloaded. log.Debugf("%v has no actual certificates, assuming valid", c) return true } cc, err := x509.ParseCertificate(c.Certificates[0]) if err != nil { log.Debugf("%v cannot be parsed, assuming valid", c) return false } if !InternalClock.Now().Before(cc.NotAfter) { log.Debugf("%v not generally valid because it is expired", c) return false } return true } acme-0.0.62/storageops/revoke.go000066400000000000000000000015671321256625400165330ustar00rootroot00000000000000package storageops import ( "fmt" "github.com/hlandau/acme/storage" ) func RevokeByCertificateOrKeyID(s storage.Store, id string) error { c := s.CertificateByID(id) if c == nil { return revokeByKeyID(s, id) } if c.Revoked { log.Warnf("%v already revoked", c) return nil } c.RevocationDesired = true return s.SaveCertificate(c) } func revokeByKeyID(s storage.Store, keyID string) error { k := s.KeyByID(keyID) if k == nil { return fmt.Errorf("cannot find certificate or key with given ID: %q", keyID) } var merr storage.MultiError s.VisitCertificates(func(c *storage.Certificate) error { if c.Key != k { return nil // continue } err := RevokeByCertificateOrKeyID(s, c.ID()) if err != nil { merr = append(merr, fmt.Errorf("failed to mark %v for revocation: %v", c, err)) } return nil }) if len(merr) > 0 { return merr } return nil } acme-0.0.62/storageops/util.go000066400000000000000000000040401321256625400162020ustar00rootroot00000000000000package storageops import ( "crypto" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/x509" "fmt" "github.com/hlandau/acme/storage" "time" ) type targetSorter []*storage.Target func (ts targetSorter) Len() int { return len(ts) } func (ts targetSorter) Swap(i, j int) { ts[i], ts[j] = ts[j], ts[i] } func (ts targetSorter) Less(i, j int) bool { return targetGt(ts[j], ts[i]) } func targetGt(a *storage.Target, b *storage.Target) bool { if a == nil && b == nil { return false // equal } else if b == nil { return true // a > nil } else if a == nil { return false // nil < a } if a.Priority > b.Priority { return true } else if a.Priority < b.Priority { return false } return len(a.Satisfy.Names) > len(b.Satisfy.Names) } // This is 30 days, which is a bit high, but Let's Encrypt sends expiration // emails at 19 days, so... const renewalMargin = 30 * 24 * time.Hour // close enough to 30 days func renewTime(notBefore, notAfter time.Time) time.Time { validityPeriod := notAfter.Sub(notBefore) renewSpan := validityPeriod / 3 if renewSpan > renewalMargin { renewSpan = renewalMargin } return notAfter.Add(-renewSpan) } func signatureAlgorithmFromKey(pk crypto.PrivateKey) (x509.SignatureAlgorithm, error) { switch pk.(type) { case *rsa.PrivateKey: return x509.SHA256WithRSA, nil case *ecdsa.PrivateKey: return x509.ECDSAWithSHA256, nil default: return x509.UnknownSignatureAlgorithm, fmt.Errorf("unknown key type %T", pk) } } func generateKey(trk *storage.TargetRequestKey) (pk crypto.PrivateKey, err error) { switch trk.Type { default: fallthrough // ... case "", "rsa": pk, err = rsa.GenerateKey(rand.Reader, clampRSAKeySize(trk.RSASize)) case "ecdsa": pk, err = ecdsa.GenerateKey(getECDSACurve(trk.ECDSACurve), rand.Reader) } return } // Error associated with a specific target, for clarity of error messages. type TargetSpecificError struct { Target *storage.Target Err error } func (tse *TargetSpecificError) Error() string { return fmt.Sprintf("error satisfying %v: %v", tse.Target, tse.Err) }