From 1bf236e6f3ffd2097bc4c9cc54ad6a049aa8c1c4 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Wed, 21 Sep 2022 00:25:49 -0700 Subject: [PATCH 0/4] *** NOT A PATCH *** *** BLURB HERE *** F. Jason Park (4): Add GS2 authorization to sasl-scram-rfc Support local ERC modules in erc-mode buffers Make erc-login generic Add non-IRCv3 SASL module to ERC doc/misc/erc.texi | 143 +++++- lisp/erc/erc-backend.el | 8 +- lisp/erc/erc-compat.el | 104 +++++ lisp/erc/erc-sasl.el | 418 ++++++++++++++++++ lisp/erc/erc.el | 108 +++-- lisp/net/sasl-scram-rfc.el | 21 +- test/lisp/erc/erc-sasl-tests.el | 300 +++++++++++++ test/lisp/erc/erc-scenarios-sasl.el | 161 +++++++ test/lisp/erc/erc-tests.el | 47 ++ test/lisp/erc/resources/sasl/external.eld | 33 ++ test/lisp/erc/resources/sasl/plain-failed.eld | 16 + test/lisp/erc/resources/sasl/plain.eld | 35 ++ test/lisp/erc/resources/sasl/scram-sha-1.eld | 47 ++ .../lisp/erc/resources/sasl/scram-sha-256.eld | 47 ++ 14 files changed, 1442 insertions(+), 46 deletions(-) create mode 100644 lisp/erc/erc-sasl.el create mode 100644 test/lisp/erc/erc-sasl-tests.el create mode 100644 test/lisp/erc/erc-scenarios-sasl.el create mode 100644 test/lisp/erc/resources/sasl/external.eld create mode 100644 test/lisp/erc/resources/sasl/plain-failed.eld create mode 100644 test/lisp/erc/resources/sasl/plain.eld create mode 100644 test/lisp/erc/resources/sasl/scram-sha-1.eld create mode 100644 test/lisp/erc/resources/sasl/scram-sha-256.eld Interdiff: diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 3db83197f9..3b7af0fb1b 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -78,6 +78,7 @@ Top Advanced Usage * Connecting:: Ways of connecting to an IRC server. +* SASL:: Authenticating via SASL. * Sample Configuration:: An example configuration file. * Options:: Options that are available for ERC. @@ -478,6 +479,10 @@ Modules @item ring Enable an input history +@cindex modules, sasl +@item sasl +Enable SASL authentication + @cindex modules, scrolltobottom @item scrolltobottom Scroll to the bottom of the buffer @@ -525,6 +530,7 @@ Advanced Usage @menu * Connecting:: Ways of connecting to an IRC server. +* SASL:: Authenticating via SASL * Sample Configuration:: An example configuration file. * Options:: Options that are available for ERC. @end menu @@ -842,6 +848,7 @@ Connecting @noindent For details, @pxref{Top,,auth-source, auth, Emacs auth-source Library}. +@anchor{ERC auth-source functions} @defopt erc-auth-source-server-function @end defopt @defopt erc-auth-source-services-function @@ -854,7 +861,8 @@ Connecting @code{:user} is the ``desired'' nickname rather than the current one. Generalized names, like @code{:user} and @code{:host}, are always used over back-end specific ones, like @code{:login} or @code{:machine}. -ERC expects a string to use as the secret or nil, if the search fails. +ERC expects a string to use as the secret or @code{nil}, if the search +fails. @findex erc-auth-source-search The default value for all three options is the function @@ -915,6 +923,139 @@ Connecting make the most sense, but any reasonably printable object is acceptable. +@node SASL +@section Authenticating via SASL +@cindex SASL + +@strong{Warning:} ERC's SASL offering is currently limited by a lack +of support for proper IRCv3 capability negotiation. In most cases, +this shouldn't affect your ability to authenticate. If you run into +trouble, please contact us (@pxref{Getting Help and Reporting Bugs}). + +Regardless of the mechanism or the network, you'll likely have to be +registered before first use. Please refer to the network's own +instructions for details. If you're new to IRC and using a bouncer, +know that you almost certainly won't be needing SASL for the +@samp{client -> bouncer} connection. + +Note that @code{sasl} is a ``local'' ERC module. This means invoking +@code{erc-sasl-mode} manually or calling @code{erc-update-modules} +won't do any good. Instead, simply add @code{sasl} to +@code{erc-modules} (or @code{let}-bind it while calling +@code{erc-tls}), and SASL will be enabled for the current connection. +But before that, please explore all custom options pertaining to your +chosen mechanism. + +@defopt erc-sasl-mechanism +The name of an SASL subprotocol type as a @emph{lowercase} symbol. + +@var{plain} and @var{scram} (``password-based''): + +@indentedblock +Here, ``password'' refers to your account password, which is usually +your @samp{NickServ} password. This often differs from any connection +(server) password given to @code{erc-tls} via its @code{:password} +parameter. To make this work, customize both @code{erc-sasl-user} and +@code{erc-sasl-password} or bind them when invoking @code{erc-tls}. + +When @code{erc-sasl-password} is a string, it's used unconditionally. +When it's a non-@code{nil} symbol, like @samp{Libera.Chat}, it's used +as the @code{:host} param in an auth-source query. When it's +@code{nil} and a session ID is on file, the ID is used instead for the +@code{:host} param (@pxref{Network Identifier}). The value of +@code{erc-sasl-user} is always specified for the @code{:user} +(@code{:login}) param. + +If a password can't be determined, a non-@code{nil} server +(connection) password will be tried. (This may change, however, so +please don't rely on it.) +@end indentedblock + +@var{external} (via Client TLS Certificate): + +@indentedblock +You'll want to specify the @code{:client-certificate} param when +opening a new connection, which is typically done by calling +@code{emacs-tls}. But before that, ensure you've registered your +fingerprint with the network. The fingerprint is usually a SHA1 or +SHA256 digest in either "normalized" or "openssl" forms. The first is +lowercase without delims (@samp{deadbeef}) and the second uppercase +with colon seps (@samp{DE:AD:BE:EF}). + +Additional considerations: +@enumerate +@item +There's no reason to send your password after registering. +@item +Most IRCds will allow you to authenticate with a client cert but +without the hassle of SASL (meaning you may not need this module). +@item +Technically, @var{EXTERNAL} merely indicates that an out-of-band mode +of authentication is in effect (being deferred to), so depending on +the specific application or service, there's an off chance client +certs aren't involved. +@end enumerate +@end indentedblock + +@var{ecdsa-nist256p-challenge}: + +@indentedblock +This mechanism is quite complicated and currently requires the +presence of the external @samp{openssl} command-line utility, so +please use something else if at all possible. Ignoring that, specify +your key file (e.g., @samp{~/pki/mykey.pem}) as the value of +@code{erc-sasl-password}, and then configure your network settings. +On servers running Atheme services, you can add your public key with +@samp{NickServ} like so: + +@example +ERC> /msg NickServ set property \ + pubkey AgGZmlYTUjJlea/BVz7yrjJ6gysiAPaQxzeUzTH4hd5j + +@end example +(You may be able to omit the @samp{property} subcommand.) +@end indentedblock + +@end defopt + +@defopt erc-sasl-user +Your network account name, typically the same one registered with +nickname services. Specify this when your @samp{NickServ} account +name differs from the nick you're connecting with. +@end defopt + +@defopt erc-sasl-password +Optional account password to send when authenticating. + +If you specify a string, it'll be considered authoritative and +accepted at face value. If you instead give a non-@code{nil} symbol, +it'll be passed as the value of the @code{:host} field in an +auth-source query, provided @code{erc-sasl-auth-source-function} is +set to a function. If you set this to @code{nil}, a non-@code{nil} +``session password'' will be tried, likely whatever you gave as the +@var{password} argument to @code{erc-tls}. As a last resort, you'll +be prompted for input. + +Note that when @code{erc-sasl-mechanism} is set to +@code{ecdsa-nist256p-challenge}, this option should hold the file name +of your key, which is typically in PEM format. +@end defopt + +@defopt erc-sasl-auth-source-function +This is nearly identical to the other ERC @samp{auth-source} function +options (@pxref{ERC auth-source functions}) except that the default +value here is @code{nil}, meaning you have to set it to something like +@code{erc-auth-source-search} for queries to be performed. +@end defopt + +@defopt erc-sasl-authzid +In the rarest of circumstances, a network may want you to specify a +specific role or assume an alternate identity. In most cases, this +happens because the server is buggy or misconfigured. If you suspect +such a thing, please contact your network operator. Otherwise, just +leave this set to @code{nil}. +@end defopt + @node Sample Configuration @section Sample Configuration diff --git a/lisp/erc/erc-sasl.el b/lisp/erc/erc-sasl.el index bd27934125..d237ab73a8 100644 --- a/lisp/erc/erc-sasl.el +++ b/lisp/erc/erc-sasl.el @@ -19,9 +19,9 @@ ;;; Commentary: -;; WARNING: this is a naive/hack (non-IRCv3) implementation of SASL. -;; Please see bug#49860, which adds full 3.2 capability negotiation. - +;; WARNING: this is a (non-IRCv3) implementation of SASL. Please see +;; bug#49860, which adds full 3.2 capability negotiation. +;; ;; Various ERC implementations of the PLAIN mechanism have surfaced ;; over the years, the first possibly being: ;; @@ -30,77 +30,14 @@ ;; This module would not exist without this and other pioneering ;; efforts. ;; -;; FIXME move the following to doc/misc/erc.texi -;; -;; Regardless of the mechanism or server, you'll likely have to be -;; registered before first use. Refer to the network's own -;; instructions for details. If you're new to IRC and using a -;; bouncer, know that you almost certainly won't be needing SASL for -;; the client -> bouncer connection. -;; -;; Note that `sasl' is a "local" ERC module (effectively introduced in -;; ERC 5.5). This means invoking `erc-sasl-mode' manually or calling -;; `erc-update-modules' won't do any good. Instead, simply add `sasl' -;; to `erc-modules' or `let'-bind it while calling `erc-tls', and SASL -;; will be enabled for the current connection. But before that, -;; please explore all custom options that pertain to your chosen -;; mechanism. -;; -;; Password-based mechanisms: -;; -;; Here, "password" refers to your account password, which is -;; usually your NickServ password. This often differs from any -;; connection (server) password given to `erc-tls' via its :password -;; arg. To make this work, customize both `erc-sasl-user' and -;; `erc-sasl-password' or bind them when invoking `erc-tls'. -;; -;; When `erc-sasl-password' is a string, it's used unconditionally. -;; When it's a non-nil symbol, like Libera.Chat, it's used as the -;; host param in an auth-source query. When it's nil and a session -;; ID is on file (see `erc-tls'), the ID is instead used for the -;; host param. The value of `erc-sasl-user' is always specified for -;; the user (login) param. See the info node "(erc) Connecting" for -;; specifics. -;; -;; If no password can be determined, a non-nil connection password -;; will be tried (but this may change, so please don't rely on it). -;; -;; EXTERNAL (with Client TLS Certificate): -;; -;; 1. Specify the `:client-certificate' param when opening a new -;; connection, which is typically done by calling `emacs-tls'. -;; See (info "(erc) Connecting"). -;; -;; 2. Ensure you've registered your fingerprint with the network and -;; (re)connect. The fingerprint is usually a SHA1 or SHA256 -;; digest in either "normalized" or "openssl" forms. The first -;; is lowercase without delims ("deadbeef") and the second -;; uppercase with colon seps ("DE:AD:BE:EF"). -;; -;; There's no reason to send your password after registering. Note -;; that most ircds will allow you to authenticate with a client cert -;; but without the hassle of SASL (meaning you may not need this -;; module). -;; -;; ECDSA-NIST256P-CHALLENGE: +;; TODO: ;; -;; Use something else if at all possible. This currently requires -;; the openssl command-line utility. On servers running Atheme -;; services, add your public key with NickServ like so: -;; -;; /msg NickServ set property -;; pubkey AgGZmlYTUjJlea/BVz7yrjJ6gysiAPaQxzeUzTH4hd5j -;; -;; (You may not need the "property" subcommand.) -;; -;; -;; TODO +;; - Find a way to obfuscate the password in memory (via something +;; - like `auth-source--obfuscate'); it's currently visible in +;; - backtraces. ;; ;; - Implement pseudo PASSWORD mechanism that chooses the strongest ;; available mechanism for you. -;; -;; - Maybe provide explicit authz. Currently, there's only an obscure -;; customizable function option for SCRAM and nothing for plain. ;;; Code: (require 'erc-backend) @@ -141,7 +78,11 @@ erc-sasl-password `erc-sasl-auth-source-function' is set to a function. When nil, a non-nil \"session password\" will be tried, likely one given as the `:password' argument to `erc-tls'. As a last -resort, the user will be prompted for input." +resort, the user will be prompted for input. + +Note that when `erc-sasl-mechanism' is set to +`ecdsa-nist256p-challenge', this option should hold the file name +of the key, which is typically in PEM format." :type '(choice (const nil) string symbol)) (defcustom erc-sasl-auth-source-function nil @@ -158,10 +99,6 @@ erc-sasl-auth-source-function (const nil) function)) -(defcustom erc-sasl-ecdsa-private-key nil - "Private signing key file for ECDSA-NIST256P-CHALLENGE." - :type '(choice (const nil) string)) - (defcustom erc-sasl-authzid nil "SASL authorization identity. Generally unneeded for normal use. Some test frameworks and @@ -246,7 +183,7 @@ erc-sasl--ecdsa-first (sasl-client-name client)) ;; FIXME do this with gnutls somehow -(defun erc-sasl--ecdsa-sign (_client step) +(defun erc-sasl--ecdsa-sign (client step) "Return signed challenge for CLIENT and STEP." (let ((challenge (sasl-step-data step))) (with-temp-buffer @@ -254,7 +191,7 @@ erc-sasl--ecdsa-sign (insert challenge) (call-process-region (point-min) (point-max) "openssl" 'delete t nil "pkeyutl" "-inkey" - (alist-get 'ecdsa-private-key erc-sasl--options) + (sasl-client-property client 'ecdsa-keyfile) "-sign") (buffer-string)))) @@ -342,18 +279,18 @@ erc-sasl--create-client "Create a ECDSA-NIST256P-CHALLENGE client." (unless (executable-find "openssl") (user-error "Could not find openssl command-line utility")) - (unless (and (alist-get 'ecdsa-private-key erc-sasl--options) - (file-exists-p (alist-get 'ecdsa-private-key - erc-sasl--options))) - (user-error "Could not find `erc-sasl-ecdsa-private-key'")) - (cl-call-next-method)) + (let ((keyfile (cdr (assq 'password erc-sasl--options)))) + (unless (and keyfile (file-exists-p keyfile)) + (user-error "`erc-sasl-password' does not point to ECDSA keyfile")) + (let ((client (cl-call-next-method))) + (sasl-client-set-property client 'ecdsa-keyfile keyfile) + client))) (defun erc-sasl--init () (setq erc-sasl--state (make-erc-sasl--state) erc-sasl--options `((user . ,erc-sasl-user) (password . ,erc-sasl-password) (mechanism . ,erc-sasl-mechanism) - (ecdsa-private-key . ,erc-sasl-ecdsa-private-key) (authzid . ,erc-sasl-authzid)))) (defun erc-sasl--mechanism-offered-p (offered) @@ -365,14 +302,6 @@ erc-sasl--mechanism-offered-p (| eot ","))) (downcase offered))) -(defun erc-sasl--add-hook () - (add-hook 'erc-server-AUTHENTICATE-functions - #'erc-sasl--authenticate-handler 0 t)) - -(defun erc-sasl--remove-hook () - (remove-hook 'erc-server-AUTHENTICATE-functions - #'erc-sasl--authenticate-handler t)) - (defun erc-sasl--authenticate-handler (_proc parsed) "Handle PARSED `erc-response' from server. Maybe transition to next state." @@ -417,7 +346,8 @@ sasl supported mechanisms. See bug#49860 for a full, CAP 3.2-aware implementation, currently a WIP as of ERC 5.5." ((unless erc--target - (erc-sasl--add-hook) + (add-hook 'erc-server-AUTHENTICATE-functions + #'erc-sasl--authenticate-handler 0 t) (erc-sasl--init) (let* ((mech (alist-get 'mechanism erc-sasl--options)) (client (erc-sasl--create-client mech))) @@ -425,7 +355,8 @@ sasl (erc-display-error-notice nil (format "Unknown mechanism: %s" mech)) (erc-error "Unknown mechanism: %s" mech)) (setf (erc-sasl--state-client erc-sasl--state) client)))) - ((erc-sasl--remove-hook) + ((remove-hook 'erc-server-AUTHENTICATE-functions + #'erc-sasl--authenticate-handler t) (kill-local-variable 'erc-sasl--options)) 'local) @@ -433,12 +364,17 @@ sasl (define-erc-response-handler (AUTHENTICATE) "Maybe authenticate to server." nil) -;; FIXME do something decisive here +(defun erc-sasl--destroy (proc) + (run-hook-with-args 'erc-quit-hook proc) + (delete-process proc) + (erc-error "Disconnected from %s; please review SASL settings" proc)) + (define-erc-response-handler (902) "Handle a ERR_NICKLOCKED response." nil - (let ((nick (car (erc-response.command-args parsed))) - (msg (erc-response.contents parsed))) - (erc-display-message parsed '(notice error) 'active 's902 ?n nick ?s msg))) + (erc-display-message parsed '(notice error) 'active 's902 + ?n (car (erc-response.command-args parsed)) + ?s (erc-response.contents parsed)) + (erc-sasl--destroy proc)) (define-erc-response-handler (903) "Handle a RPL_SASLSUCCESS response." nil @@ -447,19 +383,24 @@ sasl (erc-server-send "CAP END"))) (erc-handle-unknown-server-response proc parsed)) -(define-erc-response-handler (904 905 906 907 908) +(define-erc-response-handler (907) + "Handle a RPL_SASLALREADY response." nil + (erc-display-message parsed '(notice error) 'active 's907 + ?s (erc-response.contents parsed))) + +(define-erc-response-handler (904 905 906) "Handle various SASL-related error responses." nil - (let* ((msg (intern (format "s%s" (erc-response.command parsed)))) - (args `(parsed (notice error) active ,msg - ,@(when (string= "908" (erc-response.command parsed)) - (list '?m - (alist-get 'mechanism erc-sasl--options))) - ?s ,(erc-response.contents parsed)))) - (apply #'erc-display-message args)) - (when (member (erc-response.command parsed) '("904" "905" "906")) - (run-hook-with-args 'erc-quit-hook proc) - (delete-process proc) - (erc-error "Disconnected from %s; please review SASL settings" proc))) + (erc-display-message parsed '(notice error) 'active + (intern (format "s%s" (erc-response.command parsed))) + ?s (erc-response.contents parsed)) + (erc-sasl--destroy proc)) + +(define-erc-response-handler (908) + "Handle a RPL_SASLALREADY response." nil + (erc-display-message parsed '(notice error) 'active 's908 + '?m (alist-get 'mechanism erc-sasl--options) + '?s (erc-response.contents parsed)) + (erc-sasl--destroy proc)) (cl-defmethod erc--register-connection (&context (erc-sasl-mode (eql t))) "Send speculative/pipelined CAP and AUTHENTICATE and hope for the best." diff --git a/test/lisp/erc/erc-sasl-tests.el b/test/lisp/erc/erc-sasl-tests.el index beac287a6e..c54acc4d28 100644 --- a/test/lisp/erc/erc-sasl-tests.el +++ b/test/lisp/erc/erc-sasl-tests.el @@ -276,24 +276,25 @@ erc-sasl-tests-ecdsa-key-file (ert-deftest erc-sasl-create-client-ecdsa () (unless (executable-find "openssl") (ert-skip "System lacks openssl")) - (let* ((erc-server-current-nick "jilles") - (keyfile (make-temp-file "ecdsa_key.pem" nil nil - erc-sasl-tests-ecdsa-key-file)) - (erc-sasl--options `((ecdsa-private-key . ,keyfile))) - (client (erc-sasl--create-client 'ecdsa-nist256p-challenge)) - (step (sasl-next-step client nil))) - (ert-info ("Client's initial request") - (should (equal (format "%S" [erc-sasl--ecdsa-first "jilles"]) - (format "%S" step))) - (should (string= (sasl-step-data step) "jilles"))) - (ert-info ("Server's initial response") - (let ((resp (concat "\0\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20" - "\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37"))) - (sasl-step-set-data step resp) - (setq step (sasl-next-step client step)) - ;; FIXME this is dumb - (should (<= 68 (length (sasl-step-data step)) 72)))) - (should-not (sasl-next-step client step)) - (delete-file keyfile))) + (ert-with-temp-file keyfile + :prefix "ecdsa_key" + :suffix ".pem" + :text erc-sasl-tests-ecdsa-key-file + (let* ((erc-server-current-nick "jilles") + (erc-sasl--options `((password . ,keyfile))) + (client (erc-sasl--create-client 'ecdsa-nist256p-challenge)) + (step (sasl-next-step client nil))) + (ert-info ("Client's initial request") + (should (equal (format "%S" [erc-sasl--ecdsa-first "jilles"]) + (format "%S" step))) + (should (string= (sasl-step-data step) "jilles"))) + (ert-info ("Server's initial response") + (let ((resp (concat "\0\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20" + "\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37"))) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + ;; FIXME this is dumb + (should (<= 68 (length (sasl-step-data step)) 72)))) + (should-not (sasl-next-step client step))))) ;;; erc-sasl-tests.el ends here -- 2.37.2