Subject: Add support for TLS client certificates to 'erc-tls'
From: Amin Bandali <[email-masked]>
To: [email-masked]
Cc: [email-masked], [email-masked]
Date: Thu, 15 Apr 2021 00:16:38 -0400
List-Id: <debbugs-submit.debbugs.gnu.org>

Tags: patch

After a few recent changes in my IRC/ERC setup, I felt ERC's lack of
support for connecting to IRC networks using TLS client certificates as
an alternative to traditional password-based authentication.  So I spent
some time a few nights ago and wired things up in ERC to allow passing a
client certificate to `open-network-stream'.

Fellow ERC user 'neverwas' was kind enough to take my patch for a test
drive and provide feedback.  I have done some work on addressing their
feedback, but for now I'm attaching my very first draft here which I'd
sent to neverwas, and will attach the next version of my patch (along
with a proper commit message, NEWS entry, etc) shortly after neverwas's
reply with their feedback.

For anyone wanting to take this for a spin, the changes here should be
fully backward-compatible, and should not break anything in existing ERC
configurations.  To specify a client certificate, call `erc-tls' with
the new keywords :client-key and :client-crt with the file name of the
certificate key and certificate file itself respectively:

    (erc-tls :server "chat.freenode.net" :port 6697
             :client-key "~/my-freenode-cert.key"
             :client-crt "~/my-freenode-cert.crt")

In GNU Emacs 28.0.50 (build 43, x86_64-pc-linux-gnu, X toolkit, cairo version 1.15.10, Xaw3d scroll bars)
 of 2021-04-14 built on trisquel
Repository revision: 4ddad8f1db1f888f3365ac1330989dfbee605dd5
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.11906000
System Description: Trisquel GNU/Linux Etiona (9.0)

Configured using:
 'configure --without-gconf --without-gsettings --without-gpm
 --with-x-toolkit=lucid --with-xaw3d --with-mailutils'

From cec5c02a8a82a904a68d705ad54c3ccdcd0774cc Mon Sep 17 00:00:00 2001
From: Amin Bandali <[email-masked]>
Date: Wed, 14 Apr 2021 23:32:54 -0400
Subject: [PATCH] Add support for TLS client certificates to 'erc-tls'

---
 lisp/erc/erc-backend.el | 32 +++++++++++++++-------
 lisp/erc/erc.el         | 59 ++++++++++++++++++++++++++++++-----------
 2 files changed, 66 insertions(+), 25 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index b1f97aea06..056cc5c68f 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -138,6 +138,16 @@ erc-session-connector
 (defvar-local erc-session-port nil
   "The port used to connect to.")
 
