1 ;;; mml-smime.el --- S/MIME support for MML
3 ;; Copyright (C) 2000-2011 Free Software Foundation, Inc.
5 ;; Author: Simon Josefsson <simon@josefsson.org>
6 ;; Keywords: Gnus, MIME, S/MIME, MML
8 ;; This file is part of GNU Emacs.
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
27 ;; For Emacs <22.2 and XEmacs.
29 (unless (fboundp 'declare-function
) (defmacro declare-function
(&rest r
))))
31 (eval-when-compile (require 'cl
))
36 (autoload 'message-narrow-to-headers
"message")
37 (autoload 'message-fetch-field
"message")
39 (defcustom mml-smime-use
(if (featurep 'epg
) 'epg
'openssl
)
40 "Whether to use OpenSSL or EPG to decrypt S/MIME messages.
41 Defaults to EPG if it's loaded."
43 :type
'(choice (const :tag
"EPG" epg
)
44 (const :tag
"OpenSSL" openssl
)))
46 (defvar mml-smime-function-alist
47 '((openssl mml-smime-openssl-sign
48 mml-smime-openssl-encrypt
49 mml-smime-openssl-sign-query
50 mml-smime-openssl-encrypt-query
51 mml-smime-openssl-verify
52 mml-smime-openssl-verify-test
)
53 (epg mml-smime-epg-sign
58 mml-smime-epg-verify-test
)))
60 (defcustom mml-smime-cache-passphrase mml-secure-cache-passphrase
61 "If t, cache passphrase."
65 (defcustom mml-smime-passphrase-cache-expiry mml-secure-passphrase-cache-expiry
66 "How many seconds the passphrase is cached.
67 Whether the passphrase is cached at all is controlled by
68 `mml-smime-cache-passphrase'."
72 (defcustom mml-smime-signers nil
73 "A list of your own key ID which will be used to sign a message."
75 :type
'(repeat (string :tag
"Key ID")))
77 (defun mml-smime-sign (cont)
78 (let ((func (nth 1 (assq mml-smime-use mml-smime-function-alist
))))
81 (error "Cannot find sign function"))))
83 (defun mml-smime-encrypt (cont)
84 (let ((func (nth 2 (assq mml-smime-use mml-smime-function-alist
))))
87 (error "Cannot find encrypt function"))))
89 (defun mml-smime-sign-query ()
90 (let ((func (nth 3 (assq mml-smime-use mml-smime-function-alist
))))
94 (defun mml-smime-encrypt-query ()
95 (let ((func (nth 4 (assq mml-smime-use mml-smime-function-alist
))))
99 (defun mml-smime-verify (handle ctl
)
100 (let ((func (nth 5 (assq mml-smime-use mml-smime-function-alist
))))
102 (funcall func handle ctl
)
105 (defun mml-smime-verify-test (handle ctl
)
106 (let ((func (nth 6 (assq mml-smime-use mml-smime-function-alist
))))
108 (funcall func handle ctl
))))
110 (defun mml-smime-openssl-sign (cont)
111 (when (null smime-keys
)
112 (customize-variable 'smime-keys
)
113 (error "No S/MIME keys configured, use customize to add your key"))
114 (smime-sign-buffer (cdr (assq 'keyfile cont
)))
115 (goto-char (point-min))
116 (while (search-forward "\r\n" nil t
)
117 (replace-match "\n" t t
))
118 (goto-char (point-max)))
120 (defun mml-smime-openssl-encrypt (cont)
121 (let (certnames certfiles tmp file tmpfiles
)
122 ;; xxx tmp files are always an security issue
123 (while (setq tmp
(pop cont
))
124 (if (and (consp tmp
) (eq (car tmp
) 'certfile
))
125 (push (cdr tmp
) certnames
)))
126 (while (setq tmp
(pop certnames
))
127 (if (not (and (not (file-exists-p tmp
))
130 (setq file
(mm-make-temp-file (expand-file-name "mml."
132 (with-current-buffer tmp
133 (write-region (point-min) (point-max) file
))
134 (push file certfiles
)
135 (push file tmpfiles
)))
136 (if (smime-encrypt-buffer certfiles
)
138 (while (setq tmp
(pop tmpfiles
))
141 (while (setq tmp
(pop tmpfiles
))
144 (goto-char (point-max)))
146 (defvar gnus-extract-address-components
)
148 (defun mml-smime-openssl-sign-query ()
149 ;; query information (what certificate) from user when MML tag is
150 ;; added, for use later by the signing process
151 (when (null smime-keys
)
152 (customize-variable 'smime-keys
)
153 (error "No S/MIME keys configured, use customize to add your key"))
155 (if (= (length smime-keys
) 1)
157 (or (let ((from (cadr (funcall (if (boundp
158 'gnus-extract-address-components
)
159 gnus-extract-address-components
160 'mail-extract-address-components
)
163 (message-narrow-to-headers)
164 (message-fetch-field "from")))
166 (and from
(smime-get-key-by-email from
)))
167 (smime-get-key-by-email
168 (gnus-completing-read "Sign this part with what signature"
169 (mapcar 'car smime-keys
) nil nil nil
170 (and (listp (car-safe smime-keys
))
171 (caar smime-keys
))))))))
173 (defun mml-smime-get-file-cert ()
175 (list 'certfile
(read-file-name
176 "File with recipient's S/MIME certificate: "
177 smime-certificate-directory nil t
""))))
179 (defun mml-smime-get-dns-cert ()
180 ;; todo: deal with comma separated multiple recipients
181 (let (result who bad cert
)
184 (setq who
(read-from-minibuffer
185 (format "%sLookup certificate for: " (or bad
""))
186 (cadr (funcall (if (boundp
187 'gnus-extract-address-components
)
188 gnus-extract-address-components
189 'mail-extract-address-components
)
192 (message-narrow-to-headers)
193 (message-fetch-field "to")))
195 (if (setq cert
(smime-cert-by-dns who
))
196 (setq result
(list 'certfile
(buffer-name cert
)))
197 (setq bad
(format "`%s' not found. " who
))))
201 (defun mml-smime-get-ldap-cert ()
202 ;; todo: deal with comma separated multiple recipients
203 (let (result who bad cert
)
206 (setq who
(read-from-minibuffer
207 (format "%sLookup certificate for: " (or bad
""))
208 (cadr (funcall gnus-extract-address-components
211 (message-narrow-to-headers)
212 (message-fetch-field "to")))
214 (if (setq cert
(smime-cert-by-ldap who
))
215 (setq result
(list 'certfile
(buffer-name cert
)))
216 (setq bad
(format "`%s' not found. " who
))))
220 (autoload 'gnus-completing-read
"gnus-util")
222 (defun mml-smime-openssl-encrypt-query ()
223 ;; todo: try dns/ldap automatically first, before prompting user
226 (ecase (read (gnus-completing-read
227 "Fetch certificate from"
228 '("dns" "ldap" "file") t nil nil
230 (dns (setq certs
(append certs
231 (mml-smime-get-dns-cert))))
232 (ldap (setq certs
(append certs
233 (mml-smime-get-ldap-cert))))
234 (file (setq certs
(append certs
235 (mml-smime-get-file-cert)))))
236 (setq done
(not (y-or-n-p "Add more recipients? "))))
239 (defun mml-smime-openssl-verify (handle ctl
)
241 (insert-buffer-substring (mm-handle-multipart-original-buffer ctl
))
242 (goto-char (point-min))
243 (insert (format "Content-Type: %s; " (mm-handle-media-type ctl
)))
244 (insert (format "protocol=\"%s\"; "
245 (mm-handle-multipart-ctl-parameter ctl
'protocol
)))
246 (insert (format "micalg=\"%s\"; "
247 (mm-handle-multipart-ctl-parameter ctl
'micalg
)))
248 (insert (format "boundary=\"%s\"\n\n"
249 (mm-handle-multipart-ctl-parameter ctl
'boundary
)))
250 (when (get-buffer smime-details-buffer
)
251 (kill-buffer smime-details-buffer
))
252 (let ((buf (current-buffer))
253 (good-signature (smime-noverify-buffer))
254 (good-certificate (and (or smime-CA-file smime-CA-directory
)
255 (smime-verify-buffer)))
256 addresses openssl-output
)
257 (setq openssl-output
(with-current-buffer smime-details-buffer
259 (if (not good-signature
)
261 ;; we couldn't verify message, fail with openssl output as message
262 (mm-set-handle-multipart-parameter
263 mm-security-handle
'gnus-info
"Failed")
264 (mm-set-handle-multipart-parameter
265 mm-security-handle
'gnus-details
266 (concat "OpenSSL failed to verify message integrity:\n"
267 "-------------------------------------------\n"
269 ;; verify mail addresses in mail against those in certificate
270 (when (and (smime-pkcs7-region (point-min) (point-max))
271 (smime-pkcs7-certificates-region (point-min) (point-max)))
273 (insert-buffer-substring buf
)
274 (goto-char (point-min))
275 (while (re-search-forward "-----END CERTIFICATE-----" nil t
)
276 (when (smime-pkcs7-email-region (point-min) (point))
277 (setq addresses
(append (smime-buffer-as-string-region
278 (point-min) (point)) addresses
)))
279 (delete-region (point-min) (point)))
280 (setq addresses
(mapcar 'downcase addresses
))))
281 (if (not (member (downcase (or (mm-handle-multipart-from ctl
) "")) addresses
))
282 (mm-set-handle-multipart-parameter
283 mm-security-handle
'gnus-info
"Sender address forged")
285 (mm-set-handle-multipart-parameter
286 mm-security-handle
'gnus-info
"Ok (sender authenticated)")
287 (mm-set-handle-multipart-parameter
288 mm-security-handle
'gnus-info
"Ok (sender not trusted)")))
289 (mm-set-handle-multipart-parameter
290 mm-security-handle
'gnus-details
291 (concat "Sender claimed to be: " (mm-handle-multipart-from ctl
) "\n"
293 (concat "Addresses in certificate: "
294 (mapconcat 'identity addresses
", "))
295 "No addresses found in certificate. (Requires OpenSSL 0.9.6 or later.)")
298 "---------------\n" openssl-output
"\n"
299 "Certificate(s) inside S/MIME signature:\n"
300 "---------------------------------------\n"
301 (buffer-string) "\n")))))
304 (defun mml-smime-openssl-verify-test (handle ctl
)
305 smime-openssl-program
)
307 (defvar epg-user-id-alist
)
308 (defvar epg-digest-algorithm-alist
)
309 (defvar inhibit-redisplay
)
310 (defvar password-cache-expiry
)
313 (autoload 'epg-make-context
"epg")
314 (autoload 'epg-context-set-armor
"epg")
315 (autoload 'epg-context-set-signers
"epg")
316 (autoload 'epg-context-result-for
"epg")
317 (autoload 'epg-new-signature-digest-algorithm
"epg")
318 (autoload 'epg-verify-result-to-string
"epg")
319 (autoload 'epg-list-keys
"epg")
320 (autoload 'epg-decrypt-string
"epg")
321 (autoload 'epg-verify-string
"epg")
322 (autoload 'epg-sign-string
"epg")
323 (autoload 'epg-encrypt-string
"epg")
324 (autoload 'epg-passphrase-callback-function
"epg")
325 (autoload 'epg-context-set-passphrase-callback
"epg")
326 (autoload 'epg-configuration
"epg-config")
327 (autoload 'epg-expand-group
"epg-config")
328 (autoload 'epa-select-keys
"epa"))
330 (defvar mml-smime-epg-secret-key-id-list nil
)
332 (defun mml-smime-epg-passphrase-callback (context key-id ignore
)
334 (epg-passphrase-callback-function context key-id nil
)
339 "Passphrase for PIN: "
340 (if (setq entry
(assoc key-id epg-user-id-alist
))
341 (format "Passphrase for %s %s: " key-id
(cdr entry
))
342 (format "Passphrase for %s: " key-id
)))
347 (let ((password-cache-expiry mml-smime-passphrase-cache-expiry
))
348 (password-cache-add key-id passphrase
))
349 (setq mml-smime-epg-secret-key-id-list
350 (cons key-id mml-smime-epg-secret-key-id-list
))
351 (copy-sequence passphrase
)))))
353 (declare-function epg-key-sub-key-list
"ext:epg" (key))
354 (declare-function epg-sub-key-capability
"ext:epg" (sub-key))
355 (declare-function epg-sub-key-validity
"ext:epg" (sub-key))
357 (defun mml-smime-epg-find-usable-key (keys usage
)
360 (let ((pointer (epg-key-sub-key-list (car keys
))))
362 (if (and (memq usage
(epg-sub-key-capability (car pointer
)))
363 (not (memq (epg-sub-key-validity (car pointer
))
364 '(revoked expired
))))
365 (throw 'found
(car keys
)))
366 (setq pointer
(cdr pointer
))))
367 (setq keys
(cdr keys
)))))
369 (autoload 'mml-compute-boundary
"mml")
371 ;; We require mm-decode, which requires mm-bodies, which autoloads
372 ;; message-options-get (!).
373 (declare-function message-options-set
"message" (symbol value
))
375 (defun mml-smime-epg-sign (cont)
376 (let* ((inhibit-redisplay t
)
377 (context (epg-make-context 'CMS
))
378 (boundary (mml-compute-boundary cont
))
381 (or (message-options-get 'mml-smime-epg-signers
)
383 'mml-smime-epg-signers
384 (if (eq mm-sign-option
'guided
)
385 (epa-select-keys context
"\
386 Select keys for signing.
387 If no one is selected, default secret key is used. "
389 (if mml-smime-signers
392 (setq signer-key
(mml-smime-epg-find-usable-key
393 (epg-list-keys context signer t
)
395 (unless (or signer-key
397 (format "No secret key for %s; skip it? "
399 (error "No secret key for %s" signer
))
401 mml-smime-signers
))))))
403 (epg-context-set-signers context signers
)
404 (if mml-smime-cache-passphrase
405 (epg-context-set-passphrase-callback
407 #'mml-smime-epg-passphrase-callback
))
408 (condition-case error
409 (setq signature
(epg-sign-string context
410 (mm-replace-in-string (buffer-string)
413 mml-smime-epg-secret-key-id-list nil
)
415 (while mml-smime-epg-secret-key-id-list
416 (password-cache-remove (car mml-smime-epg-secret-key-id-list
))
417 (setq mml-smime-epg-secret-key-id-list
418 (cdr mml-smime-epg-secret-key-id-list
)))
419 (signal (car error
) (cdr error
))))
420 (if (epg-context-result-for context
'sign
)
421 (setq micalg
(epg-new-signature-digest-algorithm
422 (car (epg-context-result-for context
'sign
)))))
423 (goto-char (point-min))
424 (insert (format "Content-Type: multipart/signed; boundary=\"%s\";\n"
427 (insert (format "\tmicalg=%s; "
430 epg-digest-algorithm-alist
))))))
431 (insert "protocol=\"application/pkcs7-signature\"\n")
432 (insert (format "\n--%s\n" boundary
))
433 (goto-char (point-max))
434 (insert (format "\n--%s\n" boundary
))
435 (insert "Content-Type: application/pkcs7-signature; name=smime.p7s
436 Content-Transfer-Encoding: base64
437 Content-Disposition: attachment; filename=smime.p7s
440 (insert (base64-encode-string signature
) "\n")
441 (goto-char (point-max))
442 (insert (format "--%s--\n" boundary
))
443 (goto-char (point-max))))
445 (defun mml-smime-epg-encrypt (cont)
446 (let ((inhibit-redisplay t
)
447 (context (epg-make-context 'CMS
))
448 (config (epg-configuration))
449 (recipients (message-options-get 'mml-smime-epg-recipients
))
451 (boundary (mml-compute-boundary cont
))
458 (or (epg-expand-group config recipient
)
461 (or (message-options-get 'message-recipients
)
462 (message-options-set 'message-recipients
463 (read-string "Recipients: ")))
464 "[ \f\t\n\r\v,]+"))))
465 (if (eq mm-encrypt-option
'guided
)
467 (epa-select-keys context
"\
468 Select recipients for encryption.
469 If no one is selected, symmetric encryption will be performed. "
474 (setq recipient-key
(mml-smime-epg-find-usable-key
475 (epg-list-keys context recipient
)
477 (unless (or recipient-key
479 (format "No public key for %s; skip it? "
481 (error "No public key for %s" recipient
))
485 (error "No recipient specified")))
486 (message-options-set 'mml-smime-epg-recipients recipients
))
487 (if mml-smime-cache-passphrase
488 (epg-context-set-passphrase-callback
490 #'mml-smime-epg-passphrase-callback
))
491 (condition-case error
493 (epg-encrypt-string context
(buffer-string) recipients
)
494 mml-smime-epg-secret-key-id-list nil
)
496 (while mml-smime-epg-secret-key-id-list
497 (password-cache-remove (car mml-smime-epg-secret-key-id-list
))
498 (setq mml-smime-epg-secret-key-id-list
499 (cdr mml-smime-epg-secret-key-id-list
)))
500 (signal (car error
) (cdr error
))))
501 (delete-region (point-min) (point-max))
502 (goto-char (point-min))
504 Content-Type: application/pkcs7-mime;
505 smime-type=enveloped-data;
507 Content-Transfer-Encoding: base64
508 Content-Disposition: attachment; filename=smime.p7m
511 (insert (base64-encode-string cipher
))
512 (goto-char (point-max))))
514 (defun mml-smime-epg-verify (handle ctl
)
516 (let ((inhibit-redisplay t
)
517 context plain signature-file part signature
)
518 (when (or (null (setq part
(mm-find-raw-part-by-type
519 ctl
(or (mm-handle-multipart-ctl-parameter
521 "application/pkcs7-signature")
523 (null (setq signature
(or (mm-find-part-by-type
525 "application/pkcs7-signature"
527 (mm-find-part-by-type
529 "application/x-pkcs7-signature"
531 (mm-set-handle-multipart-parameter
532 mm-security-handle
'gnus-info
"Corrupted")
533 (throw 'error handle
))
534 (setq part
(mm-replace-in-string part
"\n" "\r\n")
535 context
(epg-make-context 'CMS
))
536 (condition-case error
537 (setq plain
(epg-verify-string context
(mm-get-part signature
) part
))
539 (mm-set-handle-multipart-parameter
540 mm-security-handle
'gnus-info
"Failed")
541 (if (eq (car error
) 'quit
)
542 (mm-set-handle-multipart-parameter
543 mm-security-handle
'gnus-details
"Quit.")
544 (mm-set-handle-multipart-parameter
545 mm-security-handle
'gnus-details
(format "%S" error
)))
546 (throw 'error handle
)))
547 (mm-set-handle-multipart-parameter
548 mm-security-handle
'gnus-info
549 (epg-verify-result-to-string (epg-context-result-for context
'verify
)))
552 (defun mml-smime-epg-verify-test (handle ctl
)
557 ;;; mml-smime.el ends here