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)
|
- [How Leiningen uses GPG](#how-leiningen-uses-gpg)
|
||||||
- [Signing a file](#signing-a-file)
|
- [Signing a file](#signing-a-file)
|
||||||
- [Overriding the gpg defaults](#overriding-the-gpg-defaults)
|
- [Overriding the gpg defaults](#overriding-the-gpg-defaults)
|
||||||
|
- [Setting the gpg passphrase for unattended deploys](#setting-the-gpg-passphrase-for-unattended-deploys)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
- [Debian based distributions](#debian-based-distributions-1)
|
- [Debian based distributions](#debian-based-distributions-1)
|
||||||
- [gpg: can't query passphrase in batch mode](#gpg-cant-query-passphrase-in-batch-mode)
|
- [gpg: can't query passphrase in batch mode](#gpg-cant-query-passphrase-in-batch-mode)
|
||||||
- [Mac OSX](#mac-osx)
|
- [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)
|
- [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 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)
|
- [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 -->
|
<!-- 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"]]
|
["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
|
## Troubleshooting
|
||||||
|
|
||||||
### Debian based distributions
|
### Debian based distributions
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
"Functions exposing user-level configuration."
|
"Functions exposing user-level configuration."
|
||||||
(:require [clojure.java.io :as io]
|
(:require [clojure.java.io :as io]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[clojure.java.shell :as shell]
|
|
||||||
[leiningen.core.utils :as utils])
|
[leiningen.core.utils :as utils])
|
||||||
(:import (com.hypirion.io Pipe)
|
(:import (com.hypirion.io Pipe)
|
||||||
(org.apache.commons.io.output TeeOutputStream)
|
(org.apache.commons.io.output TeeOutputStream)
|
||||||
|
@ -104,9 +103,14 @@
|
||||||
[env]
|
[env]
|
||||||
(into-array String (map (fn [[k v]] (str (name k) "=" v)) env)))
|
(into-array String (map (fn [[k v]] (str (name k) "=" v)) env)))
|
||||||
|
|
||||||
(defn gpg
|
(defn gpg-with-passphrase
|
||||||
"Shells out to (gpg-program) with the given arguments"
|
"Shells out to (gpg-program) with the given arguments and, if
|
||||||
[& args]
|
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
|
(try
|
||||||
(let [proc-env (as-env-strings (get-english-env))
|
(let [proc-env (as-env-strings (get-english-env))
|
||||||
proc-args (into-array String (concat [(gpg-program)] args))
|
proc-args (into-array String (concat [(gpg-program)] args))
|
||||||
|
@ -114,10 +118,15 @@
|
||||||
(.addShutdownHook (Runtime/getRuntime)
|
(.addShutdownHook (Runtime/getRuntime)
|
||||||
(Thread. (fn [] (.destroy proc))))
|
(Thread. (fn [] (.destroy proc))))
|
||||||
(with-open [out (.getInputStream proc)
|
(with-open [out (.getInputStream proc)
|
||||||
|
err (.getErrorStream proc)
|
||||||
err-output (ByteArrayOutputStream.)]
|
err-output (ByteArrayOutputStream.)]
|
||||||
(let [pump-err (doto (Pipe. (.getErrorStream proc)
|
(if passphrase
|
||||||
(TeeOutputStream. System/err err-output))
|
(with-open [in (.getOutputStream proc)]
|
||||||
.start)]
|
(io/copy passphrase in)))
|
||||||
|
(let [pump-err (doto (Pipe. err
|
||||||
|
(TeeOutputStream. System/err
|
||||||
|
err-output))
|
||||||
|
.start)]
|
||||||
(.join pump-err)
|
(.join pump-err)
|
||||||
(let [exit-code (.waitFor proc)]
|
(let [exit-code (.waitFor proc)]
|
||||||
{:exit exit-code
|
{:exit exit-code
|
||||||
|
@ -126,11 +135,26 @@
|
||||||
(catch java.io.IOException e
|
(catch java.io.IOException e
|
||||||
{:exit 1 :out "" :err (.getMessage 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?
|
(defn gpg-available?
|
||||||
"Verifies (gpg-program) exists"
|
"Verifies (gpg-program) exists"
|
||||||
[]
|
[]
|
||||||
(zero? (:exit (gpg "--version"))))
|
(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
|
(defn credentials-fn
|
||||||
"Decrypt map from credentials.clj.gpg in Leiningen home if present."
|
"Decrypt map from credentials.clj.gpg in Leiningen home if present."
|
||||||
([] (let [cred-file (io/file (leiningen-home) "credentials.clj.gpg")]
|
([] (let [cred-file (io/file (leiningen-home) "credentials.clj.gpg")]
|
||||||
|
@ -156,25 +180,37 @@
|
||||||
(re-find re? (:url settings)))]
|
(re-find re? (:url settings)))]
|
||||||
cred))))
|
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
|
(defn- resolve-credential
|
||||||
"Resolve key-value pair from result into a credential, updating result."
|
"Resolve key-value pair from result into a credential, updating result."
|
||||||
[source-settings result [k v]]
|
[source-settings result [k v]]
|
||||||
(letfn [(resolve [v]
|
(letfn [(resolve [v]
|
||||||
(cond (= :env v)
|
(or (resolve-env-keyword k v)
|
||||||
(getenv (str "LEIN_" (str/upper-case (name k))))
|
(resolve-gpg-keyword source-settings k v)
|
||||||
|
(if (coll? v) ;; collection of places to look
|
||||||
(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
|
|
||||||
(->> (map resolve v)
|
(->> (map resolve v)
|
||||||
(remove nil?)
|
(remove nil?)
|
||||||
first)
|
first))
|
||||||
|
v))]
|
||||||
:else v))]
|
|
||||||
(if (#{:username :password :passphrase :private-key-file} k)
|
(if (#{:username :password :passphrase :private-key-file} k)
|
||||||
(assoc result k (resolve v))
|
(assoc result k (resolve v))
|
||||||
(assoc result k v))))
|
(assoc result k v))))
|
||||||
|
|
|
@ -74,16 +74,37 @@
|
||||||
(add-auth-interactively))))
|
(add-auth-interactively))))
|
||||||
|
|
||||||
(defn signing-args
|
(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]
|
[file opts]
|
||||||
(let [key-spec (if-let [key (:gpg-key opts)]
|
(let [key-args (concat
|
||||||
["--default-key" key])]
|
(if-let [key (:gpg-key opts)]
|
||||||
`["--yes" "-ab" ~@key-spec "--" ~file]))
|
["--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
|
(defn sign
|
||||||
"Create a detached signature and return the signature file name."
|
"Create a detached signature and return the signature file name."
|
||||||
[file opts]
|
[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)
|
(when-not (zero? exit)
|
||||||
(main/abort "Could not sign"
|
(main/abort "Could not sign"
|
||||||
(str file "\n" err (if err "\n")
|
(str file "\n" err (if err "\n")
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
(:use [clojure.test]
|
(:use [clojure.test]
|
||||||
[clojure.java.io :only [file]]
|
[clojure.java.io :only [file]]
|
||||||
[leiningen.deploy]
|
[leiningen.deploy]
|
||||||
|
[leiningen.core.user :as user]
|
||||||
[leiningen.test.helper :only [delete-file-recursively
|
[leiningen.test.helper :only [delete-file-recursively
|
||||||
tmp-dir sample-project
|
tmp-dir sample-project
|
||||||
sample-deploy-project]]))
|
sample-deploy-project]]))
|
||||||
|
@ -66,7 +67,31 @@
|
||||||
(is (= (signing-args "foo.jar" nil)
|
(is (= (signing-args "foo.jar" nil)
|
||||||
["--yes" "-ab" "--" "foo.jar"]))
|
["--yes" "-ab" "--" "foo.jar"]))
|
||||||
(is (= (signing-args "foo.jar" {:gpg-key "123456"})
|
(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"
|
(testing "Key selection"
|
||||||
(is (= (:gpg-key (signing-opts {:signing {:gpg-key "key-project"}}
|
(is (= (:gpg-key (signing-opts {:signing {:gpg-key "key-project"}}
|
||||||
["repo" {:signing {:gpg-key "key-repo"}}]))
|
["repo" {:signing {:gpg-key "key-repo"}}]))
|
||||||
|
|
Loading…
Reference in a new issue