Merge pull request #2279 from gonewest818/unattended-signatures

invoke GPG unattended with passphrase
This commit is contained in:
Phil Hagelberg 2018-01-30 19:28:37 -08:00 committed by GitHub
commit c3de05b51f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 137 additions and 26 deletions

View file

@ -16,12 +16,14 @@
- [How Leiningen uses GPG](#how-leiningen-uses-gpg)
- [Signing a file](#signing-a-file)
- [Overriding the gpg defaults](#overriding-the-gpg-defaults)
- [Setting the gpg passphrase for unattended deploys](#setting-the-gpg-passphrase-for-unattended-deploys)
- [Troubleshooting](#troubleshooting)
- [Debian based distributions](#debian-based-distributions-1)
- [gpg: can't query passphrase in batch mode](#gpg-cant-query-passphrase-in-batch-mode)
- [Mac OSX](#mac-osx)
- [Unable to get GPG installed via Homebrew and OSX Keychain to work](#unable-to-get-gpg-installed-via-homebrew-and-osx-keychain-to-work)
- [GPG doesn't ask for a passphrase](#gpg-doesnt-ask-for-a-passphrase)
- [gpg: decryption failed: secret key not available](#gpg-decryption-failed-secret-key-not-available)
- [GPG prompts for passphrase but does not work with Leiningen](#gpg-prompts-for-passphrase-but-does-not-work-with-leiningen)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@ -250,6 +252,33 @@ repository specification in your project definition:
["snapshots" "https://blueant.com/archiva/internal/snapshots"]]
...)
### Setting the gpg passphrase for unattended deploys
It's also possible to provide the passphrase required to unlock your
keyring. This is meant only for unattended deploys, for example in a
continuous integration system like Travis CI or CircleCI or Jenkins.
Passphrase can be configured in the environment:
(defproject ham-biscuit "0.1.0"
...
:signing {:gpg-key "bob@bobsons.net"
:gpg-passphrase :env/gpgpass} ;; looks up GPGPASS from env
...)
In your CI service your gpg keyring will need to be encrypted and
injected into the build, and the passphrase likewise encrypted such that
the environment variable is visible only to the build.
For testing purposes the pasphrase can also be set as a string literal
but this is strongly discouraged in any production usage.
(defproject ham-biscuit "0.1.0"
...
:signing {:gpg-key "bob@bobsons.net"
:gpg-passphrase "my-passphrase-in-the-clear"}
...)
## Troubleshooting
### Debian based distributions

View file

@ -2,7 +2,6 @@
"Functions exposing user-level configuration."
(:require [clojure.java.io :as io]
[clojure.string :as str]
[clojure.java.shell :as shell]
[leiningen.core.utils :as utils])
(:import (com.hypirion.io Pipe)
(org.apache.commons.io.output TeeOutputStream)
@ -104,9 +103,14 @@
[env]
(into-array String (map (fn [[k v]] (str (name k) "=" v)) env)))
(defn gpg
"Shells out to (gpg-program) with the given arguments"
[& args]
(defn gpg-with-passphrase
"Shells out to (gpg-program) with the given arguments and, if
passphrase is not nil, sends the passphrase on stdin for unattended
operations such as signing artifacts for deployment. When a passphrase
is provided the caller must include the following args
[\"--passphrase-fd\" \"0\" \"--pinentry-mode\" \"loopback\"]
along with whatever other args are needed for the gpg command."
[passphrase & args]
(try
(let [proc-env (as-env-strings (get-english-env))
proc-args (into-array String (concat [(gpg-program)] args))
@ -114,10 +118,15 @@
(.addShutdownHook (Runtime/getRuntime)
(Thread. (fn [] (.destroy proc))))
(with-open [out (.getInputStream proc)
err (.getErrorStream proc)
err-output (ByteArrayOutputStream.)]
(let [pump-err (doto (Pipe. (.getErrorStream proc)
(TeeOutputStream. System/err err-output))
.start)]
(if passphrase
(with-open [in (.getOutputStream proc)]
(io/copy passphrase in)))
(let [pump-err (doto (Pipe. err
(TeeOutputStream. System/err
err-output))
.start)]
(.join pump-err)
(let [exit-code (.waitFor proc)]
{:exit exit-code
@ -126,11 +135,26 @@
(catch java.io.IOException e
{:exit 1 :out "" :err (.getMessage e)})))
(defn gpg
"Shells out to (gpg-program) with the given arguments"
[& args]
(apply gpg-with-passphrase nil args))
(defn gpg-available?
"Verifies (gpg-program) exists"
[]
(zero? (:exit (gpg "--version"))))
(defn gpg-version
"parse and return the version of gpg available"
[]
(let [pattern #"gpg\s+\(GnuPG\)\s+(\d+)\.(\d+)\.(\d+)"]
(if-let [[_ major minor patch]
(re-find pattern (:out (gpg "--version")))]
{:major (Integer/parseInt major)
:minor (Integer/parseInt minor)
:patch (Integer/parseInt patch)})))
(defn credentials-fn
"Decrypt map from credentials.clj.gpg in Leiningen home if present."
([] (let [cred-file (io/file (leiningen-home) "credentials.clj.gpg")]
@ -156,25 +180,37 @@
(re-find re? (:url settings)))]
cred))))
(defn resolve-env-keyword
"Resolve usage of :env and :env/foo in project.clj"
[k v]
(cond (= :env v)
(getenv (str "LEIN_"
(-> (name k)
(str/upper-case)
(str/replace "-" "_"))))
(and (keyword? v) (= "env" (namespace v)))
(getenv (str/upper-case (name v)))
:else nil))
(defn- resolve-gpg-keyword
"Resolve usage of :gpg in project.clj"
[source-settings k v]
(cond (= :gpg v)
(get (match-credentials source-settings (credentials)) k)))
(defn- resolve-credential
"Resolve key-value pair from result into a credential, updating result."
[source-settings result [k v]]
(letfn [(resolve [v]
(cond (= :env v)
(getenv (str "LEIN_" (str/upper-case (name k))))
(and (keyword? v) (= "env" (namespace v)))
(getenv (str/upper-case (name v)))
(= :gpg v)
(get (match-credentials source-settings (credentials)) k)
(coll? v) ;; collection of places to look
(or (resolve-env-keyword k v)
(resolve-gpg-keyword source-settings k v)
(if (coll? v) ;; collection of places to look
(->> (map resolve v)
(remove nil?)
first)
:else v))]
first))
v))]
(if (#{:username :password :passphrase :private-key-file} k)
(assoc result k (resolve v))
(assoc result k v))))

View file

@ -74,16 +74,37 @@
(add-auth-interactively))))
(defn signing-args
"Produce GPG arguments for signing a file."
"Produce GPG arguments for signing a file, taking the version of gpg
into account as necessary."
[file opts]
(let [key-spec (if-let [key (:gpg-key opts)]
["--default-key" key])]
`["--yes" "-ab" ~@key-spec "--" ~file]))
(let [key-args (concat
(if-let [key (:gpg-key opts)]
["--default-key" key])
(if (:gpg-passphrase opts)
(let [{:keys [major minor patch]} (user/gpg-version)
version (+ major (/ minor 10.))]
(if (> version 2.0)
; gpg 2.1 and newer
["--passphrase-fd" "0"
"--pinentry-mode" "loopback"]
; gpg 2.0 and older
["--passphrase-fd" "0" "--batch"]))))]
`["--yes" "-ab" ~@key-args "--" ~file]))
(defn signing-passphrase
"Produce GPG passphrase if specified in project"
[opts]
(if-let [pp (:gpg-passphrase opts)]
(or (user/resolve-env-keyword :gpg-passphrase pp)
pp)))
(defn sign
"Create a detached signature and return the signature file name."
[file opts]
(let [{:keys [err exit]} (apply user/gpg (signing-args file opts))]
(let [pass (signing-passphrase opts)
args (signing-args file opts)
_ (main/info "Signing: gpg " args)
{:keys [err exit]} (apply user/gpg-with-passphrase (cons pass args))]
(when-not (zero? exit)
(main/abort "Could not sign"
(str file "\n" err (if err "\n")

View file

@ -2,6 +2,7 @@
(:use [clojure.test]
[clojure.java.io :only [file]]
[leiningen.deploy]
[leiningen.core.user :as user]
[leiningen.test.helper :only [delete-file-recursively
tmp-dir sample-project
sample-deploy-project]]))
@ -66,7 +67,31 @@
(is (= (signing-args "foo.jar" nil)
["--yes" "-ab" "--" "foo.jar"]))
(is (= (signing-args "foo.jar" {:gpg-key "123456"})
["--yes" "-ab" "--default-key" "123456" "--" "foo.jar"])))
["--yes" "-ab" "--default-key" "123456" "--" "foo.jar"]))
(with-redefs [user/gpg-version (fn [] {:major 2 :minor 1 :patch 0})]
(is (= (signing-args "foo.jar" {:gpg-key "123456" :gpg-passphrase "abc"})
["--yes" "-ab" "--default-key" "123456"
"--passphrase-fd" "0" "--pinentry-mode" "loopback"
"--" "foo.jar"]))
(is (= (signing-args "foo.jar" {:gpg-passphrase "abc"})
["--yes" "-ab"
"--passphrase-fd" "0" "--pinentry-mode" "loopback"
"--" "foo.jar"])))
(with-redefs [user/gpg-version (fn [] {:major 1 :minor 4 :patch 0})]
(is (= (signing-args "foo.jar" {:gpg-key "123456" :gpg-passphrase "abc"})
["--yes" "-ab" "--default-key" "123456"
"--passphrase-fd" "0" "--batch"
"--" "foo.jar"]))
(is (= (signing-args "foo.jar" {:gpg-passphrase "abc"})
["--yes" "-ab"
"--passphrase-fd" "0" "--batch"
"--" "foo.jar"])))
(is (= (signing-passphrase {}) nil))
(is (= (signing-passphrase {:gpg-passphrase "abc"}) "abc"))
(with-redefs [user/getenv (fn [v] (if (= v "LEIN_GPG_PASSPHRASE") "abc" nil))]
(is (= (signing-passphrase {:gpg-passphrase :env}) "abc")))
(with-redefs [user/getenv (fn [v] (if (= v "MYPASS") "abc" nil))]
(is (= (signing-passphrase {:gpg-passphrase :env/mypass}) "abc"))))
(testing "Key selection"
(is (= (:gpg-key (signing-opts {:signing {:gpg-key "key-project"}}
["repo" {:signing {:gpg-key "key-repo"}}]))