Add non-IRCv3 SASL module to ERC

* doc/misc/erc.texi: Add SASL section in Advanced Usage chapter to
document the new SASL module.

* etc/ERC-NEWS: Mention addition of erc-sasl module for SASL support.

* lisp/erc/erc-compat.el
(erc-compat--29-sasl-scram-construct-gs2-header,
erc-compat--29-sasl-scram-client-first-message,
erc-compat--29-sasl-scram--client-final-message): Fix encoding bug and
add minimal authorization support with copies of SASL functions
introduced in Emacs 29.

* lisp/erc/erc.el (erc-modules): Add `sasl'.
* lisp/erc/erc-sasl.el: New file (bug#29108).
* test/lisp/erc/erc-sasl-tests.el: New file.
* test/lisp/erc/erc-scenarios-sasl.el: New file.
* test/lisp/erc/resources/sasl/plain-failed.eld: New file.
* test/lisp/erc/resources/sasl/plain.eld: New file.
* test/lisp/erc/resources/sasl/scram-sha-1.eld: New file.
* test/lisp/erc/resources/sasl/scram-sha-256.eld: New file.
* test/lisp/erc/resources/sasl/external.eld: New file.
This commit is contained in:
F. Jason Park 2021-07-12 03:44:28 -07:00 committed by Amin Bandali
parent ae254a65cd
commit ed8862c404
No known key found for this signature in database
GPG key ID: 8B44A0CDC7B956F2
13 changed files with 1333 additions and 5 deletions

View file

@ -78,6 +78,7 @@ Getting Started
Advanced Usage
* Connecting:: Ways of connecting to an IRC server.
* SASL:: Authenticating via SASL.
* Sample Configuration:: An example configuration file.
* Integrations:: Integrations available for ERC.
* Options:: Options that are available for ERC.
@ -482,6 +483,10 @@ Replace text in messages
@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
@ -561,6 +566,7 @@ toggle never mutates @code{erc-modules}.
@menu
* Connecting:: Ways of connecting to an IRC server.
* SASL:: Authenticating via SASL
* Sample Configuration:: An example configuration file.
* Integrations:: Integrations available for ERC.
* Options:: Options that are available for ERC.
@ -633,6 +639,7 @@ while helpers, like @code{erc-compute-nick}, will determine other
parameters, and some, like @code{client-certificate}, will just be
@code{nil}.
@anchor{ERC client-certificate}
To use a certificate with @code{erc-tls}, specify the optional
@var{client-certificate} keyword argument, whose value should be as
described in the documentation of @code{open-network-stream}: if
@ -767,7 +774,10 @@ ERC should automatically attempt to connect with another nickname.
You can manually set another nickname with the /NICK command.
@end defopt
@anchor{ERC username}
@subheading User
@cindex user
@defun erc-compute-user &optional user
Determine a suitable value to send as the first argument of the
opening @samp{USER} IRC command by consulting the following sources:
@ -879,6 +889,7 @@ netrc's case). The actual key goes in the @samp{password} (or
@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
@ -891,7 +902,8 @@ current context, if any. For example, with NickServ queries,
@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
@ -953,6 +965,143 @@ When providing an ID as an entry-point argument, strings and symbols
make the most sense, but any reasonably printable object is
acceptable.
@node SASL
@section Authenticating via SASL
@cindex SASL
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 probably won't be needing SASL for the client-to-bouncer
connection. To get started, just add @code{sasl} to
@code{erc-modules} like any other module. 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. To make this work, customize
@code{erc-sasl-user} and @code{erc-sasl-password} or specify the
@code{:user} and @code{:password} keyword arguments when invoking
@code{erc-tls}. Note that @code{:user} cannot be given interactively.
@end indentedblock
@var{external} (via Client TLS Certificate):
@indentedblock
This works in conjunction with the @code{:client-certificate} keyword
offered by @code{erc-tls}. Just ensure you've registered your
fingerprint with the network beforehand. 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}). These days, there's
usually a @samp{CERT ADD} command offered by NickServ that can
register you automatically if you issue it while connected with a
client cert. (@pxref{ERC client-certificate}).
Additional considerations:
@enumerate
@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 a remote chance your
server has something else in mind.
@end enumerate
@end indentedblock
@var{ecdsa-nist256p-challenge}:
@indentedblock
This mechanism is quite complicated and currently requires the
external @samp{openssl} executable, 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
This should be your network account username, typically the same one
registered with nickname services. Specify this when your NickServ
login differs from the @code{:user} you're connecting with.
(@pxref{ERC username})
@end defopt
@defopt erc-sasl-password
As noted elsewhere, the @code{:password} parameter for @code{erc-tls}
was orignally intended for traditional ``server passwords,'' but these
aren't really used any more. As such, this option defaults to
borrowing that parameter for its own uses, thus allowing you to call
@code{erc-tls} with @code{:password} set to your NickServ password.
You can also set this to a nonemtpy string, and ERC will send that
when needed, no questions asked. If you instead give a non-@code{nil}
symbol (other than @code{:password}), like @samp{Libera.Chat}, ERC
will use it for the @code{:host} field in an auth-source query.
Actually, the same goes for when this option is @code{nil} but an
explicit session ID is already on file (@pxref{Network Identifier}).
For all such queries, ERC specifies the resolved value of
@code{erc-sasl-user} for the @code{:user} (@code{:login}) param. Keep
in mind that none of this matters unless
@code{erc-sasl-auth-source-function} holds a function, and it's
@code{nil} by default. As a last resort, ERC will prompt you for
input.
Lastly, if your mechanism is @code{ecdsa-nist256p-challenge}, this
option should instead hold the file name of your key.
@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
@subheading Troubleshooting
@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're struggling, remember that your SASL password is almost
always your NickServ password. When in doubt, try restoring all SASL
options to their defaults and calling @code{erc-tls} with @code{:user}
set to your NickServ account name and @code{:password} to your
NickServ password. If you're still having trouble, please contact us
(@pxref{Getting Help and Reporting Bugs}).
As you try out different settings, keep in mind that it's best to
create a fresh session for every change, for example, by calling
@code{erc-tls} from scratch. More experienced users may be able to
get away with cycling @code{erc-sasl-mode} and issuing a
@samp{/reconnect}, but that's generally not recommended. Whatever the
case, you'll probably want to temporarily disable
@code{erc-server-auto-reconnect} while experimenting.
@node Sample Configuration
@section Sample Configuration

View file

@ -48,10 +48,9 @@ hell. For some, auth-source may provide a workaround in the form of
nonstandard server passwords. See the "Connection" node in the manual
under the subheading "Password".
If you require SASL immediately, please participate in ERC development
by volunteering to try (and give feedback on) edge features, one of
which is SASL. All known external offerings, past and present, are
valiant efforts whose use is nevertheless discouraged.
** Rudimentary SASL support has arrived.
A new module, 'erc-sasl', now ships with ERC 5.5. See the SASL
section in the manual for details.
** Username argument for entry-point commands.
Commands 'erc' and 'erc-tls' now accept a ':user' keyword argument,

View file

@ -2334,6 +2334,15 @@ See `erc-display-server-message'." nil
(erc-display-message parsed 'notice 'active 's671
?n nick ?a securemsg)))
(define-erc-response-handler (900)
"Handle a \"RPL_LOGGEDIN\" server command.
Some servers don't consider this SASL-specific but rather just an
indication of a server-side state change from logged-out to
logged-in." nil
;; Whenever ERC starts caring about user accounts, it should record
;; the session as being logged here.
(erc-display-message parsed 'notice proc (erc-response.contents parsed)))
(define-erc-response-handler (431 445 446 451 462 463 464 481 483 484 485
491 501 502)
;; 431 - No nickname given

View file

@ -273,6 +273,89 @@ If START or END is negative, it counts from the end."
auth-source-backend-parser-functions))
;;;; SASL
(declare-function sasl-step-data "sasl" (step))
(declare-function sasl-error "sasl" (datum))
(declare-function sasl-client-property "sasl" (client property))
(declare-function sasl-client-set-property "sasl" (client property value))
(declare-function sasl-mechanism-name "sasl" (mechanism))
(declare-function sasl-client-name "sasl" (client))
(declare-function sasl-client-mechanism "sasl" (client))
(declare-function sasl-read-passphrase "sasl" (prompt))
(declare-function sasl-unique-id "sasl" nil)
(declare-function decode-hex-string "hex-util" (string))
(declare-function rfc2104-hash "rfc2104" (hash block-length hash-length
key text))
(declare-function sasl-scram--client-first-message-bare "sasl-scram-rfc"
(client))
(declare-function cl-mapcar "cl-lib" (cl-func cl-x &rest cl-rest))
(defun erc-compat--29-sasl-scram-construct-gs2-header (client)
(let ((authzid (sasl-client-property client 'authenticator-name)))
(concat "n," (and authzid "a=") authzid ",")))
(defun erc-compat--29-sasl-scram-client-first-message (client _step)
(let ((c-nonce (sasl-unique-id)))
(sasl-client-set-property client 'c-nonce c-nonce))
(concat (erc-compat--29-sasl-scram-construct-gs2-header client)
(sasl-scram--client-first-message-bare client)))
(defun erc-compat--29-sasl-scram--client-final-message
(hash-fun block-length hash-length client step)
(unless (string-match
"^r=\\([^,]+\\),s=\\([^,]+\\),i=\\([0-9]+\\)\\(?:$\\|,\\)"
(sasl-step-data step))
(sasl-error "Unexpected server response"))
(let* ((hmac-fun
(lambda (text key)
(decode-hex-string
(rfc2104-hash hash-fun block-length hash-length key text))))
(step-data (sasl-step-data step))
(nonce (match-string 1 step-data))
(salt-base64 (match-string 2 step-data))
(iteration-count (string-to-number (match-string 3 step-data)))
(c-nonce (sasl-client-property client 'c-nonce))
(cbind-input
(if (string-prefix-p c-nonce nonce)
(erc-compat--29-sasl-scram-construct-gs2-header client) ; *1
(sasl-error "Invalid nonce from server")))
(client-final-message-without-proof
(concat "c=" (base64-encode-string cbind-input t) "," ; *2
"r=" nonce))
(password
(sasl-read-passphrase
(format "%s passphrase for %s: "
(sasl-mechanism-name (sasl-client-mechanism client))
(sasl-client-name client))))
(salt (base64-decode-string salt-base64))
(string-xor (lambda (a b)
(apply #'unibyte-string (cl-mapcar #'logxor a b))))
(salted-password (let ((digest (concat salt (string 0 0 0 1)))
(xored nil))
(dotimes (_i iteration-count xored)
(setq digest (funcall hmac-fun digest password))
(setq xored (if (null xored)
digest
(funcall string-xor xored
digest))))))
(client-key (funcall hmac-fun "Client Key" salted-password))
(stored-key (decode-hex-string (funcall hash-fun client-key)))
(auth-message (concat "n=" (sasl-client-name client)
",r=" c-nonce "," step-data
"," client-final-message-without-proof))
(client-signature (funcall hmac-fun
(encode-coding-string auth-message 'utf-8)
stored-key))
(client-proof (funcall string-xor client-key client-signature))
(client-final-message
(concat client-final-message-without-proof ","
"p=" (base64-encode-string client-proof t)))) ; *3
(sasl-client-set-property client 'auth-message auth-message)
(sasl-client-set-property client 'salted-password salted-password)
client-final-message))
;;;; Misc 29.1
(defmacro erc-compat--with-memoization (table &rest forms)

417
lisp/erc/erc-sasl.el Normal file
View file

@ -0,0 +1,417 @@
;;; erc-sasl.el --- SASL for ERC -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;;
;; This file is part of GNU Emacs.
;;
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published
;; by the Free Software Foundation, either version 3 of the License,
;; or (at your option) any later version.
;;
;; GNU Emacs is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This "non-IRCv3" implementation resembles others that have surfaced
;; over the years, the first possibly being from Joseph Gay:
;;
;; https://lists.gnu.org/archive/html/erc-discuss/2012-02/msg00001.html
;;
;; See options and Info manual for usage.
;;
;; TODO:
;;
;; - Find a way to obfuscate the password in memory (via something
;; like `auth-source--obfuscate'); it's currently visible in
;; backtraces.
;;
;; - Implement a proxy mechanism that chooses the strongest available
;; mechanism for you. Requires CAP 3.2 (see bug#49860).
;;
;; - Integrate with whatever solution ERC eventually settles on to
;; handle user options for different network contexts. At the
;; moment, this does its own thing for stashing and restoring
;; session options, but ERC should make abstractions available for
;; all local modules to use, possibly based on connection-local
;; variables.
;;; Code:
(require 'erc)
(require 'rx)
(require 'sasl)
(require 'sasl-scram-rfc)
(require 'sasl-scram-sha256 nil t) ; not present in Emacs 27
(defgroup erc-sasl nil
"SASL for ERC."
:group 'erc
:package-version '(ERC . "5.4.1")) ; FIXME increment on next release
(defcustom erc-sasl-mechanism 'plain
"SASL mechanism to connect with.
Note that any value other than nil or `external' likely requires
`erc-sasl-user' and `erc-sasl-password'."
:type '(choice (const plain)
(const external)
(const scram-sha-1)
(const scram-sha-256)
(const scram-sha-512)
(const ecdsa-nist256p-challenge)))
(defcustom erc-sasl-user :user
"Account username to send when authenticating.
This is also referred to as the authentication identity or
\"authcid\". A value of `:user' or `:nick' indicates that the
corresponding connection parameter on file should be used. These
are most often derived from arguments provided to the `erc' and
`erc-tls' entry points. In the case of `:nick', a downcased
version is used."
:type '(choice string (const :user) (const :nick)))
(defcustom erc-sasl-password :password
"Optional account password to send when authenticating.
When the value is a string, ERC will use it unconditionally for
most mechanisms. Likewise with `:password', except ERC will
instead use the \"session password\" on file, which often
originates from the entry-point commands `erc' or `erc-tls'.
Otherwise, when `erc-sasl-auth-source-function' is a function,
ERC will attempt an auth-source query, possibly using a non-nil
symbol for the suggested `:host' parameter if set as this
option's value or passed as an `:id' to `erc-tls'. Failing that,
ERC will prompt for input.
Note that, with `:password', ERC will forgo sending a traditional
server password via the IRC \"PASS\" command. Also, when
`erc-sasl-mechanism' is set to `ecdsa-nist256p-challenge', this
option should hold the file name of the key."
:type '(choice (const nil) (const :password) string symbol))
(defcustom erc-sasl-auth-source-function nil
"Function to query auth-source for an SASL password.
Called with keyword params known to `auth-source-search', which
includes `erc-sasl-user' for the `:user' field and
`erc-sasl-password' for the `:host' field, when the latter option
is a non-nil, non-keyword symbol. In return, ERC expects a
string to send as the SASL password, or nil, to move on to the
next approach, as described in the doc string for the option
`erc-sasl-password'. See info node `(erc) Connecting' for
details on ERC's auth-source integration."
:type '(choice (function-item erc-auth-source-search)
(const nil)
function))
(defcustom erc-sasl-authzid nil
"SASL authorization identity, likely unneeded for everyday use."
:type '(choice (const nil) string))
;; Analogous to what erc-backend does to persist opening params.
(defvar-local erc-sasl--options nil)
;; Session-local (server buffer) SASL subproto state
(defvar-local erc-sasl--state nil)
(cl-defstruct erc-sasl--state
"Holder for client object and subproto state."
(client nil :type vector)
(step nil :type vector)
(pending nil :type string))
(defun erc-sasl--get-user ()
(pcase (alist-get 'user erc-sasl--options)
(:user erc-session-username)
(:nick (erc-downcase (erc-current-nick)))
(v v)))
(defun erc-sasl--read-password (prompt)
"Return configured option or server password.
PROMPT is passed to `read-passwd' if necessary."
(if-let
((found (pcase (alist-get 'password erc-sasl--options)
(:password erc-session-password)
((and (pred stringp) v) (unless (string-empty-p v) v))
((and (guard erc-sasl-auth-source-function)
v (let host
(or v (erc-networks--id-given erc-networks--id))))
(apply erc-sasl-auth-source-function
:user (erc-sasl--get-user)
(and host (list :host (symbol-name host))))))))
(copy-sequence found)
(read-passwd prompt)))
(defun erc-sasl--plain-response (client steps)
(let ((sasl-read-passphrase #'erc-sasl--read-password))
(sasl-plain-response client steps)))
(declare-function erc-compat--29-sasl-scram--client-final-message "erc-compat"
(hash-fun block-length hash-length client step))
(defun erc-sasl--scram-sha-hack-client-final-message (&rest args)
;; In the future (29+), we'll hopefully be able to call
;; `sasl-scram--client-final-message' directly
(require 'erc-compat)
(let ((sasl-read-passphrase #'erc-sasl--read-password))
(apply #'erc-compat--29-sasl-scram--client-final-message args)))
(defun erc-sasl--scram-sha-1-client-final-message (client step)
(erc-sasl--scram-sha-hack-client-final-message 'sha1 64 20 client step))
(defun erc-sasl--scram-sha-256-client-final-message (client step)
(erc-sasl--scram-sha-hack-client-final-message 'sasl-scram-sha256 64 32
client step))
(defun erc-sasl--scram-sha512 (object &optional start end binary)
(secure-hash 'sha512 object start end binary))
(defun erc-sasl--scram-sha-512-client-final-message (client step)
(erc-sasl--scram-sha-hack-client-final-message #'erc-sasl--scram-sha512
128 64 client step))
(defun erc-sasl--scram-sha-512-authenticate-server (client step)
(sasl-scram--authenticate-server #'erc-sasl--scram-sha512
128 64 client step))
(defun erc-sasl--ecdsa-first (client _step)
"Return CLIENT name."
(sasl-client-name client))
;; FIXME do this with gnutls somehow
(defun erc-sasl--ecdsa-sign (client step)
"Return signed challenge for CLIENT and current STEP."
(let ((challenge (sasl-step-data step)))
(with-temp-buffer
(set-buffer-multibyte nil)
(insert challenge)
(call-process-region (point-min) (point-max)
"openssl" 'delete t nil "pkeyutl" "-inkey"
(sasl-client-property client 'ecdsa-keyfile)
"-sign")
(buffer-string))))
(pcase-dolist
(`(,name . ,steps)
'(("PLAIN"
erc-sasl--plain-response)
("EXTERNAL"
ignore)
("SCRAM-SHA-1"
erc-compat--29-sasl-scram-client-first-message
erc-sasl--scram-sha-1-client-final-message
sasl-scram-sha-1-authenticate-server)
("SCRAM-SHA-256"
erc-compat--29-sasl-scram-client-first-message
erc-sasl--scram-sha-256-client-final-message
sasl-scram-sha-256-authenticate-server)
("SCRAM-SHA-512"
erc-compat--29-sasl-scram-client-first-message
erc-sasl--scram-sha-512-client-final-message
erc-sasl--scram-sha-512-authenticate-server)
("ECDSA-NIST256P-CHALLENGE"
erc-sasl--ecdsa-first
erc-sasl--ecdsa-sign)))
(let ((feature (intern (concat "erc-sasl-" (downcase name)))))
(put feature 'sasl-mechanism (sasl-make-mechanism name steps))
(provide feature)))
(cl-defgeneric erc-sasl--create-client (mechanism)
"Create and return a new SASL client object for MECHANISM."
(let ((sasl-mechanism-alist (copy-sequence sasl-mechanism-alist))
(sasl-mechanisms sasl-mechanisms)
(name (upcase (symbol-name mechanism)))
(feature (intern-soft (concat "erc-sasl-" (symbol-name mechanism))))
client)
(when feature
(setf (alist-get name sasl-mechanism-alist nil nil #'equal) `(,feature))
(cl-pushnew name sasl-mechanisms :test #'equal)
(setq client (sasl-make-client (sasl-find-mechanism (list name))
(erc-sasl--get-user)
"N/A" "N/A"))
(sasl-client-set-property client 'authenticator-name
(alist-get 'authzid erc-sasl--options))
client)))
(cl-defmethod erc-sasl--create-client ((_ (eql plain)))
"Create and return a new PLAIN client object."
;; https://tools.ietf.org/html/rfc4616#section-2.
(let* ((sans (remq (assoc "PLAIN" sasl-mechanism-alist)
sasl-mechanism-alist))
(sasl-mechanism-alist (cons '("PLAIN" erc-sasl-plain) sans))
(authc (erc-sasl--get-user))
(port (if (numberp erc-session-port)
(number-to-string erc-session-port)
"0"))
;; In most cases, `erc-server-announced-name' won't be known.
(host (or erc-server-announced-name erc-session-server))
(mech (sasl-find-mechanism '("PLAIN")))
(client (sasl-make-client mech authc port host)))
(sasl-client-set-property client 'authenticator-name
(alist-get 'authzid erc-sasl--options))
client))
(cl-defmethod erc-sasl--create-client ((_ (eql scram-sha-256)))
"Create and return a new SCRAM-SHA-256 client."
(when (featurep 'sasl-scram-sha256)
(cl-call-next-method)))
(cl-defmethod erc-sasl--create-client ((_ (eql scram-sha-512)))
"Create and return a new SCRAM-SHA-512 client."
(when (featurep 'sasl-scram-sha256)
(cl-call-next-method)))
(cl-defmethod erc-sasl--create-client ((_ (eql ecdsa-nist256p-challenge)))
"Create and return a new ECDSA-NIST256P-CHALLENGE client."
(let ((keyfile (cdr (assq 'password erc-sasl--options))))
;; Better to signal usage errors now than inside a process filter.
(cond ((or (not (stringp keyfile)) (not (file-readable-p keyfile)))
(erc-display-error-notice
nil "`erc-sasl-password' not accessible as a file")
nil)
((not (executable-find "openssl"))
(erc-display-error-notice nil "Could not find openssl program")
nil)
(t
(let ((client (cl-call-next-method)))
(sasl-client-set-property client 'ecdsa-keyfile keyfile)
client)))))
;; This stands alone because it's also used by bug#49860.
(defun erc-sasl--init ()
(setq erc-sasl--state (make-erc-sasl--state))
;; If the previous attempt failed during registration, this may be
;; non-nil and contain erroneous values, but how can we detect that?
;; What if the server dropped the connection for some other reason?
(setq erc-sasl--options
(or (and erc--server-reconnecting
(alist-get 'erc-sasl--options erc--server-reconnecting))
`((user . ,erc-sasl-user)
(password . ,erc-sasl-password)
(mechanism . ,erc-sasl-mechanism)
(authzid . ,erc-sasl-authzid)))))
(defun erc-sasl--mechanism-offered-p (offered)
"Return non-nil when OFFERED appears among a list of mechanisms."
(string-match-p (rx-to-string
`(: (| bot ",")
,(symbol-name (alist-get 'mechanism erc-sasl--options))
(| eot ",")))
(downcase offered)))
(erc-define-catalog
'english
'((s902 . "ERR_NICKLOCKED nick %n unavailable: %s")
(s904 . "ERR_SASLFAIL (authentication failed) %s")
(s905 . "ERR SASLTOOLONG (credentials too long) %s")
(s906 . "ERR_SASLABORTED (authentication aborted) %s")
(s907 . "ERR_SASLALREADY (already authenticated) %s")
(s908 . "RPL_SASLMECHS (unsupported mechanism: %m) %s")))
(define-erc-module sasl nil
"Non-IRCv3 SASL support for ERC.
This doesn't solicit or validate a suite of supported mechanisms."
;; See bug#49860 for a CAP 3.2-aware WIP implementation.
((unless erc--target
(erc-sasl--init)
(let* ((mech (alist-get 'mechanism erc-sasl--options))
(client (erc-sasl--create-client mech)))
(unless client
(erc-display-error-notice
nil (format "Unknown or unsupported SASL mechanism: %s" mech))
(erc-error "Unknown or unsupported SASL mechanism: %s" mech))
(setf (erc-sasl--state-client erc-sasl--state) client))))
((kill-local-variable 'erc-sasl--state)
(kill-local-variable 'erc-sasl--options))
'local)
(define-erc-response-handler (AUTHENTICATE)
"Begin or resume an SASL session." nil
(if-let* ((response (car (erc-response.command-args parsed)))
((= 400 (length response))))
(cl-callf (lambda (s) (concat s response))
(erc-sasl--state-pending erc-sasl--state))
(cl-assert response t)
(when (string= "+" response)
(setq response ""))
(setf response (base64-decode-string
(concat (erc-sasl--state-pending erc-sasl--state)
response))
(erc-sasl--state-pending erc-sasl--state) nil)
(let ((client (erc-sasl--state-client erc-sasl--state))
(step (erc-sasl--state-step erc-sasl--state))
data)
(when step
(sasl-step-set-data step response))
(setq step (setf (erc-sasl--state-step erc-sasl--state)
(sasl-next-step client step))
data (sasl-step-data step))
(when (string= data "")
(setq data nil))
(when data
(setq data (base64-encode-string data t)))
(erc-server-send (concat "AUTHENTICATE " (or data "+"))))))
(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 an ERR_NICKLOCKED response." nil
(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
(when erc-sasl-mode
(unless erc-server-connected
(erc-server-send "CAP END")))
(erc-display-message parsed 'notice proc (erc-response.contents parsed)))
(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
(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 (string-join (cdr (erc-response.command-args 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."
(if-let* ((c (erc-sasl--state-client erc-sasl--state))
(m (sasl-mechanism-name (sasl-client-mechanism c))))
(progn
(erc-server-send "CAP REQ :sasl")
(if (and erc-session-password
(eq :password (alist-get 'password erc-sasl--options)))
(let (erc-session-password)
(erc-login))
(erc-login))
(erc-server-send (format "AUTHENTICATE %s" m)))
(erc-sasl--destroy erc-server-process)))
(provide 'erc-sasl)
;;; erc-sasl.el ends here
;;
;; Local Variables:
;; generated-autoload-file: "erc-loaddefs.el"
;; End:

View file

@ -1860,6 +1860,7 @@ removed from the list will be disabled."
(const :tag "readonly: Make displayed lines read-only" readonly)
(const :tag "replace: Replace text in messages" replace)
(const :tag "ring: Enable an input history" ring)
(const :tag "sasl: Enable SASL authentication" sasl)
(const :tag "scrolltobottom: Scroll to the bottom of the buffer"
scrolltobottom)
(const :tag "services: Identify to Nickserv (IRC Services) automatically"

View file

@ -0,0 +1,344 @@
;;; erc-sasl-tests.el --- Tests for erc-sasl. -*- lexical-binding:t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;;
;; This file is part of GNU Emacs.
;;
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(require 'ert-x)
(require 'erc-sasl)
(ert-deftest erc-sasl--mechanism-offered-p ()
(let ((erc-sasl--options '((mechanism . external))))
(should (erc-sasl--mechanism-offered-p "foo,external"))
(should (erc-sasl--mechanism-offered-p "external,bar"))
(should (erc-sasl--mechanism-offered-p "foo,external,bar"))
(should-not (erc-sasl--mechanism-offered-p "fooexternal"))
(should-not (erc-sasl--mechanism-offered-p "externalbar"))))
(ert-deftest erc-sasl--read-password--basic ()
(ert-info ("Explicit erc-sasl-password")
(let ((erc-sasl--options '((password . "foo"))))
(should (string= (erc-sasl--read-password nil) "foo"))))
(ert-info ("Explicit session password")
(let ((erc-session-password "foo")
(erc-sasl--options '((password . :password))))
(should (string= (erc-sasl--read-password nil) "foo"))))
(ert-info ("Fallback to prompt skip auth-source")
(should-not erc-sasl-auth-source-function)
(let ((erc-session-password "bar")
(erc-networks--id (erc-networks--id-create nil)))
(should (string= (ert-simulate-keys "bar\r"
(erc-sasl--read-password "?"))
"bar"))))
(ert-info ("Prompt when auth-source fails and `erc-sasl-password' null")
(let ((erc-sasl--options '((password)))
(erc-sasl-auth-source-function #'ignore))
(should (string= (ert-simulate-keys "baz\r"
(erc-sasl--read-password "pwd:"))
"baz")))))
(ert-deftest erc-sasl--read-password--auth-source ()
(ert-with-temp-file netrc-file
:text (string-join
(list
;; If you swap these first 2 lines, *1 below fails
"machine FSF.chat port 6697 user bob password sesame"
"machine GNU/chat port 6697 user bob password spam"
"machine MyHost port irc password 123")
"\n")
(let* ((auth-sources (list netrc-file))
(erc-session-server "irc.gnu.org")
(erc-session-port 6697)
(erc-networks--id (erc-networks--id-create nil))
calls
(erc-sasl-auth-source-function
(lambda (&rest r)
(push r calls)
(apply #'erc--auth-source-search r)))
erc-server-announced-name ; too early
auth-source-do-cache)
(ert-info ("Symbol as password specifies machine")
(let ((erc-sasl--options '((user . "bob") (password . FSF.chat)))
(erc-networks--id (make-erc-networks--id)))
(should (string= (erc-sasl--read-password nil) "sesame"))
(should (equal (pop calls) '(:user "bob" :host "FSF.chat")))))
(ert-info ("ID for :host and `erc-session-username' for :user") ; *1
(let ((erc-session-username "bob")
(erc-sasl--options '((user . :user) (password)))
(erc-networks--id (erc-networks--id-create 'GNU/chat)))
(should (string= (erc-sasl--read-password nil) "spam"))
(should (equal (pop calls) '(:user "bob" :host "GNU/chat")))))
(ert-info ("ID for :host and current nick for :user") ; *1
(let ((erc-server-current-nick "bob")
(erc-sasl--options '((user . :nick) (password)))
(erc-networks--id (erc-networks--id-create 'GNU/chat)))
(should (string= (erc-sasl--read-password nil) "spam"))
(should (equal (pop calls) '(:user "bob" :host "GNU/chat")))))
(ert-info ("Symbol as password, entry lacks user field")
(let ((erc-server-current-nick "fake")
(erc-sasl--options '((user . :nick) (password . MyHost)))
(erc-networks--id (erc-networks--id-create 'GNU/chat)))
(should (string= (erc-sasl--read-password nil) "123"))
(should (equal (pop calls) '(:user "fake" :host "MyHost"))))))))
(ert-deftest erc-sasl-create-client--plain ()
(let* ((erc-session-password "password123")
(erc-session-username "tester")
(erc-sasl--options '((user . :user) (password . :password)))
(erc-session-port 1667)
(erc-session-server "localhost")
(client (erc-sasl--create-client 'plain))
(result (sasl-next-step client nil)))
(should (equal (format "%S" [erc-sasl--plain-response
"\0tester\0password123"])
(format "%S" result)))
(should (string= (sasl-step-data result) "\0tester\0password123"))
(should-not (sasl-next-step client result)))
(should (equal (assoc-default "PLAIN" sasl-mechanism-alist) '(sasl-plain))))
(ert-deftest erc-sasl-create-client--external ()
(let* ((erc-server-current-nick "tester")
(erc-sasl--options '((user . :nick) (password . :password)))
(client (erc-sasl--create-client 'external)) ; unused ^
(result (sasl-next-step client nil)))
(should (equal (format "%S" [ignore nil]) (format "%S" result)))
(should-not (sasl-step-data result))
(should-not (sasl-next-step client result)))
(should-not (member "EXTERNAL" sasl-mechanisms))
(should-not (assoc-default "EXTERNAL" sasl-mechanism-alist)))
(ert-deftest erc-sasl-create-client--scram-sha-1 ()
(let* ((erc-sasl--options '((user . "jilles") (password . "sesame")
(authzid . "jilles")))
(mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" ""))
(sasl-unique-id-function (lambda () (pop mock-rvs)))
(client (erc-sasl--create-client 'scram-sha-1))
(step (sasl-next-step client nil)))
(ert-info ("Client's initial request")
(let ((req "n,a=jilles,n=jilles,r=c5RqLCZy0L4fGkKAZ0hujFBs"))
(should (equal (format "%S"
`[erc-compat--29-sasl-scram-client-first-message
,req])
(format "%S" step)))
(should (string= (sasl-step-data step) req))))
(ert-info ("Server's initial response")
(let ((resp (concat "r=c5RqLCZy0L4fGkKAZ0hujFBsXQoKcivqCw9iDZPSpb,"
"s=5mJO6d4rjCnsBU1X,"
"i=4096"))
(req (concat "c=bixhPWppbGxlcyw=,"
"r=c5RqLCZy0L4fGkKAZ0hujFBsXQoKcivqCw9iDZPSpb,"
"p=OVUhgPu8wEm2cDoVLfaHzVUYPWU=")))
(sasl-step-set-data step resp)
(setq step (sasl-next-step client step))
(should (equal (format "%S"
`[erc-sasl--scram-sha-1-client-final-message
,req])
(format "%S" step)))
(should (string= (sasl-step-data step) req))))
(ert-info ("Server's final message")
(let ((resp "v=ZWR23c9MJir0ZgfGf5jEtLOn6Ng="))
(sasl-step-set-data step resp)
(setq step (sasl-next-step client step))
(should-not (sasl-step-data step)))))
(should (eq sasl-unique-id-function #'sasl-unique-id-function)))
(ert-deftest erc-sasl-create-client--scram-sha-256 ()
(unless (featurep 'sasl-scram-sha256)
(ert-skip "Emacs lacks sasl-scram-sha256"))
(let* ((erc-server-current-nick "jilles")
(erc-session-password "sesame")
(erc-sasl--options '((user . :nick) (password . :password)
(authzid . "jilles")))
(mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" ""))
(sasl-unique-id-function (lambda () (pop mock-rvs)))
(client (erc-sasl--create-client 'scram-sha-256))
(step (sasl-next-step client nil)))
(ert-info ("Client's initial request")
(let ((req "n,a=jilles,n=jilles,r=c5RqLCZy0L4fGkKAZ0hujFBs"))
(should (equal (format "%S"
`[erc-compat--29-sasl-scram-client-first-message
,req])
(format "%S" step)))
(should (string= (sasl-step-data step) req))))
(ert-info ("Server's initial response")
(let ((resp (concat
"r=c5RqLCZy0L4fGkKAZ0hujFBse697140729d8445fb95ec94ceacb14b3,"
"s=MTk2M2VkMzM5ZmU0NDRiYmI0MzIyOGVhN2YwNzYwNmI=,"
"i=4096"))
(req (concat
"c=bixhPWppbGxlcyw=,"
"r=c5RqLCZy0L4fGkKAZ0hujFBse697140729d8445fb95ec94ceacb14b3,"
"p=1vDesVBzJmv0lX0Ae1kHFtdVHkC6j4gISKVqaR45HFg=")))
(sasl-step-set-data step resp)
(setq step (sasl-next-step client step))
(should (equal (format "%S"
`[erc-sasl--scram-sha-256-client-final-message
,req])
(format "%S" step)))
(should (string= (sasl-step-data step) req))))
(ert-info ("Server's final message")
(let ((resp "v=gUePTYSZN9xgcE06KSyKO9fUmSwH26qifoapXyEs75s="))
(sasl-step-set-data step resp)
(setq step (sasl-next-step client step))
(should-not (sasl-step-data step)))))
(should (eq sasl-unique-id-function #'sasl-unique-id-function)))
(ert-deftest erc-sasl-create-client--scram-sha-256--no-authzid ()
(unless (featurep 'sasl-scram-sha256)
(ert-skip "Emacs lacks sasl-scram-sha256"))
(let* ((erc-server-current-nick "jilles")
(erc-session-password "sesame")
(erc-sasl--options '((user . :nick) (password . :password) (authzid)))
(mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" ""))
(sasl-unique-id-function (lambda () (pop mock-rvs)))
(client (erc-sasl--create-client 'scram-sha-256))
(step (sasl-next-step client nil)))
(ert-info ("Client's initial request")
(let ((req "n,,n=jilles,r=c5RqLCZy0L4fGkKAZ0hujFBs"))
(should (equal (format "%S"
`[erc-compat--29-sasl-scram-client-first-message
,req])
(format "%S" step)))
(should (string= (sasl-step-data step) req))))
(ert-info ("Server's initial response")
(let ((resp (concat
"r=c5RqLCZy0L4fGkKAZ0hujFBsd4067f0afdb54c3dbd4fe645b84cae37,"
"s=ZTg1MmE1YmFhZGI1NDcyMjk3NzYwZmRjZDM3Y2I1OTM=,"
"i=4096"))
(req (concat
"c=biws,"
"r=c5RqLCZy0L4fGkKAZ0hujFBsd4067f0afdb54c3dbd4fe645b84cae37,"
"p=LP4sjJrjJKp5qTsARyZCppXpKLu4FMM284hNESPvGhI=")))
(sasl-step-set-data step resp)
(setq step (sasl-next-step client step))
(should (equal (format "%S"
`[erc-sasl--scram-sha-256-client-final-message
,req])
(format "%S" step)))
(should (string= (sasl-step-data step) req))))
(ert-info ("Server's final message")
(let ((resp "v=847WXfnmReGyE1qlq1And6R4bPBNROTZ7EMS/QrJtUM="))
(sasl-step-set-data step resp)
(setq step (sasl-next-step client step))
(should-not (sasl-step-data step)))))
(should (eq sasl-unique-id-function #'sasl-unique-id-function)))
(ert-deftest erc-sasl-create-client--scram-sha-512--no-authzid ()
(unless (featurep 'sasl-scram-sha256)
(ert-skip "Emacs lacks sasl-scram-sha512"))
(let* ((erc-server-current-nick "jilles")
(erc-session-password "sesame")
(erc-sasl--options '((user . :nick) (password . :password) (authzid)))
(mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" ""))
(sasl-unique-id-function (lambda () (pop mock-rvs)))
(client (erc-sasl--create-client 'scram-sha-512))
(step (sasl-next-step client nil)))
(ert-info ("Client's initial request")
(let ((req "n,,n=jilles,r=c5RqLCZy0L4fGkKAZ0hujFBs"))
(should (equal (format "%S"
`[erc-compat--29-sasl-scram-client-first-message
,req])
(format "%S" step)))
(should (string= (sasl-step-data step) req))))
(ert-info ("Server's initial response")
(let ((resp (concat
"r=c5RqLCZy0L4fGkKAZ0hujFBs54c592745ce14e559fcc3f27b15464f6,"
"s=YzMzOWZiY2U0YzcwNDA0M2I4ZGE2M2ZjOTBjODExZTM=,"
"i=4096"))
(req (concat
"c=biws,"
"r=c5RqLCZy0L4fGkKAZ0hujFBs54c592745ce14e559fcc3f27b15464f6,"
"p=vMBb9tKxFAfBtel087/GLbo4objAIYr1wM+mFv/jYLKXE"
"NUF0vynm81qQbywQE5ScqFFdAfwYMZq/lj4s0V1OA==")))
(sasl-step-set-data step resp)
(setq step (sasl-next-step client step))
(should (equal (format
"%S" `[erc-sasl--scram-sha-512-client-final-message
,req])
(format "%S" step)))
(should (string= (sasl-step-data step) req))))
(ert-info ("Server's final message")
(let ((resp (concat "v=Va7NIvt8wCdhvxnv+bZriSxGoto6On5EVnRHO/ece8zs0"
"qpQassdqir1Zlwh3e3EmBq+kcSy+ClNCsbzBpXe/w==")))
(sasl-step-set-data step resp)
(setq step (sasl-next-step client step))
(should-not (sasl-step-data step)))))
(should (eq sasl-unique-id-function #'sasl-unique-id-function)))
(defconst erc-sasl-tests-ecdsa-key-file "
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIIJueQ3W2IrGbe9wKdOI75yGS7PYZSj6W4tg854hlsvmoAoGCCqGSM49
AwEHoUQDQgAEAZmaVhNSMmV5r8FXPvKuMnqDKyIA9pDHN5TNMfiF3mMeikGgK10W
IRX9cyi2wdYg9mUUYyh9GKdBCYHGUJAiCA==
-----END EC PRIVATE KEY-----
")
(ert-deftest erc-sasl-create-client-ecdsa ()
:tags '(:unstable)
;; This is currently useless because it just roundtrips shelling out
;; to pkeyutl.
(ert-skip "Placeholder")
(unless (executable-find "openssl")
(ert-skip "System lacks openssl"))
(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))
(ert-with-temp-file sigfile
:prefix "ecdsa_sig"
:suffix ".sig"
:text (sasl-step-data step)
(with-temp-buffer
(set-buffer-multibyte nil)
(insert resp)
(let ((ec (call-process-region
(point-min) (point-max)
"openssl" 'delete t nil "pkeyutl"
"-inkey" keyfile "-sigfile" sigfile
"-verify")))
(unless (zerop ec)
(message "%s" (buffer-string)))
(should (zerop ec)))))))
(should-not (sasl-next-step client step)))))
;;; erc-sasl-tests.el ends here

View file

@ -0,0 +1,144 @@
;;; erc-scenarios-sasl.el --- SASL tests for ERC -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;;
;; This file is part of GNU Emacs.
;;
;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see
;; <https://www.gnu.org/licenses/>.
;;; Code:
(require 'ert-x)
(eval-and-compile
(let ((load-path (cons (ert-resource-directory) load-path)))
(require 'erc-scenarios-common)))
(require 'erc-sasl)
(ert-deftest erc-scenarios-sasl--plain ()
:tags '(:expensive-test)
(erc-scenarios-common-with-cleanup
((erc-scenarios-common-dialog "sasl")
(erc-server-flood-penalty 0.1)
(dumb-server (erc-d-run "localhost" t 'plain))
(port (process-contact dumb-server :service))
(erc-modules (cons 'sasl erc-modules))
(erc-sasl-password "password123")
(expect (erc-d-t-make-expecter)))
(ert-info ("Connect")
(with-current-buffer (erc :server "127.0.0.1"
:port port
:nick "tester"
:user "tester"
:full-name "tester")
(should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
(ert-info ("Notices received")
(with-current-buffer (erc-d-t-wait-for 10 (get-buffer "ExampleOrg"))
(funcall expect 10 "This server is in debug mode")
;; Regression "\0\0\0\0 ..." caused by (fillarray passphrase 0)
(should (string= erc-sasl-password "password123"))))))
(ert-deftest erc-scenarios-sasl--external ()
:tags '(:expensive-test)
(erc-scenarios-common-with-cleanup
((erc-scenarios-common-dialog "sasl")
(erc-server-flood-penalty 0.1)
(dumb-server (erc-d-run "localhost" t 'external))
(port (process-contact dumb-server :service))
(erc-modules (cons 'sasl erc-modules))
(erc-sasl-mechanism 'external)
(expect (erc-d-t-make-expecter)))
(ert-info ("Connect")
(with-current-buffer (erc :server "127.0.0.1"
:port port
:nick "tester"
:user "tester"
:full-name "tester")
(should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
(ert-info ("Notices received")
(with-current-buffer (erc-d-t-wait-for 10 (get-buffer "ExampleOrg"))
(funcall expect 10 "Authentication successful")
(funcall expect 10 "This server is in debug mode")))))
(ert-deftest erc-scenarios-sasl--plain-fail ()
:tags '(:expensive-test)
(erc-scenarios-common-with-cleanup
((erc-scenarios-common-dialog "sasl")
(erc-server-flood-penalty 0.1)
(dumb-server (erc-d-run "localhost" t 'plain-failed))
(port (process-contact dumb-server :service))
(erc-modules (cons 'sasl erc-modules))
(erc-sasl-password "wrong")
(erc-sasl-mechanism 'plain)
(expect (erc-d-t-make-expecter))
(buf nil))
(ert-info ("Connect")
(setq buf (erc :server "127.0.0.1"
:port port
:nick "tester"
:user "tester"
:full-name "tester"))
(let ((err (should-error
(with-current-buffer buf
(funcall expect 20 "Connection failed!")))))
(should (string-search "please review" (cadr err)))
(with-current-buffer buf
(funcall expect 10 "Opening connection")
(funcall expect 20 "SASL authentication failed")
(should-not (erc-server-process-alive)))))))
(defun erc-scenarios--common--sasl (mech)
(erc-scenarios-common-with-cleanup
((erc-scenarios-common-dialog "sasl")
(erc-server-flood-penalty 0.1)
(dumb-server (erc-d-run "localhost" t mech))
(port (process-contact dumb-server :service))
(erc-modules (cons 'sasl erc-modules))
(erc-sasl-user :nick)
(erc-sasl-mechanism mech)
(mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" ""))
(sasl-unique-id-function (lambda () (pop mock-rvs)))
(expect (erc-d-t-make-expecter)))
(ert-info ("Connect")
(with-current-buffer (erc :server "127.0.0.1"
:port port
:nick "jilles"
:password "sesame"
:full-name "jilles")
(should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
(ert-info ("Notices received")
(with-current-buffer (erc-d-t-wait-for 10 (get-buffer "jaguar"))
(funcall expect 10 "Found your hostname")
(funcall expect 20 "marked as being away")))))
(ert-deftest erc-scenarios-sasl--scram-sha-1 ()
:tags '(:expensive-test)
(let ((erc-sasl-authzid "jilles"))
(erc-scenarios--common--sasl 'scram-sha-1)))
(ert-deftest erc-scenarios-sasl--scram-sha-256 ()
:tags '(:expensive-test)
(unless (featurep 'sasl-scram-sha256)
(ert-skip "Emacs lacks sasl-scram-sha256"))
(erc-scenarios--common--sasl 'scram-sha-256))
;;; erc-scenarios-sasl.el ends here

View file

@ -0,0 +1,33 @@
;; -*- mode: lisp-data; -*-
((cap-req 10 "CAP REQ :sasl"))
((nick 1 "NICK tester"))
((user 1 "USER tester 0 * :tester"))
((auth-req 3.2 "AUTHENTICATE EXTERNAL")
(0.0 ":irc.example.org CAP * ACK :sasl")
(0.0 "AUTHENTICATE +"))
((auth-noop 3.2 "AUTHENTICATE +")
(0.0 ":irc.example.org 900 * * tester :You are now logged in as tester")
(0.0 ":irc.example.org 903 * :Authentication successful"))
((cap-end 3.2 "CAP END")
(0.0 ":irc.example.org 001 tester :Welcome to the ExampleOrg IRC Network tester")
(0.01 ":irc.example.org 002 tester :Your host is irc.example.org, running version oragono-2.6.1")
(0.01 ":irc.example.org 003 tester :This server was created Sat, 17 Jul 2021 09:06:42 UTC")
(0.01 ":irc.example.org 004 tester irc.example.org oragono-2.6.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
(0.0 ":irc.example.org 005 tester AWAYLEN=200 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
(0.01 ":irc.example.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY :are supported by this server")
(0.01 ":irc.example.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
(0.0 ":irc.example.org 251 tester :There are 1 users and 0 invisible on 1 server(s)")
(0.0 ":irc.example.org 252 tester 0 :IRC Operators online")
(0.0 ":irc.example.org 253 tester 0 :unregistered connections")
(0.0 ":irc.example.org 254 tester 0 :channels formed")
(0.0 ":irc.example.org 255 tester :I have 1 clients and 0 servers")
(0.0 ":irc.example.org 265 tester 1 1 :Current local users 1, max 1")
(0.21 ":irc.example.org 266 tester 1 1 :Current global users 1, max 1")
(0.0 ":irc.example.org 422 tester :MOTD File is missing"))
((mode-user 1.2 "MODE tester +i")
(0.0 ":irc.example.org 221 tester +Zi")
(0.0 ":irc.example.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))

View file

@ -0,0 +1,16 @@
;; -*- mode: lisp-data; -*-
((cap-req 10 "CAP REQ :sasl"))
((nick 1 "NICK tester"))
((user 1 "USER tester 0 * :tester")
(0.0 ":irc.foonet.org NOTICE * :*** Looking up your hostname...")
(0.0 ":irc.foonet.org NOTICE * :*** Found your hostname")
(0.0 ":irc.foonet.org CAP * ACK :cap-notify sasl"))
((authenticate-plain 3.2 "AUTHENTICATE PLAIN")
(0.0 ":irc.foonet.org AUTHENTICATE +"))
((authenticate-gimme 3.2 "AUTHENTICATE AHRlc3RlcgB3cm9uZw==")
(0.0 ":irc.foonet.org 900 * * tester :You are now logged in as tester")
(0.0 ":irc.foonet.org 904 * :SASL authentication failed: Invalid account credentials"))
((cap-end 3.2 "CAP END"))

View file

@ -0,0 +1,39 @@
;; -*- mode: lisp-data; -*-
((cap-req 10 "CAP REQ :sasl"))
((nick 1 "NICK tester"))
((user 1 "USER tester 0 * :tester")
(0.0 ":irc.example.org NOTICE * :*** Looking up your hostname...")
(0.0 ":irc.example.org NOTICE * :*** Found your hostname")
(0.0 ":irc.example.org CAP * ACK :sasl"))
((authenticate-plain 3.2 "AUTHENTICATE PLAIN")
(0.0 ":irc.example.org AUTHENTICATE +"))
((authenticate-gimme 3.2 "AUTHENTICATE AHRlc3RlcgBwYXNzd29yZDEyMw==")
(0.0 ":irc.example.org 900 * * tester :You are now logged in as tester")
(0.0 ":irc.example.org 903 * :Authentication successful"))
((cap-end 3.2 "CAP END")
(0.0 ":irc.example.org 001 tester :Welcome to the ExampleOrg IRC Network tester")
(0.01 ":irc.example.org 002 tester :Your host is irc.example.org, running version oragono-2.6.1")
(0.01 ":irc.example.org 003 tester :This server was created Sat, 17 Jul 2021 09:06:42 UTC")
(0.01 ":irc.example.org 004 tester irc.example.org oragono-2.6.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
(0.0 ":irc.example.org 005 tester AWAYLEN=200 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
(0.01 ":irc.example.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY :are supported by this server")
(0.01 ":irc.example.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
(0.0 ":irc.example.org 251 tester :There are 1 users and 0 invisible on 1 server(s)")
(0.0 ":irc.example.org 252 tester 0 :IRC Operators online")
(0.0 ":irc.example.org 253 tester 0 :unregistered connections")
(0.0 ":irc.example.org 254 tester 0 :channels formed")
(0.0 ":irc.example.org 255 tester :I have 1 clients and 0 servers")
(0.0 ":irc.example.org 265 tester 1 1 :Current local users 1, max 1")
(0.21 ":irc.example.org 266 tester 1 1 :Current global users 1, max 1")
(0.0 ":irc.example.org 422 tester :MOTD File is missing"))
((mode-user 1.2 "MODE tester +i")
(0.0 ":irc.example.org 221 tester +Zi")
(0.0 ":irc.example.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
((quit 5 "QUIT :\2ERC\2")
(0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit"))
((drop 1 DROP))

View file

@ -0,0 +1,47 @@
;;; -*- mode: lisp-data -*-
((cap-req 5.2 "CAP REQ :sasl"))
((nick 10 "NICK jilles"))
((user 10 "USER user 0 * :jilles")
(0 "NOTICE AUTH :*** Processing connection to jaguar.test")
(0 "NOTICE AUTH :*** Looking up your hostname...")
(0 "NOTICE AUTH :*** Checking Ident")
(0 "NOTICE AUTH :*** No Ident response")
(0 "NOTICE AUTH :*** Found your hostname")
(0 ":jaguar.test CAP jilles ACK :sasl"))
((auth-init 10 "AUTHENTICATE SCRAM-SHA-1")
(0 "AUTHENTICATE +"))
((auth-challenge 10 "AUTHENTICATE bixhPWppbGxlcyxuPWppbGxlcyxyPWM1UnFMQ1p5MEw0ZkdrS0FaMGh1akZCcw==")
(0 "AUTHENTICATE cj1jNVJxTENaeTBMNGZHa0tBWjBodWpGQnNYUW9LY2l2cUN3OWlEWlBTcGIscz01bUpPNmQ0cmpDbnNCVTFYLGk9NDA5Ng=="))
((auth-final 10 "AUTHENTICATE Yz1iaXhoUFdwcGJHeGxjeXc9LHI9YzVScUxDWnkwTDRmR2tLQVowaHVqRkJzWFFvS2NpdnFDdzlpRFpQU3BiLHA9T1ZVaGdQdTh3RW0yY0RvVkxmYUh6VlVZUFdVPQ==")
(0 "AUTHENTICATE dj1aV1IyM2M5TUppcjBaZ2ZHZjVqRXRMT242Tmc9"))
((auth-done 10 "AUTHENTICATE +")
(0 ":jaguar.test 900 jilles jilles!jilles@localhost.stack.nl jilles :You are now logged in as jilles")
(0 ":jaguar.test 903 jilles :SASL authentication successful"))
((cap-end 10.2 "CAP END")
(0 ":jaguar.test 001 jilles :Welcome to the jaguar IRC Network jilles!~jilles@127.0.0.1")
(0 ":jaguar.test 002 jilles :Your host is jaguar.test, running version InspIRCd-3")
(0 ":jaguar.test 003 jilles :This server was created 09:44:05 Dec 24 2020")
(0 ":jaguar.test 004 jilles jaguar.test InspIRCd-3 BILRSWcghiorswz ABEFHIJLMNOQRSTXYabcefghijklmnopqrstuvz :BEFHIJLXYabefghjkloqv")
(0 ":jaguar.test 005 jilles ACCEPT=30 AWAYLEN=200 BOT=B CALLERID=g CASEMAPPING=rfc1459 CHANLIMIT=#:120 CHANMODES=IXbeg,k,BEFHJLfjl,AMNOQRSTcimnprstuz CHANNELLEN=64 CHANTYPES=# ELIST=CMNTU ESILENCE=CcdiNnPpTtx EXCEPTS=e :are supported by this server")
(0 ":jaguar.test 005 jilles EXTBAN=,ANOQRSTUacmnprz HOSTLEN=64 INVEX=I KEYLEN=32 KICKLEN=255 LINELEN=512 MAXLIST=I:100,X:100,b:100,e:100,g:100 MAXTARGETS=20 MODES=20 MONITOR=30 NAMELEN=128 NAMESX NETWORK=jaguar :are supported by this server")
(0 ":jaguar.test 005 jilles NICKLEN=31 PREFIX=(Yqaohv)!~&@%+ REMOVE SAFELIST SECURELIST=60 SILENCE=32 STATUSMSG=!~&@%+ TOPICLEN=307 UHNAMES USERIP USERLEN=11 USERMODES=,,s,BILRSWcghiorwz WATCH=30 :are supported by this server")
(0 ":jaguar.test 005 jilles :are supported by this server")
(0 ":jaguar.test 251 jilles :There are 740 users and 108 invisible on 11 servers")
(0 ":jaguar.test 252 jilles 10 :operator(s) online")
(0 ":jaguar.test 254 jilles 373 :channels formed")
(0 ":jaguar.test 255 jilles :I have 28 clients and 1 servers")
(0 ":jaguar.test 265 jilles :Current local users: 28 Max: 29")
(0 ":jaguar.test 266 jilles :Current global users: 848 Max: 879")
(0 ":jaguar.test 375 jilles :jaguar.test message of the day")
(0 ":jaguar.test 372 jilles : ~~ some message of the day ~~")
(0 ":jaguar.test 372 jilles : ~~ or rkpryyrag gb rnpu bgure ~~")
(0 ":jaguar.test 376 jilles :End of message of the day."))
((mode-user 1.2 "MODE jilles +i")
(0 ":jilles!~jilles@127.0.0.1 MODE jilles :+ri")
(0 ":jaguar.test 306 jilles :You have been marked as being away"))

View file

@ -0,0 +1,47 @@
;;; -*- mode: lisp-data -*-
((cap-req 5.2 "CAP REQ :sasl"))
((nick 10 "NICK jilles"))
((user 10 "USER user 0 * :jilles")
(0 "NOTICE AUTH :*** Processing connection to jaguar.test")
(0 "NOTICE AUTH :*** Looking up your hostname...")
(0 "NOTICE AUTH :*** Checking Ident")
(0 "NOTICE AUTH :*** No Ident response")
(0 "NOTICE AUTH :*** Found your hostname")
(0 ":jaguar.test CAP jilles ACK :sasl"))
((auth-init 10 "AUTHENTICATE SCRAM-SHA-256")
(0 "AUTHENTICATE +"))
((auth-challenge 10 "AUTHENTICATE biwsbj1qaWxsZXMscj1jNVJxTENaeTBMNGZHa0tBWjBodWpGQnM=")
(0 "AUTHENTICATE cj1jNVJxTENaeTBMNGZHa0tBWjBodWpGQnNkNDA2N2YwYWZkYjU0YzNkYmQ0ZmU2NDViODRjYWUzNyxzPVpUZzFNbUUxWW1GaFpHSTFORGN5TWprM056WXdabVJqWkRNM1kySTFPVE09LGk9NDA5Ng=="))
((auth-final 10 "AUTHENTICATE Yz1iaXdzLHI9YzVScUxDWnkwTDRmR2tLQVowaHVqRkJzZDQwNjdmMGFmZGI1NGMzZGJkNGZlNjQ1Yjg0Y2FlMzcscD1MUDRzakpyakpLcDVxVHNBUnlaQ3BwWHBLTHU0Rk1NMjg0aE5FU1B2R2hJPQ==")
(0 "AUTHENTICATE dj04NDdXWGZubVJlR3lFMXFscTFBbmQ2UjRiUEJOUk9UWjdFTVMvUXJKdFVNPQ=="))
((auth-done 10 "AUTHENTICATE +")
(0 ":jaguar.test 900 jilles jilles!jilles@localhost.stack.nl jilles :You are now logged in as jilles")
(0 ":jaguar.test 903 jilles :SASL authentication successful"))
((cap-end 10.2 "CAP END")
(0 ":jaguar.test 001 jilles :Welcome to the jaguar IRC Network jilles!~jilles@127.0.0.1")
(0 ":jaguar.test 002 jilles :Your host is jaguar.test, running version InspIRCd-3")
(0 ":jaguar.test 003 jilles :This server was created 09:44:05 Dec 24 2020")
(0 ":jaguar.test 004 jilles jaguar.test InspIRCd-3 BILRSWcghiorswz ABEFHIJLMNOQRSTXYabcefghijklmnopqrstuvz :BEFHIJLXYabefghjkloqv")
(0 ":jaguar.test 005 jilles ACCEPT=30 AWAYLEN=200 BOT=B CALLERID=g CASEMAPPING=rfc1459 CHANLIMIT=#:120 CHANMODES=IXbeg,k,BEFHJLfjl,AMNOQRSTcimnprstuz CHANNELLEN=64 CHANTYPES=# ELIST=CMNTU ESILENCE=CcdiNnPpTtx EXCEPTS=e :are supported by this server")
(0 ":jaguar.test 005 jilles EXTBAN=,ANOQRSTUacmnprz HOSTLEN=64 INVEX=I KEYLEN=32 KICKLEN=255 LINELEN=512 MAXLIST=I:100,X:100,b:100,e:100,g:100 MAXTARGETS=20 MODES=20 MONITOR=30 NAMELEN=128 NAMESX NETWORK=jaguar :are supported by this server")
(0 ":jaguar.test 005 jilles NICKLEN=31 PREFIX=(Yqaohv)!~&@%+ REMOVE SAFELIST SECURELIST=60 SILENCE=32 STATUSMSG=!~&@%+ TOPICLEN=307 UHNAMES USERIP USERLEN=11 USERMODES=,,s,BILRSWcghiorwz WATCH=30 :are supported by this server")
(0 ":jaguar.test 005 jilles :are supported by this server")
(0 ":jaguar.test 251 jilles :There are 740 users and 108 invisible on 11 servers")
(0 ":jaguar.test 252 jilles 10 :operator(s) online")
(0 ":jaguar.test 254 jilles 373 :channels formed")
(0 ":jaguar.test 255 jilles :I have 28 clients and 1 servers")
(0 ":jaguar.test 265 jilles :Current local users: 28 Max: 29")
(0 ":jaguar.test 266 jilles :Current global users: 848 Max: 879")
(0 ":jaguar.test 375 jilles :jaguar.test message of the day")
(0 ":jaguar.test 372 jilles : ~~ some message of the day ~~")
(0 ":jaguar.test 372 jilles : ~~ or rkpryyrag gb rnpu bgure ~~")
(0 ":jaguar.test 376 jilles :End of message of the day."))
((mode-user 1.2 "MODE jilles +i")
(0 ":jilles!~jilles@127.0.0.1 MODE jilles :+ri")
(0 ":jaguar.test 306 jilles :You have been marked as being away"))