Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bb.edn
Original file line number Diff line number Diff line change
Expand Up @@ -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"]})}
Expand Down
51 changes: 51 additions & 0 deletions resources/lein-signing-key.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

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
=/HwY
-----END PGP PUBLIC KEY BLOCK-----
29 changes: 28 additions & 1 deletion resources/templates/lein.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down
2 changes: 1 addition & 1 deletion src/docker_clojure/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 9 additions & 1 deletion src/docker_clojure/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down Expand Up @@ -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
Expand All @@ -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."
Expand Down
12 changes: 11 additions & 1 deletion src/docker_clojure/docker.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/docker_clojure/dockerfile.clj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
build-tool)]))

(defn all-prereqs [dir variant]
(lein/prereqs dir variant)
(tools-deps/prereqs dir variant))

(defn copy-java?
Expand Down
20 changes: 14 additions & 6 deletions src/docker_clojure/dockerfile/lein.clj
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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)}))

Expand Down
104 changes: 104 additions & 0 deletions src/docker_clojure/lein_key.clj
Original file line number Diff line number Diff line change
@@ -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)))))
9 changes: 9 additions & 0 deletions test/docker_clojure/docker_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
23 changes: 23 additions & 0 deletions test/docker_clojure/dockerfile_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading