Merge pull request #2279 from gonewest818/unattended-signatures
invoke GPG unattended with passphrase
This commit is contained in:
commit
c3de05b51f
4 changed files with 137 additions and 26 deletions
29
doc/GPG.md
29
doc/GPG.md
|
@ -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
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"}}]))
|
||||
|
|
Loading…
Reference in a new issue