+(defvar-local erc-session-cert-key nil
+  "File name of the private key of the client certificate used to connect over TLS.
+The corresponding client certificate's file name must be set in
+`erc-session-cert-crt'.")
+
+(defvar-local erc-session-cert-crt nil
+  "File name of the client certificate used to connect over TLS.
+The file name of the corresponding client certificate's private
+key must be set in `erc-session-cert-key'.")
+
 (defvar-local erc-server-announced-name nil
   "The name the server announced to use.")
 
@@ -505,18 +515,22 @@ erc-server-process-alive
          (memq (process-status erc-server-process) '(run open)))))
 
 ;;;; Connecting to a server
-(defun erc-open-network-stream (name buffer host service)
-  "As `open-network-stream', but does non-blocking IO"
-  (make-network-process :name name :buffer  buffer
-                        :host host :service service :nowait t))
+(defun erc-open-network-stream (name buffer host service &rest parameters)
+  "Like `open-network-stream', but does non-blocking IO."
+  (let ((p (plist-put parameters :nowait t)))
+    (open-network-stream name buffer host service p)))
 
-(defun erc-server-connect (server port buffer)
+(defun erc-server-connect (server port buffer &optional client-key client-crt)
   "Perform the connection and login using the specified SERVER and PORT.
-We will store server variables in the buffer given by BUFFER."
-  (let ((msg (erc-format-message 'connect ?S server ?p port)) process)
+We will store server variables in the buffer given by BUFFER.
+CLIENT-KEY and CLIENT-CRT may optionally be used to specify a TLS
+client certificate."
+  (let ((msg (erc-format-message 'connect ?S server ?p port)) process
+        (args `(,(format "erc-%s-%s" server port) nil ,server ,port)))
+    (when (and client-key client-crt)
+      (setq args `(,@args :client-certificate (,client-key ,client-crt))))
     (message "%s" msg)
-    (setq process (funcall erc-server-connect-function
-                           (format "erc-%s-%s" server port) nil server port))
+    (setq process (apply erc-server-connect-function args))
     (unless (processp process)
       (error "Connection attempt failed"))
     ;; Misc server variables
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index e20aa8057d..45594d7b87 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1967,7 +1967,8 @@ erc-setup-buffer
        (switch-to-buffer buffer)))))
 
 (defun erc-open (&optional server port nick full-name
-                           connect passwd tgt-list channel process)
+                           connect passwd tgt-list channel process
+                           client-key client-crt)
   "Connect to SERVER on PORT as NICK with FULL-NAME.
 
 If CONNECT is non-nil, connect to the server.  Otherwise assume
@@ -1977,6 +1978,10 @@ erc-open
 Use PASSWD as user password on the server.  If TGT-LIST is
 non-nil, use it to initialize `erc-default-recipients'.
 
+CLIENT-KEY and CLIENT-CRT can be file names for the private key
+and certificate parts of a client certificate to use when
+connecting over TLS.
+
 Returns the buffer for the given server or channel."
   (let ((server-announced-name (when (and (boundp 'erc-session-server)
                                           (string= server erc-session-server))
@@ -2073,13 +2078,18 @@ erc-open
       (erc-display-prompt)
       (goto-char (point-max)))
 
-    (erc-determine-parameters server port nick full-name)
+    (erc-determine-parameters server port nick full-name
+                              client-key client-crt)
 
     ;; Saving log file on exit
     (run-hook-with-args 'erc-connect-pre-hook buffer)
 
     (when connect
-      (erc-server-connect erc-session-server erc-session-port buffer))
+      (erc-server-connect erc-session-server
+                          erc-session-port
+                          buffer
+                          erc-session-cert-key
+                          erc-session-cert-crt))
     (erc-update-mode-line)
 
     ;; Now display the buffer in a window as per user wishes.
@@ -2192,7 +2202,9 @@ erc
                     (port   (erc-compute-port))
                     (nick   (erc-compute-nick))
                     password
-                    (full-name (erc-compute-full-name)))
+                    (full-name (erc-compute-full-name))
+                    client-key
+                    client-crt)
   "ERC is a powerful, modular, and extensible IRC client.
 This function is the main entry point for ERC.
 
@@ -2203,17 +2215,25 @@ erc
    (port   (erc-compute-port))
    (nick   (erc-compute-nick))
    password
-   (full-name (erc-compute-full-name)))
+   (full-name (erc-compute-full-name))
+   client-key
+   client-crt
 
 That is, if called with
 
    (erc :server \"chat.freenode.net\" :full-name \"Harry S Truman\")
 
-then the server and full-name will be set to those values, whereas
-`erc-compute-port', `erc-compute-nick' and `erc-compute-full-name' will
-be invoked for the values of the other parameters."
+then the server and full-name will be set to those values,
+whereas `erc-compute-port', `erc-compute-nick' and
+`erc-compute-full-name' will be invoked for the values of the
+other parameters.  CLIENT-KEY and CLIENT-CRT, if non-nil, should
+be file names for the private key and certificate parts of a
+client certificate respectively to be used when connecting over
+TLS."
   (interactive (erc-select-read-args))
-  (erc-open server port nick full-name t password))
+  (erc-open server port nick full-name t password
+            nil nil nil
+            client-key client-crt))
 
 ;;;###autoload
 (defalias 'erc-select #'erc)
@@ -2228,13 +2248,18 @@ erc-tls
   (let ((erc-server-connect-function 'erc-open-tls-stream))
     (apply #'erc r)))
 
-(defun erc-open-tls-stream (name buffer host port)
+(defun erc-open-tls-stream (name buffer host port &rest parameters)
   "Open an TLS stream to an IRC server.
-The process will be given the name NAME, its target buffer will be
-BUFFER.  HOST and PORT specify the connection target."
-  (open-network-stream name buffer host port
-		       :nowait t
-                       :type 'tls))
+The process will be given the name NAME, its target buffer will
+be BUFFER.  HOST and PORT specify the connection target.
+PARAMETERS should be a sequence of keywords and values, per
+`open-network-stream'."
+  (let ((p (plist-put parameters :type 'tls))
+        args)
+    (unless (plist-get p :nowait)
+      (setq p (plist-put p :nowait t)))
+    (setq args `(,name ,buffer ,host ,port ,@p))
+    (apply #'open-network-stream args)))
 
 ;;; Displaying error messages
 
@@ -6030,7 +6055,7 @@ erc-login
 
 ;; connection properties' heuristics
 
-(defun erc-determine-parameters (&optional server port nick name)
+(defun erc-determine-parameters (&optional server port nick name key crt)
   "Determine the connection and authentication parameters.
 Sets the buffer local variables:
 
@@ -6042,6 +6067,8 @@ erc-determine-parameters
   (setq erc-session-connector erc-server-connect-function
         erc-session-server (erc-compute-server server)
         erc-session-port (or port erc-default-port)
+        erc-session-cert-key key
+        erc-session-cert-crt crt
         erc-session-user-full-name (erc-compute-full-name name))
   (erc-set-current-nick (erc-compute-nick nick)))
 
-- 
2.17.1