aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJarno Malmari2017-04-01 09:19:46 +0300
committerEli Zaretskii2017-04-01 09:19:46 +0300
commit5b264d88792fec2a31a48c0de5ffe396c3c14604 (patch)
treeec7ddb582bdcca9e795cc803046c35207b6e254e
parent226cafd24df9c233f6359c93273d4da22db7f62d (diff)
downloademacs-5b264d88792fec2a31a48c0de5ffe396c3c14604.tar.gz
emacs-5b264d88792fec2a31a48c0de5ffe396c3c14604.zip
Initial implementation of HTTP Digest qop for url
This also refactors digest authentication functions in url-auth.el. * lisp/url/url-auth.el (url-digest-auth, url-digest-auth-create-key): (url-digest-auth-build-response, url-digest-auth-directory-id-assoc): (url-digest-auth-name-value-string, url-digest-auth-source-creds): (url-digest-cached-key, url-digest-cache-key, url-digest-find-creds): (url-digest-find-new-key, url-digest-prompt-creds): Add new functions to simplify code and aid in unit testing. (url-digest-auth-build-response): Hook up new functionality, or fall back to previous. (url-digest-auth-make-request-digest-qop): (url-digest-auth-make-cnonce, url-digest-auth-nonce-count): (url-digest-auth-name-value-string): Add new helper functions. * test/lisp/url/url-auth-tests.el (url-auth-test-colonjoin): (url-auth-test-digest-ha1, url-auth-test-digest-ha2): (url-auth-test-digest-request-digest): Add a few tests as now more features are testable via intermediate functions. (url-auth-test-challenges, url-auth-test-digest-request-digest): Test the new implementation. Parts of these were accidentally already merged in the past.
-rw-r--r--lisp/url/url-auth.el403
-rw-r--r--test/lisp/url/url-auth-tests.el51
2 files changed, 347 insertions, 107 deletions
diff --git a/lisp/url/url-auth.el b/lisp/url/url-auth.el
index 7b6cdd53790..2885d4e12e2 100644
--- a/lisp/url/url-auth.el
+++ b/lisp/url/url-auth.el
@@ -131,8 +131,8 @@ instead of the filename inheritance method."
131;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 131;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
132;;; Digest authorization code 132;;; Digest authorization code
133;;; ------------------------ 133;;; ------------------------
134;;; This implements the DIGEST authorization type. See the internet draft 134;;; This implements the DIGEST authorization type. See RFC 2617
135;;; ftp://ds.internic.net/internet-drafts/draft-ietf-http-digest-aa-01.txt 135;;; https://www.ietf.org/rfc/rfc2617.txt
136;;; for the complete documentation on this type. 136;;; for the complete documentation on this type.
137;;; 137;;;
138;;; This is very secure 138;;; This is very secure
@@ -143,107 +143,306 @@ Its value is an assoc list of assoc lists. The first assoc list is
143keyed by the server name. The cdr of this is an assoc list based 143keyed by the server name. The cdr of this is an assoc list based
144on the \"directory\" specified by the url we are looking up.") 144on the \"directory\" specified by the url we are looking up.")
145 145
146(defsubst url-digest-auth-colonjoin (&rest args)
147 "Concatenate ARGS as strings with colon as a separator."
148 (mapconcat 'identity args ":"))
149
150(defsubst url-digest-auth-kd (data secret)
151 "Apply digest algorithm to DATA using SECRET and return the result."
152 (md5 (url-digest-auth-colonjoin secret data)))
153
154(defsubst url-digest-auth-make-ha1 (user realm password)
155 "Compute checksum out of strings USER, REALM, and PASSWORD."
156 (md5 (url-digest-auth-colonjoin user realm password)))
157
158(defsubst url-digest-auth-make-ha2 (method digest-uri)
159 "Compute checksum out of strings METHOD and DIGEST-URI."
160 (md5 (url-digest-auth-colonjoin method digest-uri)))
161
162(defsubst url-digest-auth-make-request-digest (ha1 ha2 nonce)
163 "Construct the request-digest from hash strings HA1, HA2, and NONCE.
164This is the value that server receives as a proof that user knows
165a password."
166 (url-digest-auth-kd (url-digest-auth-colonjoin nonce ha2) ha1))
167
168(defsubst url-digest-auth-make-request-digest-qop (qop ha1 ha2 nonce nc cnonce)
169 "Construct the request-digest with qop.
170QOP describes the \"quality of protection\" and algorithm to use.
171All of the strings QOP, HA1, HA2, NONCE, NC, and CNONCE are
172combined into a single hash value that proves to a server the
173user knows a password. It's worth noting that HA2 already
174depends on value of QOP."
175 (url-digest-auth-kd (url-digest-auth-colonjoin
176 nonce nc cnonce qop ha2) ha1))
177
178(defsubst url-digest-auth-directory-id (url realm)
179 "Make an identifier for selecting a key in key cache.
180The identifier is made either from URL or REALM. It represents a
181protection space within a server so that one server can have
182multiple authorizations."
183 (or realm (or (url-file-directory (url-filename url)) "/")))
184
185(defsubst url-digest-auth-server-id (url)
186 "Make an identifier for selecting a server in key cache.
187The identifier is made from URL's host and port. Together with
188`url-digest-auth-directory-id' these identify a single key in the
189key cache `url-digest-auth-storage'."
190 (format "%s:%d" (url-host url) (url-port url)))
191
192(defun url-digest-auth-make-cnonce ()
193 "Compute a new unique client nonce value."
194 (base64-encode-string
195 (apply 'format "%016x%04x%04x%05x%05x" (random) (current-time)) t))
196
197(defun url-digest-auth-nonce-count (nonce)
198 "The number requests sent to server with the given NONCE.
199This count includes the request we're preparing here.
200
201Currently, this is not implemented and will always return 1.
202
203Value returned is in string format with leading zeroes, such as
204\"00000001\"."
205 (format "%08x" 1))
206
207(defun url-digest-auth-name-value-string (pairs)
208 "Concatenate name-value pairs in association list PAIRS.
209
210Output is formatted as \"name1=\\\"value1\\\", name2=\\\"value2\\\", ...\""
211 (mapconcat (lambda (pair)
212 (format "%s=\"%s\""
213 (symbol-name (car pair))
214 (cdr pair)))
215 pairs ", "))
216
217(defun url-digest-auth-source-creds (url)
218 "Find credentials for URL object from the Emacs auth-source.
219Return value is a plist that has `:user' and `:secret' properties
220if credentials were found. Otherwise nil."
221 (let ((server (url-digest-auth-server-id url))
222 (type (url-type url)))
223 (list :user (url-do-auth-source-search server type :user)
224 :secret (url-do-auth-source-search server type :secret))))
225
226(defun url-digest-prompt-creds (url realm &optional creds)
227 "Prompt credentials for URL and REALM, defaulting to CREDS.
228CREDS is a plist that may have properties `:user' and `:secret'."
229 ;; Set explicitly in case creds were nil. This makes the second
230 ;; plist-put modify the same plist.
231 (setq creds
232 (plist-put creds :user
233 (read-string (url-auth-user-prompt url realm)
234 (or (plist-get creds :user)
235 (user-real-login-name)))))
236 (plist-put creds :secret
237 (read-passwd "Password: " nil (plist-get creds :secret))))
238
239(defun url-digest-auth-directory-id-assoc (dirkey keylist)
240 "Find the best match for DIRKEY in key alist KEYLIST.
241
242The string DIRKEY should be obtained using
243`url-digest-auth-directory-id'. The key list to search through
244is the alist KEYLIST where car of each element may match DIRKEY.
245If DIRKEY represents a realm, the list is searched only for an
246exact match. For directory names, an ancestor is sufficient for
247a match."
248 (or
249 ;; Check exact match first.
250 (assoc dirkey keylist)
251 ;; No exact match found. Continue to look for partial match if
252 ;; dirkey is not a realm.
253 (and (string-match "/" dirkey)
254 (let (match)
255 (while (and (null match) keylist)
256 (if (or
257 ;; Any realm candidate matches. Why?
258 (not (string-match "/" (caar keylist)))
259 ;; Parent directory matches.
260 (string-prefix-p (caar keylist) dirkey))
261 (setq match (car keylist))
262 (setq keylist (cdr keylist))))
263 match))))
264
265(defun url-digest-cached-key (url realm)
266 "Find best match for URL and REALM from `url-digest-auth-storage'.
267The return value is a list consisting of a realm (or a directory)
268a user name, and hashed authentication tokens HA1 and HA2.
269Modifying the contents of the returned list will modify the cache
270variable `url-digest-auth-storage' itself."
271 (url-digest-auth-directory-id-assoc
272 (url-digest-auth-directory-id url realm)
273 (cdr (assoc (url-digest-auth-server-id url) url-digest-auth-storage))))
274
275(defun url-digest-cache-key (key url)
276 "Add key to `url-digest-auth-storage'.
277KEY has the same format as returned by `url-digest-cached-key'.
278The key is added to cache hierarchy under server id, deduced from
279URL."
280 (let ((serverid (url-digest-auth-server-id url)))
281 (push (list serverid key) url-digest-auth-storage)))
282
146(defun url-digest-auth-create-key (username password realm method uri) 283(defun url-digest-auth-create-key (username password realm method uri)
147 "Create a key for digest authentication method" 284 "Create a key for digest authentication method.
148 (let* ((info (if (stringp uri) 285The USERNAME and PASSWORD are the credentials for REALM and are
149 (url-generic-parse-url uri) 286used in making a hashed value named HA1. The HTTP METHOD and URI
150 uri)) 287makes a second hashed value HA2. These hashes are used in making
151 (a1 (md5 (concat username ":" realm ":" password))) 288the authentication key that can be stored without saving the
152 (a2 (md5 (concat method ":" (url-filename info))))) 289password in plain text. The return value is a list (HA1 HA2).
153 (list a1 a2))) 290
154 291For backward compatibility, URI is allowed to be a URL cl-struct
155(defun url-digest-auth (url &optional prompt overwrite realm args) 292object."
156 "Get the username/password for the specified URL. 293 (and username password realm
157If optional argument PROMPT is non-nil, ask for the username/password 294 (list (url-digest-auth-make-ha1 username realm password)
158to use for the URL and its descendants. If optional third argument 295 (url-digest-auth-make-ha2 method (cond ((stringp uri) uri)
159OVERWRITE is non-nil, overwrite the old username/password pair if it 296 (t (url-filename uri)))))))
160is found in the assoc list. If REALM is specified, use that as the realm 297
161instead of hostname:portnum." 298(defun url-digest-auth-build-response (key url realm attrs)
162 (if args 299 "Compute authorization string for the given challenge using KEY.
163 (let* ((href (if (stringp url) 300
164 (url-generic-parse-url url) 301The string looks like 'Digest username=\"John\", realm=\"The
165 url)) 302Realm\", ...'
166 (server (url-host href)) 303
167 (type (url-type href)) 304Part of the challenge is already solved in a pre-computed KEY
168 (port (url-port href)) 305which is list of a realm (or a directory), user name, and hash
169 (file (url-filename href)) 306tokens HA1 and HA2.
170 (enable-recursive-minibuffers t) 307
171 user pass byserv retval data) 308Some fields are filled as is from the given URL, REALM, and
172 (setq file (cond 309using the contents of alist ATTRS.
173 (realm realm) 310
174 ((string-match "/$" file) file) 311ATTRS is expected to contain at least the server's \"nonce\"
175 (t (url-file-directory file))) 312value. It also might contain the optional \"opaque\" value.
176 server (format "%s:%d" server port) 313Newer implementations conforming to RFC 2617 should also contain
177 byserv (cdr-safe (assoc server url-digest-auth-storage))) 314qop (Quality Of Protection) and related attributes.
178 (cond 315
179 ((and prompt (not byserv)) 316Restrictions on Quality of Protection scheme: The qop value
180 (setq user (or 317\"auth-int\" or algorithm any other than \"MD5\" are not
181 (url-do-auth-source-search server type :user) 318implemented."
182 (read-string (url-auth-user-prompt url realm) 319
183 (user-real-login-name))) 320 (when key
184 pass (or 321 (let ((user (nth 1 key))
185 (url-do-auth-source-search server type :secret) 322 (ha1 (nth 2 key))
186 (read-passwd "Password: ")) 323 (ha2 (nth 3 key))
187 url-digest-auth-storage 324 (digest-uri (url-filename url))
188 (cons (list server 325 (qop (cdr-safe (assoc "qop" attrs)))
189 (cons file 326 (nonce (cdr-safe (assoc "nonce" attrs)))
190 (setq retval 327 (opaque (cdr-safe (assoc "opaque" attrs))))
191 (cons user 328
192 (url-digest-auth-create-key 329 (concat
193 user pass realm 330 "Digest "
194 (or url-request-method "GET") 331 (url-digest-auth-name-value-string
195 url))))) 332 (append (list (cons 'username user)
196 url-digest-auth-storage))) 333 (cons 'realm realm)
197 (byserv 334 (cons 'nonce nonce)
198 (setq retval (cdr-safe (assoc file byserv))) 335 (cons 'uri digest-uri))
199 (if (and (not retval) ; no exact match, check directories 336
200 (string-match "/" file)) ; not looking for a realm 337 (cond
201 (while (and byserv (not retval)) 338 ((null qop)
202 (setq data (car (car byserv))) 339 (list (cons 'response (url-digest-auth-make-request-digest
203 (if (or (not (string-match "/" data)) 340 ha1 ha2 nonce))))
204 (and 341 ((string= qop "auth")
205 (>= (length file) (length data)) 342 (let ((nc (url-digest-auth-nonce-count nonce))
206 (string= data (substring file 0 (length data))))) 343 (cnonce (url-digest-auth-make-cnonce)))
207 (setq retval (cdr (car byserv)))) 344 (list (cons 'qop qop)
208 (setq byserv (cdr byserv)))) 345 (cons 'nc nc)
209 (if overwrite 346 (cons 'cnonce cnonce)
210 (if (and (not retval) prompt) 347 (cons 'response
211 (setq user (or 348 (url-digest-auth-make-request-digest-qop
212 (url-do-auth-source-search server type :user) 349 qop ha1 ha2 nonce nc cnonce)))))
213 (read-string (url-auth-user-prompt url realm) 350 (t (message "Quality of protection \"%s\" is not implemented." qop)
214 (user-real-login-name))) 351 nil))
215 pass (or 352
216 (url-do-auth-source-search server type :secret) 353
217 (read-passwd "Password: ")) 354 (if opaque (list (cons 'opaque opaque)))))))))
218 retval (setq retval 355
219 (cons user 356(defun url-digest-find-creds (url prompt &optional realm)
220 (url-digest-auth-create-key 357 "Find or ask credentials for URL.
221 user pass realm 358
222 (or url-request-method "GET") 359Primary method for finding credentials is from Emacs auth-source.
223 url))) 360If password isn't found, and PROMPT is non-nil, query credentials
224 byserv (assoc server url-digest-auth-storage)) 361via minibuffer. Optional REALM may be used when prompting as a
225 (setcdr byserv 362hint to the user.
226 (cons (cons file retval) (cdr byserv)))))) 363
227 (t (setq retval nil))) 364Return value is nil in case either user name or password wasn't
228 (if retval 365found. Otherwise, it's a plist containing `:user' and `:secret'.
229 (if (cdr-safe (assoc "opaque" args)) 366Additional `:source' property denotes the origin of the
230 (let ((nonce (or (cdr-safe (assoc "nonce" args)) "nonegiven")) 367credentials and its value can be either symbol `authsource' or
231 (opaque (cdr-safe (assoc "opaque" args)))) 368`interactive'."
232 (format 369 (let ((creds (url-digest-auth-source-creds url)))
233 (concat "Digest username=\"%s\", realm=\"%s\"," 370
234 "nonce=\"%s\", uri=\"%s\"," 371 ;; If credentials weren't found and prompting is allowed, prompt
235 "response=\"%s\", opaque=\"%s\"") 372 ;; the user.
236 (nth 0 retval) realm nonce (url-filename href) 373 (if (and prompt
237 (md5 (concat (nth 1 retval) ":" nonce ":" 374 (or (null creds)
238 (nth 2 retval))) opaque)) 375 (null (plist-get creds :secret))))
239 (let ((nonce (or (cdr-safe (assoc "nonce" args)) "nonegiven"))) 376 (progn
240 (format 377 (setq creds (url-digest-prompt-creds url realm creds))
241 (concat "Digest username=\"%s\", realm=\"%s\"," 378 (plist-put creds :source 'interactive))
242 "nonce=\"%s\", uri=\"%s\"," 379 (plist-put creds :source 'authsource))
243 "response=\"%s\"") 380
244 (nth 0 retval) realm nonce (url-filename href) 381 (and (plist-get creds :user)
245 (md5 (concat (nth 1 retval) ":" nonce ":" 382 (plist-get creds :secret)
246 (nth 2 retval)))))))))) 383 creds)))
384
385(defun url-digest-find-new-key (url realm prompt)
386 "Find credentials and create a new authorization key for given URL and REALM.
387
388Return value is the new key, or nil if credentials weren't found.
389\"New\" in this context means a key that's not yet found in cache
390variable `url-digest-auth-storage'. You may use `url-digest-cache-key'
391to put it there.
392
393This function uses `url-digest-find-creds' to find the
394credentials. It first looks in auth-source. If not found, and
395PROMPT is non-nil, user is asked for credentials interactively
396via minibuffer."
397 (let (creds)
398 (unwind-protect
399 (if (setq creds (url-digest-find-creds url prompt realm))
400 (cons (url-digest-auth-directory-id url realm)
401 (cons (plist-get creds :user)
402 (url-digest-auth-create-key
403 (plist-get creds :user)
404 (plist-get creds :secret)
405 realm
406 (or url-request-method "GET")
407 (url-filename url)))))
408 (if (and creds
409 ;; Don't clear secret for `authsource' since it will
410 ;; corrupt any future fetches for it.
411 (not (eq (plist-get creds :source) 'authsource)))
412 (clear-string (plist-get creds :secret))))))
413
414(defun url-digest-auth (url &optional prompt overwrite realm attrs)
415 "Get the HTTP Digest response string for the specified URL.
416
417If optional argument PROMPT is non-nil, ask for the username and
418password to use for the URL and its descendants but only if one
419cannot be found from cache. Look also in Emacs auth-source.
420
421If optional third argument OVERWRITE is non-nil, overwrite the
422old credentials, if they're found in cache, with new ones from
423user prompt or from Emacs auth-source.
424
425If REALM is specified, use that instead of the URL descendant
426method to match cached credentials.
427
428Alist ATTRS contains additional attributes for the authentication
429challenge such as nonce and opaque."
430 (if attrs
431 (let* ((href (if (stringp url) (url-generic-parse-url url) url))
432 (enable-recursive-minibuffers t)
433 (key (url-digest-cached-key href realm)))
434
435 (if (or (null key) overwrite)
436 (let ((newkey (url-digest-find-new-key href realm (cond
437 (key nil)
438 (t prompt)))))
439 (if (and newkey key overwrite)
440 (setcdr key (cdr newkey))
441 (if (and newkey (null key))
442 (url-digest-cache-key (setq key newkey) href)))))
443
444 (if key
445 (url-digest-auth-build-response key href realm attrs)))))
247 446
248(defvar url-registered-auth-schemes nil 447(defvar url-registered-auth-schemes nil
249 "A list of the registered authorization schemes and various and sundry 448 "A list of the registered authorization schemes and various and sundry
diff --git a/test/lisp/url/url-auth-tests.el b/test/lisp/url/url-auth-tests.el
index 11e5a479720..30636db083c 100644
--- a/test/lisp/url/url-auth-tests.el
+++ b/test/lisp/url/url-auth-tests.el
@@ -77,6 +77,49 @@ server's WWW-Authenticate header field.")
77 :expected-ha2 "b44272ea65ee4af7fb26c5dba58f6863" 77 :expected-ha2 "b44272ea65ee4af7fb26c5dba58f6863"
78 :expected-response "0d84884d967e04440efc77e9e2b5b561"))) 78 :expected-response "0d84884d967e04440efc77e9e2b5b561")))
79 79
80(ert-deftest url-auth-test-colonjoin ()
81 "Check joining strings with `:'."
82 (should (string= (url-digest-auth-colonjoin) ""))
83 (should (string= (url-digest-auth-colonjoin nil) ""))
84 (should (string= (url-digest-auth-colonjoin nil nil nil) "::"))
85 (should (string= (url-digest-auth-colonjoin "") ""))
86 (should (string= (url-digest-auth-colonjoin "" "") ":"))
87 (should (string= (url-digest-auth-colonjoin "one") "one"))
88 (should (string= (url-digest-auth-colonjoin "one" "two" "three") "one:two:three")))
89
90(ert-deftest url-auth-test-digest-ha1 ()
91 "Check HA1 computation."
92 (dolist (row url-auth-test-challenges)
93 (should (string= (url-digest-auth-make-ha1 (plist-get row :username)
94 (plist-get row :realm)
95 (plist-get row :password))
96 (plist-get row :expected-ha1)
97 ))))
98
99(ert-deftest url-auth-test-digest-ha2 ()
100 "Check HA2 computation."
101 (dolist (row url-auth-test-challenges)
102 (should (string= (url-digest-auth-make-ha2 (plist-get row :method)
103 (plist-get row :uri))
104 (plist-get row :expected-ha2)))))
105
106(ert-deftest url-auth-test-digest-request-digest ()
107 "Check digest response value."
108 (dolist (row url-auth-test-challenges)
109 (should (string= (plist-get row :expected-response)
110 (if (plist-member row :qop)
111 (url-digest-auth-make-request-digest-qop
112 (plist-get row :qop)
113 (plist-get row :expected-ha1)
114 (plist-get row :expected-ha2)
115 (plist-get row :nonce)
116 (plist-get row :nc)
117 (plist-get row :cnonce))
118 (url-digest-auth-make-request-digest
119 (plist-get row :expected-ha1)
120 (plist-get row :expected-ha2)
121 (plist-get row :nonce)))))))
122
80(ert-deftest url-auth-test-digest-create-key () 123(ert-deftest url-auth-test-digest-create-key ()
81 "Check user credentials in their hashed form." 124 "Check user credentials in their hashed form."
82 (dolist (challenge url-auth-test-challenges) 125 (dolist (challenge url-auth-test-challenges)
@@ -223,14 +266,12 @@ test and cannot be passed by arguments to `url-digest-auth'."
223 (progn 266 (progn
224 ;; We don't know these, just check that they exists. 267 ;; We don't know these, just check that they exists.
225 (should (string-match-p ".*response=\".*?\".*" auth)) 268 (should (string-match-p ".*response=\".*?\".*" auth))
226 ;; url-digest-auth doesn't return these AFAICS. 269 (should (string-match-p ".*nc=\".*?\".*" auth))
227;;; (should (string-match-p ".*nc=\".*?\".*" auth)) 270 (should (string-match-p ".*cnonce=\".*?\".*" auth)))
228;;; (should (string-match-p ".*cnonce=\".*?\".*" auth))
229 )
230 (should (string-match ".*response=\"\\(.*?\\)\".*" auth)) 271 (should (string-match ".*response=\"\\(.*?\\)\".*" auth))
231 (should (string= (match-string 1 auth) 272 (should (string= (match-string 1 auth)
232 (plist-get challenge :expected-response)))) 273 (plist-get challenge :expected-response))))
233 ))) 274 )))
234 275
235(ert-deftest url-auth-test-digest-auth-opaque () 276(ert-deftest url-auth-test-digest-auth-opaque ()
236 "Check that `opaque' value is added to result when presented by 277 "Check that `opaque' value is added to result when presented by