| git.druid.rocks | index | druid520 | radium | radium-sign.el |
radium-sign.el
;;; radium-sign.el, sign/verify the current file: ssh or signify -*- lexical-binding: t; -*-
;;
;; item 11: "which keys configurable/asked" - the METHOD (ssh vs
;; signify) is a defcustom, set once, not asked every time (whichever
;; of the two is actually installed wins if only one is); the KEY is
;; asked every time via completing-read, since which key to sign with
;; is exactly the kind of per-use decision a default would get wrong
;; half the time. output is always FILE.sig next to FILE, the same
;; sigfile convention both tools and git's own ssh-signing already use.
;;
;; ssh signing (openssh >= 8.2, `ssh-keygen -Y sign/verify') needs a
;; NAMESPACE - openssh's own manpage example for signing an arbitrary
;; file (not a git object, not an email) uses "file", so that's the
;; default here too (radium-sign-ssh-namespace, still a real defcustom
;; if something else is ever wanted). candidate keys are ~/.ssh/*.pub:
;; `ssh-keygen -Y sign -f pubkey' signs through ssh-agent when the
;; matching private key is loaded there rather than needing the
;; private key file itself touched by this elisp at all - the safer of
;; the two ways ssh-keygen -Y sign can be given a key.
;;
;; signify has no equivalent agent-mediated mode - -s wants the real
;; secret key file - so candidates come from radium-sign-signify-key-dir
;; (~/.signify by default, configurable: signify itself has no fixed
;; per-user convention the way ssh has ~/.ssh).
(defgroup radium nil
"this config's own settings - one shared group so `M-x customize-group
radium' finds all of them, rather than each defcustom scattered under
whichever built-in group happens to be nearby."
:group 'emacs)
(defcustom radium-sign-method 'ssh
"which tool C-c v S/V use: `ssh' or `signify'. if the one this is set
to isn't actually installed, radium-sign falls back to whichever one
is - and only errors if neither is."
:type '(choice (const ssh) (const signify))
:group 'radium)
(defcustom radium-sign-ssh-namespace "file"
"the -n namespace `ssh-keygen -Y sign/verify' signs/checks under -
openssh's own manpage example for signing a plain file, not a git
object or an email, uses exactly this."
:type 'string
:group 'radium)
(defcustom radium-sign-signify-key-dir "~/.signify"
"where C-c v S/V look for signify *.sec/*.pub keys - signify itself
has no fixed convention for this the way ssh has ~/.ssh."
:type 'directory
:group 'radium)
(defun radium-sign--available-methods ()
(delq nil (list (and (executable-find "ssh-keygen") 'ssh)
(and (executable-find "signify") 'signify))))
(defun radium-sign--method ()
"radium-sign-method if it's actually installed, else whichever
available method there is, else a real error - never a silent no-op."
(let ((avail (radium-sign--available-methods)))
(cond
((memq radium-sign-method avail) radium-sign-method)
(avail (car avail))
(t (user-error "radium-sign: neither ssh-keygen nor signify is installed")))))
(defun radium-sign--ssh-keys ()
(file-expand-wildcards (expand-file-name "*.pub" "~/.ssh")))
(defun radium-sign--signify-keys (ext)
(file-expand-wildcards (expand-file-name (concat "*." ext) radium-sign-signify-key-dir)))
(defun radium-sign--read-key (candidates prompt)
"completing-read over CANDIDATES if there are any, else a real
read-file-name - never assume the file i actually want is in the
list, just offer what's there to complete against."
(if candidates
(completing-read prompt candidates nil nil)
(read-file-name prompt)))
(defun radium-sign-file ()
"sign the file this buffer visits (C-c v S): ssh-keygen -Y sign or
signify -S, whichever radium-sign--method resolves to, with the key
asked every time. writes FILE.sig next to FILE."
(interactive)
(let* ((file (or buffer-file-name (user-error "radium-sign: buffer has no file")))
(method (radium-sign--method)))
(pcase method
('ssh
(let* ((key (radium-sign--read-key (radium-sign--ssh-keys) "sign with key: "))
(buf (get-buffer-create "*radium-sign*")))
(if (zerop (call-process "ssh-keygen" nil buf nil
"-Y" "sign" "-f" key "-n" radium-sign-ssh-namespace file))
(message "radium-sign: signed %s -> %s.sig" (file-name-nondirectory file) (file-name-nondirectory file))
(display-buffer buf)
(user-error "radium-sign: ssh-keygen failed, see *radium-sign*"))))
('signify
(let* ((key (radium-sign--read-key (radium-sign--signify-keys "sec") "sign with key: "))
(sig (concat file ".sig"))
(buf (get-buffer-create "*radium-sign*")))
(if (zerop (call-process "signify" nil buf nil
"-S" "-s" key "-m" file "-x" sig))
(message "radium-sign: signed %s -> %s" (file-name-nondirectory file) (file-name-nondirectory sig))
(display-buffer buf)
(user-error "radium-sign: signify failed, see *radium-sign*")))))))
(defun radium-verify-file ()
"verify FILE.sig against the file this buffer visits (C-c v V),
same method/key-prompt shape as radium-sign-file."
(interactive)
(let* ((file (or buffer-file-name (user-error "radium-verify: buffer has no file")))
(sig (concat file ".sig"))
(method (radium-sign--method)))
(unless (file-exists-p sig)
(user-error "radium-verify: no %s" sig))
(pcase method
('ssh
;; -Y verify has no "here's the public key" flag at all - only
;; -f ALLOWED-SIGNERS-FILE (an authorized_keys-shaped file:
;; "identity [namespaces=\"ns\"] keytype keydata") plus -I to
;; name which identity in it to check against, and the message
;; itself is read from stdin, not a flag - confirmed directly
;; against a real generated key/signature pair, not assumed
;; from the sign side's own flags.
(let* ((key (radium-sign--read-key (radium-sign--ssh-keys) "verify against key: "))
(allowed (make-temp-file "radium-sign-allowed"))
(buf (get-buffer-create "*radium-sign*")))
(unwind-protect
(progn
(with-temp-file allowed
(insert "radium-sign " (with-temp-buffer
(insert-file-contents key)
(string-trim (buffer-string))) "\n"))
(if (zerop (call-process "ssh-keygen" file buf nil
"-Y" "verify" "-f" allowed "-I" "radium-sign"
"-n" radium-sign-ssh-namespace "-s" sig))
(message "radium-verify: %s: signature OK" (file-name-nondirectory file))
(display-buffer buf)
(user-error "radium-verify: signature check FAILED, see *radium-sign*")))
(delete-file allowed))))
('signify
(let* ((key (radium-sign--read-key (radium-sign--signify-keys "pub") "verify against key: "))
(buf (get-buffer-create "*radium-sign*")))
(if (zerop (call-process "signify" nil buf nil
"-V" "-p" key "-m" file "-x" sig))
(message "radium-verify: %s: signature OK" (file-name-nondirectory file))
(display-buffer buf)
(user-error "radium-verify: signature check FAILED, see *radium-sign*")))))))
(global-set-key (kbd "C-c v S") #'radium-sign-file)
(global-set-key (kbd "C-c v V") #'radium-verify-file)
(provide 'radium-sign)