emacs/test/lisp/net/ntlm-tests.el

428 lines
16 KiB
EmacsLisp

;;; ntlm-tests.el --- tests for ntlm.el -*- lexical-binding: t -*-
;; Copyright (C) 2020-2024 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:
;; Run this with `NTLM_TESTS_VERBOSE=1' to get verbose debugging.
;;; Code:
(require 'ert)
(require 'ert-x)
(require 'ntlm)
(defsubst ntlm-tests-message (format-string &rest arguments)
"Print a message conditional on an environment variable being set.
FORMAT-STRING and ARGUMENTS are passed to the message function."
(when (getenv "NTLM_TESTS_VERBOSE")
(apply #'message (concat "ntlm-tests: " format-string) arguments)))
;; This is the Lisp bignum implementation of `ntlm--time-to-timestamp',
;; for reference.
(defun ntlm-tests--time-to-timestamp (time)
"Convert TIME to an NTLMv2 timestamp.
Return a unibyte string representing the number of tenths of a
microsecond since January 1, 1601 as a 64-bit little-endian
signed integer. TIME must be on the form (HIGH LOW USEC PSEC)."
(let* ((s (+ (ash (nth 0 time) 16) (nth 1 time)))
(us (nth 2 time))
(ps (nth 3 time))
(tenths-of-us-since-jan-1-1601
(+ (* s 10000000) (* us 10) (/ ps 100000)
;; tenths of microseconds between 1601-01-01 and 1970-01-01
116444736000000000)))
(apply #'unibyte-string
(mapcar (lambda (i)
(logand (ash tenths-of-us-since-jan-1-1601 (* i -8))
#xff))
(number-sequence 0 7)))))
(ert-deftest ntlm-time-to-timestamp ()
;; Verify poor man's bignums in implementation that can run on Emacs < 27.1.
(let ((time '(24471 63910 412962 0)))
(should (equal (ntlm--time-to-timestamp time)
(ntlm-tests--time-to-timestamp time))))
(let ((time '(397431 65535 999999 999999)))
(should (equal (ntlm--time-to-timestamp time)
(ntlm-tests--time-to-timestamp time)))))
(defvar ntlm-tests--username-oem "ntlm"
"The username for NTLM authentication tests, in OEM string encoding.")
(defvar ntlm-tests--username-unicode
(ntlm-ascii2unicode ntlm-tests--username-oem
(length ntlm-tests--username-oem))
"The username for NTLM authentication tests, in Unicode string encoding.")
(defvar ntlm-tests--password "ntlm"
"The password used for NTLM authentication tests.")
(defvar ntlm-tests--client-supports-unicode nil
"Non-nil if client supports Unicode strings.
If client only supports OEM strings, nil.")
(defvar ntlm-tests--challenge nil "The global random challenge.")
(defun ntlm-server-build-type-2 ()
"Return an NTLM Type 2 message as a string.
This string will be returned from the NTLM server to the NTLM client."
(let ((target (if ntlm-tests--client-supports-unicode
(ntlm-ascii2unicode "DOMAIN" (length "DOMAIN"))
"DOMAIN"))
(target-information ntlm-tests--password)
;; Flag byte 1 flags.
(_negotiate-unicode 1)
(negotiate-oem 2)
(request-target 4)
;; Flag byte 2 flags.
(negotiate-ntlm 2)
(_negotiate-local-call 4)
(_negotiate-always-sign 8)
;; Flag byte 3 flags.
(_target-type-domain 1)
(_target-type-server 2)
(target-type-share 4)
(_negotiate-ntlm2-key 8)
(negotiate-target-information 128)
;; Flag byte 4 flags, unused.
(_negotiate-128 32)
(_negotiate-56 128))
(concat
;; Signature.
"NTLMSSP" (unibyte-string 0)
;; Type 2.
(unibyte-string 2 0 0 0)
;; Target length
(unibyte-string (length target) 0)
;; Target allocated space.
(unibyte-string (length target) 0)
;; Target offset.
(unibyte-string 48 0 0 0)
;; Flags.
;; Flag byte 1.
;; Tell the client that this test server only supports OEM
;; strings. This test server will handle Unicode strings
;; anyway though.
(unibyte-string (logior negotiate-oem request-target))
;; Flag byte 2.
(unibyte-string negotiate-ntlm)
;; Flag byte 3.
(unibyte-string (logior negotiate-target-information target-type-share))
;; Flag byte 4. Not sure what 2 means here.
(unibyte-string 2)
;; Challenge. Set this to (unibyte-string 1 2 3 4 5 6 7 8)
;; instead of (ntlm-generate-nonce) to hold constant for
;; debugging.
(setq ntlm-tests--challenge (ntlm-generate-nonce))
;; Context.
(make-string 8 0)
(unibyte-string (length target-information) 0)
(unibyte-string (length target-information) 0)
(unibyte-string 54 0 0 0)
target
target-information)))
(defun ntlm-server-hash (challenge blob username password)
"Hash CHALLENGE, BLOB, USERNAME and PASSWORD for a Type 3 check."
(hmac-md5 (concat challenge blob)
(hmac-md5 (concat
(upcase
;; This calculation always uses
;; Unicode username, even when the
;; server only supports OEM strings.
(ntlm-ascii2unicode username (length username))) "")
(cadr (ntlm-get-password-hashes password)))))
(defun ntlm-server-check-authorization (authorization-string)
"Return t if AUTHORIZATION-STRING correctly authenticates the user."
(let* ((binary (base64-decode-string
(caddr (split-string authorization-string " "))))
(_lm-response-length (md4-unpack-int16 (substring binary 12 14)))
(_lm-response-offset
(cdr (md4-unpack-int32 (substring binary 16 20))))
(ntlm-response-length (md4-unpack-int16 (substring binary 20 22)))
(ntlm-response-offset
(cdr (md4-unpack-int32 (substring binary 24 28))))
(ntlm-hash
(substring binary ntlm-response-offset (+ ntlm-response-offset 16)))
(username-length (md4-unpack-int16 (substring binary 36 38)))
(username-offset (cdr (md4-unpack-int32 (substring binary 40 44))))
(username (substring binary username-offset
(+ username-offset username-length))))
(if (equal ntlm-response-length 24)
(let* ((expected
(ntlm-smb-owf-encrypt
(cadr (ntlm-get-password-hashes ntlm-tests--password))
ntlm-tests--challenge))
(received (substring binary ntlm-response-offset
(+ ntlm-response-offset
ntlm-response-length))))
(ntlm-tests-message "Got NTLMv1 response:")
(ntlm-tests-message "Expected hash: ===%S===" expected)
(ntlm-tests-message "Got hash: ===%S===" received)
(ntlm-tests-message "Expected username: ===%S==="
ntlm-tests--username-oem)
(ntlm-tests-message "Got username: ===%S===" username)
(and (or (equal username ntlm-tests--username-oem)
(equal username ntlm-tests--username-unicode))
(equal expected received)))
(let* ((ntlm-response-blob
(substring binary (+ ntlm-response-offset 16)
(+ (+ ntlm-response-offset 16)
(- ntlm-response-length 16))))
(_ntlm-timestamp (substring ntlm-response-blob 8 16))
(_ntlm-nonce (substring ntlm-response-blob 16 24))
(_target-length (md4-unpack-int16 (substring binary 28 30)))
(_target-offset
(cdr (md4-unpack-int32 (substring binary 32 36))))
(_workstation-length (md4-unpack-int16 (substring binary 44 46)))
(_workstation-offset
(cdr (md4-unpack-int32 (substring binary 48 52)))))
(cond
;; This test server claims to only support OEM strings,
;; but also checks Unicode strings.
((or (equal username ntlm-tests--username-oem)
(equal username ntlm-tests--username-unicode))
(let* ((password ntlm-tests--password)
(ntlm-hash-from-type-3 (ntlm-server-hash
ntlm-tests--challenge
ntlm-response-blob
;; Always -oem since
;; `ntlm-server-hash'
;; always converts it to
;; Unicode.
ntlm-tests--username-oem
password)))
(ntlm-tests-message "Got NTLMv2 response:")
(ntlm-tests-message "Expected hash: ==%S==" ntlm-hash)
(ntlm-tests-message "Got hash: ==%S==" ntlm-hash-from-type-3)
(ntlm-tests-message "Expected username: ===%S==="
ntlm-tests--username-oem)
(ntlm-tests-message " or username: ===%S==="
ntlm-tests--username-unicode)
(ntlm-tests-message "Got username: ===%S===" username)
(equal ntlm-hash ntlm-hash-from-type-3)))
(t
nil))))))
(require 'eieio)
(require 'cl-lib)
;; Silence some byte-compiler warnings that occur when
;; web-server/web-server.el is not found.
(eval-when-compile (cl-pushnew 'headers eieio--known-slot-names)
(cl-pushnew 'process eieio--known-slot-names))
(declare-function ws-send nil)
(declare-function ws-parse-request nil)
(declare-function ws-start nil)
(declare-function ws-stop-all nil)
(eval-and-compile
(push (expand-file-name "../elpa/packages/web-server/" source-directory)
load-path)
(require 'web-server nil t)
(push (expand-file-name "../elpa/packages/url-http-ntlm/" source-directory)
load-path)
(require 'url-http-ntlm nil t))
(defun ntlm-server-do-token (request _process)
"Process an NTLM client's REQUEST.
PROCESS is unused."
(with-slots (process headers) request
(let* ((header-alist (cdr headers))
(authorization-header (assoc ':AUTHORIZATION header-alist))
(authorization-string (cdr authorization-header)))
(if (and (stringp authorization-string)
(string-match "NTLM " authorization-string))
(let* ((challenge (substring authorization-string (match-end 0)))
(binary (base64-decode-string challenge))
(type (aref binary 8))
;; Flag byte 1 flags.
(negotiate-unicode 1)
(negotiate-oem 2)
(flags-byte-1 (aref binary 12))
(client-supports-unicode
(not (zerop (logand flags-byte-1 negotiate-unicode))))
(client-supports-oem
(not (zerop (logand flags-byte-1 negotiate-oem))))
(connection-header (assoc ':CONNECTION header-alist))
(_keep-alive
(when connection-header (cdr connection-header)))
(response
(cl-case type
(1
;; Return Type 2 message.
(when (and (not client-supports-unicode)
(not client-supports-oem))
(warn (concat
"Weird client supports neither Unicode"
" nor OEM strings, using OEM.")))
(setq ntlm-tests--client-supports-unicode
client-supports-unicode)
(concat
"HTTP/1.1 401 Unauthorized\r\n"
"WWW-Authenticate: NTLM "
(base64-encode-string
(ntlm-server-build-type-2) t) "\r\n"
"WWW-Authenticate: Negotiate\r\n"
"WWW-Authenticate: Basic realm=\"domain\"\r\n"
"Content-Length: 0\r\n\r\n"))
(3
(if (ntlm-server-check-authorization
authorization-string)
"HTTP/1.1 200 OK\r\n\r\nAuthenticated.\r\n"
(progn
(if process
(set-process-filter process nil)
(error "Type 3 message found first?"))
(concat "HTTP/1.1 401 Unauthorized\r\n\r\n"
"Access Denied.\r\n")))))))
(if response
(ws-send process response)
(when process
(set-process-filter process nil)))
(when (equal type 3)
(set-process-filter process nil)
(process-send-eof process)))
(progn
;; Did not get NTLM anything.
(set-process-filter process nil)
(process-send-eof process)
(concat "HTTP/1.1 401 Unauthorized\r\n\r\n"
"Access Denied.\r\n"))))))
(defun ntlm-server-filter (process string)
"Read from PROCESS a STRING and treat it as a request from an NTLM client."
(let ((request (make-instance 'ws-request
:process process :pending string)))
(if (ws-parse-request request)
(ntlm-server-do-token request process)
(error "Failed to parse request"))))
(defun ntlm-server-handler (request)
"Handle an HTTP REQUEST."
(with-slots (process headers) request
(let* ((header-alist (cdr headers))
(authorization-header (assoc ':AUTHORIZATION header-alist))
(connection-header (assoc ':CONNECTION header-alist))
(keep-alive (when connection-header (cdr connection-header)))
(response (concat
"HTTP/1.1 401 Unauthorized\r\n"
"WWW-Authenticate: Negotiate\r\n"
"WWW-Authenticate: NTLM\r\n"
"WWW-Authenticate: Basic realm=\"domain\"\r\n"
"Content-Length: 0\r\n\r\n")))
(if (null authorization-header)
;; Tell client to use NTLM. Firefox will create a new
;; connection.
(progn
(process-send-string process response)
(process-send-eof process))
(progn
(ntlm-server-do-token request nil)
(set-process-filter process #'ntlm-server-filter)
(if (equal (upcase keep-alive) "KEEP-ALIVE")
:keep-alive
(error "NTLM server expects keep-alive connection header")))))))
(defun ntlm-server-start ()
"Start an NTLM server on port 8080 for testing."
(ws-start 'ntlm-server-handler 8080))
(defun ntlm-server-stop ()
"Stop the NTLM server."
(ws-stop-all))
(defvar ntlm-tests--result-buffer nil "Final NTLM result buffer.")
(require 'url)
(defun ntlm-tests--url-retrieve-internal-around (original &rest arguments)
"Save the result buffer from a `url-retrieve-internal' to a global variable.
ORIGINAL is the original `url-retrieve-internal' function and
ARGUMENTS are passed to it."
(setq ntlm-tests--result-buffer (apply original arguments)))
(defun ntlm-tests--authenticate ()
"Authenticate using credentials from the authinfo resource file."
(setq ntlm-tests--result-buffer nil)
(let ((auth-sources (list (ert-resource-file "authinfo")))
(auth-source-do-cache nil)
(auth-source-debug (when (getenv "NTLM_TESTS_VERBOSE") 'trivia)))
(ntlm-tests-message "Using auth-sources: %S" auth-sources)
(url-retrieve-synchronously "http://localhost:8080"))
(sleep-for 0.1)
(ntlm-tests-message "Results are in: %S" ntlm-tests--result-buffer)
(with-current-buffer ntlm-tests--result-buffer
(buffer-string)))
(defun ntlm-tests--start-server-authenticate-stop-server ()
"Start an NTLM server, authenticate against it, then stop the server."
(advice-add #'url-retrieve-internal
:around #'ntlm-tests--url-retrieve-internal-around)
(ntlm-server-stop)
(ntlm-server-start)
(let ((result (ntlm-tests--authenticate)))
(advice-remove #'url-retrieve-internal
#'ntlm-tests--url-retrieve-internal-around)
(ntlm-server-stop)
result))
(defvar ntlm-tests--successful-result
(concat "HTTP/1.1 200 OK\n\nAuthenticated." (unibyte-string 13) "\n")
"Expected result of successful NTLM authentication.")
(require 'find-func)
(defun ntlm-tests--ensure-ws-parse-ntlm-support ()
"Ensure NTLM special-case in `ws-parse'."
(let* ((hit (find-function-search-for-symbol
'ws-parse nil (locate-file "web-server.el" load-path)))
(buffer (car hit))
(position (cdr hit)))
(with-current-buffer buffer
(goto-char position)
(search-forward-regexp
":NTLM" (save-excursion (forward-sexp) (point)) t))))
(require 'lisp-mnt)
(defvar ntlm-tests--dependencies-present
(and (featurep 'url-http-ntlm)
(version<= "2.0.4"
(lm-version (locate-file "url-http-ntlm.el" load-path)))
(featurep 'web-server)
(ntlm-tests--ensure-ws-parse-ntlm-support))
"Non-nil if GNU ELPA test dependencies were loaded.")
(ert-deftest ntlm-authentication ()
"Check ntlm.el's implementation of NTLM authentication over HTTP."
(skip-unless ntlm-tests--dependencies-present)
(should (equal (ntlm-tests--start-server-authenticate-stop-server)
ntlm-tests--successful-result)))
(ert-deftest ntlm-authentication-old-compatibility-level ()
(skip-unless ntlm-tests--dependencies-present)
(setq ntlm-compatibility-level 0)
(should (equal (ntlm-tests--start-server-authenticate-stop-server)
ntlm-tests--successful-result)))
(provide 'ntlm-tests)
;;; ntlm-tests.el ends here