diff --git a/bb.edn b/bb.edn index 30e18e18..7fa9d607 100644 --- a/bb.edn +++ b/bb.edn @@ -12,6 +12,8 @@ manifest (apply dc/-main "manifest" *command-line-args*) build-images {:task (apply dc/-main "build-images" *command-line-args*)} changed-build-dirs (apply dc/-main "changed-build-dirs" *command-line-args*) + refresh-lein-key {:doc "Refresh resources/lein-signing-key.asc from the keyserver" + :task (dc/-main "refresh-lein-key")} test (clojure '-X:test) fmt-check {:doc "Check formatting with cljfmt" :task (fmt/check {:paths ["src" "test" "bb.edn" "deps.edn" "tests.edn"]})} diff --git a/resources/lein-signing-key.asc b/resources/lein-signing-key.asc new file mode 100644 index 00000000..43531e3b --- /dev/null +++ b/resources/lein-signing-key.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=/HwY +-----END PGP PUBLIC KEY BLOCK----- diff --git a/resources/templates/lein.tmpl b/resources/templates/lein.tmpl index e67fe478..eef8ee7d 100644 --- a/resources/templates/lein.tmpl +++ b/resources/templates/lein.tmpl @@ -3,15 +3,42 @@ ENV LEIN_INSTALL=/usr/local/bin/ WORKDIR /tmp +COPY {{gpg-key-file}} /tmp/{{gpg-key-file}} + # No standalone uberjar is published for this version, so build it from source. # Verify Leiningen's GPG-signed release tag and pin the expected commit, then # bootstrap leiningen-core's dependencies with Maven and run `lein uberjar`. +# +# The signing key comes from the copy in {{gpg-key-file}}. We also fetch it +# from keyserver.ubuntu.com: if the keyserver's copy differs (e.g. the key was +# revoked, extended or gained a subkey) the build fails so the change can be +# reviewed; if the keyserver can't be reached we carry on with the local copy. +# Only the fields that describe the key itself are compared, since gpg also +# records where and when each copy was imported. RUN set -eux; \ {% for dep in install-deps %}{{dep}} && \ {% endfor %}export GNUPGHOME="$(mktemp -d)" && \ export LEIN_ROOT=1 && \ mkdir -p $LEIN_INSTALL /usr/share/java /root/.lein && \ -gpg --batch --keyserver hkps://keyserver.ubuntu.com --recv-keys {{gpg-key}} && \ +key_summary() { gpg --batch --homedir "$1" --with-colons --list-keys {{gpg-key}} | awk -F: '/^(pub|sub|uid|fpr):/ { print $1, $2, $5, $6, $7, $8, $10, $12 }'; } && \ +gpg --batch --import /tmp/{{gpg-key-file}} && \ +[ "$(gpg --batch --with-colons --list-keys | awk -F: '$1 == "fpr" && prev == "pub" { print $10 } { prev = $1 }')" = "{{gpg-key}}" ] && \ +keyserver_home="$(mktemp -d)" && \ +if gpg --batch --homedir "$keyserver_home" --keyserver hkps://keyserver.ubuntu.com --recv-keys {{gpg-key}}; then \ + if [ "$(key_summary "$GNUPGHOME")" != "$(key_summary "$keyserver_home")" ]; then \ + printf '%s\n' \ + "ERROR: Leiningen's signing key {{gpg-key}} on keyserver.ubuntu.com no longer matches {{gpg-key-file}}." \ + "It may have been revoked, had its expiry changed, or gained a subkey or user ID. Review the change, then" \ + "refresh the local copy by running 'bb refresh-lein-key' in docker-clojure and regenerate the Dockerfiles." \ + "Local copy:" "$(key_summary "$GNUPGHOME")" \ + "Keyserver copy:" "$(key_summary "$keyserver_home")" >&2; \ + exit 1; \ + fi; \ +else \ + echo "WARNING: couldn't fetch {{gpg-key}} from keyserver.ubuntu.com; using the local copy in {{gpg-key-file}}." >&2; \ +fi && \ +gpgconf --homedir "$keyserver_home" --kill all && \ +rm -rf "$keyserver_home" /tmp/{{gpg-key-file}} && \ git clone --depth 1 --branch $LEIN_VERSION https://codeberg.org/leiningen/leiningen.git && \ cd leiningen && \ git verify-tag $LEIN_VERSION && \ diff --git a/src/docker_clojure/config.clj b/src/docker_clojure/config.clj index c20dc634..aa34bd22 100644 --- a/src/docker_clojure/config.clj +++ b/src/docker_clojure/config.clj @@ -116,7 +116,7 @@ :default :debian/bookworm}) (def build-tools - {"lein" "2.13.0" + {"lein" "2.13.1" "tools-deps" "1.12.6.1673"}) (def default-build-tool "tools-deps") diff --git a/src/docker_clojure/core.clj b/src/docker_clojure/core.clj index 9a0a2df4..8cc2ba77 100644 --- a/src/docker_clojure/core.clj +++ b/src/docker_clojure/core.clj @@ -8,6 +8,7 @@ [docker-clojure.config :as cfg] [docker-clojure.docker :as docker] [docker-clojure.dockerfile :as df] + [docker-clojure.lein-key :as lein-key] [docker-clojure.log :refer [log] :as logger] [docker-clojure.manifest :as manifest] [docker-clojure.util :refer [get-or-default]] @@ -144,6 +145,10 @@ (do (log "ERROR diffing target against" (str base ":") err) ::failed)))) +(defn cmd-refresh-lein-key [_] + (when-not (lein-key/refresh!) + ::failed)) + (def cmd-table [{:cmds ["clean"] :fn cmd-clean @@ -170,7 +175,10 @@ :fn cmd-changed-build-dirs :doc "List the build dirs worth building to verify staged target changes" :args->opts [:base] - :spec {:base {:desc "Git ref to compare staged target against (default: HEAD)"}}}]) + :spec {:base {:desc "Git ref to compare staged target against (default: HEAD)"}}} + {:cmds ["refresh-lein-key"] + :fn cmd-refresh-lein-key + :doc "Refresh the local copy of Leiningen's signing key from the keyserver"}]) (defn -main "Entrypoint for CLI." diff --git a/src/docker_clojure/docker.clj b/src/docker_clojure/docker.clj index 581b5c67..c49709ac 100644 --- a/src/docker_clojure/docker.clj +++ b/src/docker_clojure/docker.clj @@ -55,6 +55,16 @@ (and (= "lein" build-tool) (df/entrypoint? variant)) (conj ["sh" "-c" (lein-routing-check) "sh" (pr-str print-lein-commands)])))) +(defn abbreviate-arg + "Shortens a command arg for logging: multi-line scripts become a line count + and long args are truncated." + [arg] + (let [lines (str/split-lines arg)] + (cond + (< 1 (count lines)) (str "<" (count lines) "-line script>") + (< 60 (count arg)) (str (subs arg 0 40) "…") + :else arg))) + (defn smoke-test "Runs the variant's smoke test commands in `image-tag`. Returns true if they all succeed." @@ -63,7 +73,7 @@ (let [run-cmd (remove nil? (concat ["docker" "run" "--rm" platform-flag image-tag] cmd)) - _ (apply log "Running" run-cmd) + _ (apply log "Running" (map abbreviate-arg run-cmd)) {:keys [out err exit]} (apply sh run-cmd)] (if (zero? exit) true diff --git a/src/docker_clojure/dockerfile.clj b/src/docker_clojure/dockerfile.clj index e5260a18..99a6045c 100644 --- a/src/docker_clojure/dockerfile.clj +++ b/src/docker_clojure/dockerfile.clj @@ -17,6 +17,7 @@ build-tool)])) (defn all-prereqs [dir variant] + (lein/prereqs dir variant) (tools-deps/prereqs dir variant)) (defn copy-java? diff --git a/src/docker_clojure/dockerfile/lein.clj b/src/docker_clojure/dockerfile/lein.clj index 51fa4996..f644d8a9 100644 --- a/src/docker_clojure/dockerfile/lein.clj +++ b/src/docker_clojure/dockerfile/lein.clj @@ -1,9 +1,18 @@ (ns docker-clojure.dockerfile.lein (:require [docker-clojure.dockerfile.shared - :refer [install-distro-deps render-template + :refer [copy-resource-file! install-distro-deps render-template uninstall-distro-build-deps]])) -(defn prereqs [_ _] nil) +;; Leiningen release tags are signed with this key (Phil Hagelberg). +(def ^:const signing-key "9D13D9426A0814B3373CF5E3D8A8243577A7859F") + +;; Local copy of `signing-key` (exported with `--export-options export-minimal`) +;; so builds don't depend on the keyserver being up. Builds still check it +;; against keyserver.ubuntu.com and fail if the two have diverged. +(def ^:const signing-key-file "lein-signing-key.asc") + +(defn prereqs [dir _variant] + (copy-resource-file! dir signing-key-file)) ;; Leiningen no longer publishes a standalone uberjar, so we build it from ;; source: clone the GPG-signed release tag, bootstrap leiningen-core's deps @@ -27,15 +36,13 @@ ;; first use. (def ^:const bundled-clojure-version "1.12.5") -;; Leiningen release tags are signed with this key (Phil Hagelberg). -(def ^:const signing-key "9D13D9426A0814B3373CF5E3D8A8243577A7859F") - ;; The commit each release tag is expected to point at. After cloning we assert ;; HEAD matches, so a moved or re-pointed upstream tag can't slip a different ;; commit past us (belt-and-suspenders with `git verify-tag`). Requested by the ;; docker-library/official-images maintainers. (def release-commits - {"2.13.0" "d703e4802feb3e5c3fa9ae9f1874fb7a3a3e3030"}) + {"2.13.0" "d703e4802feb3e5c3fa9ae9f1874fb7a3a3e3030" + "2.13.1" "d109a01c2bc295c1ed218ed335c36481938b745b"}) (defn release-commit [version] (or (get release-commits version) @@ -50,6 +57,7 @@ :lein-commit (release-commit build-tool-version) :clojure-version bundled-clojure-version :gpg-key signing-key + :gpg-key-file signing-key-file :install-deps (install-deps variant) :uninstall-deps (uninstall-build-deps variant)})) diff --git a/src/docker_clojure/lein_key.clj b/src/docker_clojure/lein_key.clj new file mode 100644 index 00000000..8f47f291 --- /dev/null +++ b/src/docker_clojure/lein_key.clj @@ -0,0 +1,104 @@ +(ns docker-clojure.lein-key + "Refreshes the local copy of Leiningen's release signing key that lein builds + import (and check against the keyserver) instead of relying on the keyserver + being up." + (:require [clojure.java.io :as io] + [clojure.java.shell :refer [sh]] + [clojure.string :as str] + [docker-clojure.dockerfile.lein :as lein]) + (:import (java.nio.file Files) + (java.nio.file.attribute FileAttribute PosixFilePermissions))) + +(def keyserver "hkps://keyserver.ubuntu.com") + +(def key-file (io/file "resources" lein/signing-key-file)) + +(def ^:private fetch-attempts 3) + +(defn primary-fingerprints + "Returns the fingerprints of the primary keys in gpg `--with-colons` output. + Each `pub` record is followed by its `fpr` record; subkeys' `fpr` records + follow `sub` records instead." + [colons] + (->> (str/split-lines colons) + (map #(str/split % #":" -1)) + (partition 2 1) + (keep (fn [[prev cur]] + (when (and (= "pub" (first prev)) (= "fpr" (first cur))) + (nth cur 9)))))) + +(defn summary + "The parts of gpg `--with-colons` output that describe the key itself, the + same fields lein builds compare against the keyserver." + [colons] + (->> (str/split-lines colons) + (map #(str/split % #":" -1)) + (filter #(#{"pub" "sub" "uid" "fpr"} (first %))) + (map #(str/join " " (map (fn [i] (get % i "")) [0 1 4 5 6 7 9 11]))) + (str/join "\n"))) + +(defn- gpg [home & args] + (apply sh "gpg" "--batch" "--homedir" (str home) args)) + +(defn- temp-home [] + (Files/createTempDirectory + "lein-key" (into-array FileAttribute + [(PosixFilePermissions/asFileAttribute + (PosixFilePermissions/fromString "rwx------"))]))) + +(defn- delete-tree [dir] + (doseq [f (reverse (file-seq (io/file (str dir))))] + (io/delete-file f true))) + +(defn- fetch! + "Fetches the signing key into `home`, retrying since the keyserver is flaky. + Returns nil on success or the last error." + [home] + (loop [attempt 1] + (let [{:keys [exit err]} (gpg home "--keyserver" keyserver + "--recv-keys" lein/signing-key)] + (cond + (zero? exit) nil + (< attempt fetch-attempts) (do (println "Fetch attempt" attempt + "failed, retrying:" (str/trim err)) + (Thread/sleep (* 2000 attempt)) + (recur (inc attempt))) + :else err)))) + +(defn refresh! + "Fetches Leiningen's signing key from the keyserver, checks it's the pinned + key, and writes it (minus third-party signatures) to `key-file`. Returns true + on success." + [] + (let [home (temp-home)] + (try + (println "Fetching" lein/signing-key "from" keyserver "...") + (if-let [err (fetch! home)] + (do (println "ERROR fetching key:" (str/trim err)) + false) + (let [colons (:out (gpg home "--with-colons" "--list-keys")) + primary (primary-fingerprints colons)] + (if (not= [lein/signing-key] primary) + (do (println "ERROR: expected only key" lein/signing-key + "from the keyserver but got" (pr-str primary)) + false) + (let [{:keys [exit out err]} + (gpg home "--armor" "--export-options" "export-minimal" + "--export" lein/signing-key) + old (when (.exists key-file) (slurp key-file))] + (if-not (and (zero? exit) (str/includes? out "BEGIN PGP")) + (do (println "ERROR exporting key:" (str/trim err)) + false) + (do (spit key-file out) + (println (if (= old out) + (str key-file " is already up to date.") + (str "Updated " key-file "."))) + (println (str "\n" (summary colons) "\n")) + (when (not= old out) + (println "Review the change (e.g. a revocation or new" + "subkey) before committing it, then run" + "`bb dockerfiles`.")) + true)))))) + (finally + (sh "gpgconf" "--homedir" (str home) "--kill" "all") + (delete-tree home))))) diff --git a/test/docker_clojure/docker_test.clj b/test/docker_clojure/docker_test.clj index 69af0280..aa8ae970 100644 --- a/test/docker_clojure/docker_test.clj +++ b/test/docker_clojure/docker_test.clj @@ -70,6 +70,15 @@ :debian/bookworm "tools-deps-bookworm" :debian-slim/bookworm-slim "tools-deps-bookworm-slim"))) +(deftest abbreviate-arg-test + (testing "leaves short args alone" + (is (= "version" (abbreviate-arg "version")))) + (testing "replaces multi-line scripts with their line count" + (is (= "<3-line script>" (abbreviate-arg "set -e\necho hi\nexit 0")))) + (testing "truncates long args" + (is (= (str (apply str (repeat 40 "x")) "…") + (abbreviate-arg (apply str (repeat 61 "x"))))))) + (deftest smoke-test-cmds-test (testing "every variant checks its build tools start" (are [build-tool jdk-version cmds] diff --git a/test/docker_clojure/dockerfile_test.clj b/test/docker_clojure/dockerfile_test.clj index 44390108..6dd5a11d 100644 --- a/test/docker_clojure/dockerfile_test.clj +++ b/test/docker_clojure/dockerfile_test.clj @@ -12,6 +12,29 @@ (build-dir {:base-image-tag "openjdk:11-alpine" :build-tool "lein"}))))) +(deftest lein-signing-key-test + (testing "lein prereqs put the signing key in the build dir" + (let [dir (doto (java.io.File/createTempFile "lein-prereqs" "") + (.delete) + (.mkdirs)) + key (java.io.File. dir lein/signing-key-file)] + (try + (lein/prereqs (str dir) {}) + (is (str/starts-with? (slurp key) + "-----BEGIN PGP PUBLIC KEY BLOCK-----")) + (finally + (.delete key) + (.delete dir))))) + (testing "lein Dockerfile imports the local key and checks the keyserver" + (let [install (lein/install cfg/installer-hashes + {:distro :debian/bookworm + :build-tool-version "2.13.1"})] + (is (str/includes? install (str "COPY " lein/signing-key-file))) + (is (str/includes? install (str "gpg --batch --import /tmp/" + lein/signing-key-file))) + (is (str/includes? install (str "--recv-keys " lein/signing-key))) + (is (str/includes? install "bb refresh-lein-key"))))) + (deftest contents-test (testing "includes 'FROM base-image'" (is (str/includes? (contents cfg/installer-hashes diff --git a/test/docker_clojure/lein_key_test.clj b/test/docker_clojure/lein_key_test.clj new file mode 100644 index 00000000..5c273ab0 --- /dev/null +++ b/test/docker_clojure/lein_key_test.clj @@ -0,0 +1,36 @@ +(ns docker-clojure.lein-key-test + (:require [clojure.string :as str] + [clojure.test :refer [deftest is testing]] + [docker-clojure.lein-key :as lein-key])) + +(def colons + (str/join + "\n" + ["tru::1:1762870000:0:3:1:5" + "pub:-:4096:1:D8A8243577A7859F:1684357211:::-:::scESC::::::23::0:" + "fpr:::::::::9D13D9426A0814B3373CF5E3D8A8243577A7859F:" + "uid:-::::1684357211::972BED54DF6E1907FE129BFA8E51BA51411F22ED::Phil Hagelberg ::::::::::0:" + "sub:-:4096:1:4F1856267D44B221:1684357211::::::e::::::23:" + "fpr:::::::::B35D39C992B539C8EF362D9D4F1856267D44B221:"])) + +(deftest primary-fingerprints-test + (testing "returns primary key fingerprints but not subkey ones" + (is (= ["9D13D9426A0814B3373CF5E3D8A8243577A7859F"] + (lein-key/primary-fingerprints colons)))) + (testing "returns every primary key" + (is (= ["AAAA" "BBBB"] + (lein-key/primary-fingerprints + (str/join "\n" ["pub:-:::::" "fpr:::::::::AAAA:" + "sub:-:::::" "fpr:::::::::CCCC:" + "pub:-:::::" "fpr:::::::::BBBB:"])))))) + +(deftest summary-test + (testing "keeps the key-describing fields and drops import metadata" + (is (= (str/join + "\n" + ["pub - D8A8243577A7859F 1684357211 scESC" + "fpr 9D13D9426A0814B3373CF5E3D8A8243577A7859F " + "uid - 1684357211 972BED54DF6E1907FE129BFA8E51BA51411F22ED Phil Hagelberg " + "sub - 4F1856267D44B221 1684357211 e" + "fpr B35D39C992B539C8EF362D9D4F1856267D44B221 "]) + (lein-key/summary colons)))))