Merged Alex Schroeder's header-filling patch. Thanks!!
authorTim Vaughan <plugd@thelambdalab.xyz>
Sat, 19 Sep 2020 10:08:06 +0000 (12:08 +0200)
committerTim Vaughan <plugd@thelambdalab.xyz>
Sat, 19 Sep 2020 10:08:25 +0000 (12:08 +0200)
ISSUES.org
elpher.el
elpher.texi

index 6a9d666..1c8088f 100644 (file)
@@ -115,23 +115,27 @@ do whatever we like once we also have a special key map in place as well.
 I guess I need to look into what native widgets Emacs has for displaying
 collapsable hierarchies.
 
-** OPEN Implement Gemini support [88%]
+** OPEN Add history browsing
    
-Here is the checklist of features required before release:
-- [X] basic genimi transactions
-- [ ] gemini transactions requiring client certificates
-- [X] gemini input handling
-- [X] gemini map files (text/gemini)
-- [X] Support for plain text responses (text/*)
-- [X] Support for image responses (text/image)
-- [X] Support for mime-specified character encodeing
-- [X] Saving responses to disk
-- [X] Viewing raw responses
-  
-The last few will be made infinitely easier if we factor the
-gopher "getter" code differently.
+** OPEN Sanitize certificate names
+   :LOGBOOK:
+   - State "OPEN"       from              [2020-06-22 Mon 10:32]
+   :END:
+   
+Currently things will break in undefined ways if a name is specified
+that contains path separators and probably other characters that I
+haven't thought of.  This is dangerously unacceptable and needs to be
+fixed right away.
 
-** OPEN Add history browsing
+** OPEN Make installing existing certificates easier
+   :LOGBOOK:
+   - State "OPEN"       from "CLOSED"     [2020-06-22 Mon 10:34]
+   :END:
+
+It's naive to think that people don't have client certificates created
+outside of elpher. Thus we need some easy way to "install" these
+certificates, either by copying them or by referencing them in some
+way.
 
 * Closed Enhancements
   
@@ -219,3 +223,22 @@ occasionally pop up in termal windows.  Lets leave it for now.
    cursor.
 
    A small amount of feedback could help with this.
+
+** CLOSED Implement Gemini support [100%]
+   :LOGBOOK:
+   - State "CLOSED"     from "OPEN"       [2020-06-20 Sat 22:32]
+   :END:
+   
+Here is the checklist of features required before release:
+- [X] basic genimi transactions
+- [X] gemini transactions requiring client certificates
+- [X] gemini input handling
+- [X] gemini map files (text/gemini)
+- [X] Support for plain text responses (text/*)
+- [X] Support for image responses (text/image)
+- [X] Support for mime-specified character encodeing
+- [X] Saving responses to disk
+- [X] Viewing raw responses
+  
+The last few will be made infinitely easier if we factor the
+gopher "getter" code differently.
index e6f986b..be4a04a 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -4,10 +4,10 @@
 
 ;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
 ;; Created: 11 April 2019
-;; Version: 2.8.0
+;; Version: 2.10.1
 ;; Keywords: comm gopher
 ;; Homepage: http://thelambdalab.xyz/elpher
-;; Package-Requires: ((emacs "26.1"))
+;; Package-Requires: ((emacs "26.2"))
 
 ;; This file is not part of GNU Emacs.
 
@@ -71,7 +71,7 @@
 ;;; Global constants
 ;;
 
-(defconst elpher-version "2.8.0"
+(defconst elpher-version "2.10.0"
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
 ;;
 
 (defgroup elpher nil
-  "A gopher client."
+  "A gopher and gemini client."
   :group 'applications)
 
 ;; General appearance and customizations
@@ -134,6 +134,16 @@ The default behaviour is to use the ansi-color package to interpret these
 sequences."
   :type '(boolean))
 
+(defcustom elpher-certificate-directory
+  (file-name-as-directory (locate-user-emacs-file "elpher-certificates"))
+  "Specify the name of the directory where client certificates will be stored.
+These certificates may be used for establishing authenticated TLS connections."
+  :type '(directory))
+
+(defcustom elpher-openssl-command "openssl"
+  "The command used to launch openssl when generating TLS client certificates."
+  :type '(file))
+
 (defcustom elpher-gemini-TLS-cert-checks nil
   "If non-nil, verify gemini server TLS certs using the default security level.
 Otherwise, certificate verification is disabled.
@@ -199,7 +209,7 @@ some servers which do not support IPv6 can take a long time to time-out."
   "Face used for html type directory records.")
 
 (defface elpher-gemini
-  '((t :inherit font-lock-regexp-grouping-backslash))
+  '((t :inherit font-lock-constant-face))
   "Face used for Gemini type directory records.")
 
 (defface elpher-other-url
@@ -276,9 +286,23 @@ some servers which do not support IPv6 can take a long time to time-out."
               ;; Gemini defaults
               (if (equal (url-filename url) "")
                   (setf (url-filename url) "/"))))
-          url)
+          (elpher-remove-redundant-ports url))
       (set-match-data data))))
 
+(defun elpher-remove-redundant-ports (address)
+  "Remove redundant port specifiers from ADDRESS.
+Here 'redundant' means that the specified port matches the default
+for that protocol, eg 70 for gopher."
+  (if (and (not (elpher-address-special-p address))
+           (eq (url-portspec address) ; (url-port) is too slow!
+               (pcase (url-type address)
+                 ("gemini" 1965)
+                 ((or "gopher" "gophers") 70)
+                 ("finger" 79)
+                 (_ -1))))
+      (setf (url-portspec address) nil))
+  address)
+
 (defun elpher-make-gopher-address (type selector host port &optional tls)
   "Create an ADDRESS object using gopher directory record attributes.
 The basic attributes include: TYPE, SELECTOR, HOST and PORT.
@@ -561,6 +585,9 @@ ERROR can be either an error object or a string."
 (defvar elpher-use-tls nil
   "If non-nil, use TLS to communicate with gopher servers.")
 
+(defvar elpher-client-certificate nil
+  "If non-nil, contains client certificate details to use for TLS connections.")
+
 (defun elpher-process-cleanup ()
   "Immediately shut down any extant elpher process and timers."
   (let ((p (get-process "elpher-process")))
@@ -610,7 +637,9 @@ the host operating system and the local network capabilities."
                                                 (cons 'gnutls-x509pki
                                                       (gnutls-boot-parameters
                                                        :type 'gnutls-x509pki
-                                                       :hostname host)))))
+                                                       :hostname host
+                                                       :keylist
+                                                       (elpher-get-current-keylist address))))))
                (timer (run-at-time elpher-connection-timeout nil
                                    (lambda ()
                                      (elpher-process-cleanup)
@@ -635,7 +664,8 @@ the host operating system and the local network capabilities."
                                       (t
                                        (elpher-network-error address "Connection time-out.")))))))
           (setq elpher-network-timer timer)
-          (elpher-buffer-message (concat "Connecting to " host "..."))
+          (elpher-buffer-message (concat "Connecting to " host "..."
+                                         " (press 'u' to abort)"))
           (set-process-filter proc
                               (lambda (_proc string)
                                 (when timer
@@ -660,7 +690,8 @@ the host operating system and the local network capabilities."
                                       (cond
                                        ((string-prefix-p "open" event)    ; request URL
                                         (elpher-buffer-message
-                                         (concat "Connected to " host ". Receiving data..."))
+                                         (concat "Connected to " host ". Receiving data..."
+                                                 " (press 'u' to abort)"))
                                         (let ((inhibit-eol-conversion t))
                                           (process-send-string proc query-string)))
                                        ((string-prefix-p "deleted" event)) ; do nothing
@@ -686,6 +717,134 @@ the host operating system and the local network capabilities."
        (error "Error initiating connection to server")))))
 
 
+;;; Client-side TLS Certificate Management
+;;
+
+(defun elpher-generate-certificate (common-name key-file cert-file &optional temporary)
+  "Generate a key and a self-signed client TLS certificate using openssl.
+
+The Common Name field of the certificate is set to COMMON-NAME.  The
+arguments KEY-FILE and CERT-FILE should contain the absolute paths of
+the key and certificate files to write.
+
+If TEMPORARY is non-nil, the certificate will be given an exporation
+period of one day, and the key and certificate files will be deleted
+when the certificate is no longer needed for the current session.
+
+Otherwise, the certificate will be given a 100 year expiration period
+and the files will not be deleted.
+
+The function returns a list containing the current host name, the
+temporary flag, and the key and cert file names in the form required
+by `gnutls-boot-parameters`."
+  (let ((exp-key-file (expand-file-name key-file))
+        (exp-cert-file (expand-file-name cert-file)))
+    (condition-case nil
+        (progn
+          (call-process elpher-openssl-command nil nil nil
+                        "req" "-x509" "-newkey" "rsa:2048"
+                        "-days" (if temporary "1" "36500")
+                        "-nodes"
+                        "-subj" (concat "/CN=" common-name)
+                        "-keyout" exp-key-file
+                        "-out" exp-cert-file)
+          (list (elpher-address-host (elpher-page-address elpher-current-page))
+                temporary exp-key-file exp-cert-file))
+      (error
+       (message "Check that openssl is installed, or customize `elpher-openssl-command`.")
+       (error "Program 'openssl', required for certificate generation, not found")))))
+
+(defun elpher-generate-throwaway-certificate ()
+  "Generate and return details of a throwaway certificate.
+The key and certificate files will be deleted when they are no
+longer needed for this session."
+  (let* ((file-base (make-temp-name "elpher"))
+         (key-file (concat temporary-file-directory file-base ".key"))
+         (cert-file (concat temporary-file-directory file-base ".crt")))
+    (elpher-generate-certificate file-base key-file cert-file t)))
+
+(defun elpher-generate-persistent-certificate (file-base common-name)
+  "Generate and return details of a persistent certificate.
+The argument FILE-BASE is used as the base for the key and certificate
+files, while COMMON-NAME specifies the common name field of the
+certificate.
+
+The key and certificate files are written to in `elpher-certificate-directory'."
+  (let* ((key-file (concat elpher-certificate-directory file-base ".key"))
+         (cert-file (concat elpher-certificate-directory file-base ".crt")))
+    (elpher-generate-certificate common-name key-file cert-file)))
+
+(defun elpher-get-existing-certificate (file-base)
+  "Return a certificate object corresponding to an existing certificate.
+It is assumed that the key files FILE-BASE.key and FILE-BASE.crt exist in
+the directory `elpher-certificate-directory'."
+  (let* ((key-file (concat elpher-certificate-directory file-base ".key"))
+         (cert-file (concat elpher-certificate-directory file-base ".crt")))
+    (list (elpher-address-host (elpher-page-address elpher-current-page))
+          nil
+          (expand-file-name key-file)
+          (expand-file-name cert-file))))
+
+(defun elpher-install-and-use-existing-certificate (key-file-src cert-file-src file-base)
+  "Install a key+certificate file pair in `elpher-certificate-directory'.
+The strings KEY-FILE-SRC and CERT-FILE-SRC are the existing key and
+certificate files to install.  The argument FILE-BASE is used as the
+base for the installed key and certificate files."
+  (let* ((key-file (concat elpher-certificate-directory file-base ".key"))
+         (cert-file (concat elpher-certificate-directory file-base ".crt")))
+    (if (or (file-exists-p key-file)
+            (file-exists-p cert-file))
+        (error "A certificate with base name %s is already installed" file-base))
+    (copy-file key-file-src key-file)
+    (copy-file cert-file-src cert-file)
+    (list (elpher-address-host (elpher-page-address elpher-current-page))
+          nil
+          (expand-file-name key-file)
+          (expand-file-name cert-file))))
+
+(defun elpher-list-existing-certificates ()
+  "Return a list of the persistent certificates in `elpher-certificate-directory'."
+  (unless (file-directory-p elpher-certificate-directory)
+    (make-directory elpher-certificate-directory))
+  (mapcar
+   (lambda (file)
+     (file-name-sans-extension file))
+   (directory-files elpher-certificate-directory nil "\.key$")))
+
+(defun elpher-forget-current-certificate ()
+  "Causes any current certificate to be forgotten.)
+In the case of throwaway certificates, the key and certificate files
+are also deleted."
+  (interactive)
+  (when elpher-client-certificate
+    (unless (and (called-interactively-p 'any)
+                 (not (y-or-n-p (concat "Really forget client certificate? "
+                                        "(Throwaway certificates will be deleted.)"))))
+      (when (cadr elpher-client-certificate)
+        (delete-file (elt elpher-client-certificate 2))
+        (delete-file (elt elpher-client-certificate 3)))
+      (setq elpher-client-certificate nil)
+      (if (called-interactively-p 'any)
+          (message "Client certificate forgotten.")))))
+
+(defun elpher-get-current-keylist (address)
+  "Retrieve the `gnutls-boot-parameters'-compatable keylist.
+
+This is obtained from the client certificate described by
+`elpher-current-certificate', if one is available and the host for
+that certificate matches the host in ADDRESS.
+
+If `elpher-current-certificate' is non-nil, and its host name doesn't
+match that of ADDRESS, the certificate is forgotten."
+  (if elpher-client-certificate
+      (if (string= (car elpher-client-certificate)
+                   (elpher-address-host address))
+          (list (cddr elpher-client-certificate))
+        (elpher-forget-current-certificate)
+        (message "Disabling client certificate for new host")
+        nil)
+    nil))
+
 
 ;;; Gopher selector retrieval
 ;;
@@ -979,7 +1138,10 @@ that the response was malformed."
         (?1 ; Input required
          (elpher-with-clean-buffer
           (insert "Gemini server is requesting input."))
-         (let* ((query-string (read-string (concat response-meta ": ")))
+         (let* ((query-string
+                 (if (eq (elt response-code 1) ?1)
+                     (read-passwd (concat response-meta ": "))
+                   (read-string (concat response-meta ": "))))
                 (query-address (seq-copy (elpher-page-address elpher-current-page)))
                 (old-fname (url-filename query-address)))
            (setf (url-filename query-address)
@@ -1008,11 +1170,68 @@ that the response was malformed."
          (error "Gemini server reports PERMANENT FAILURE for this request: %s %s"
                 response-code response-meta))
         (?6 ; Client certificate required
-         (error "Gemini server requires client certificate (unsupported at this time)"))
+         (elpher-with-clean-buffer
+          (if elpher-client-certificate
+              (insert "Gemini server does not recognise the provided TLS certificate:\n\n")
+            (insert "Gemini server is requesting a valid TLS certificate:\n\n"))
+          (auto-fill-mode 1)
+          (elpher-gemini-insert-text response-meta))
+         (let ((chosen-certificate (elpher-choose-client-certificate)))
+           (unless chosen-certificate
+             (error "Gemini server requires a client certificate and none was provided"))
+           (setq elpher-client-certificate chosen-certificate))
+         (elpher-with-clean-buffer)
+         (elpher-get-gemini-response (elpher-page-address elpher-current-page) renderer))
         (_other
          (error "Gemini server response unknown: %s %s"
                 response-code response-meta))))))
 
+(defun elpher-choose-client-certificate ()
+  "Prompt for a client certificate to use to establish a TLS connection."
+  (let* ((read-answer-short t))
+    (pcase (read-answer "What do you want to do? "
+                        '(("throwaway" ?t
+                           "generate and use throw-away certificate")
+                          ("persistent" ?p
+                           "generate new or use existing persistent certificate")
+                          ("abort" ?a
+                           "stop immediately")))
+      ("throwaway"
+       (setq elpher-client-certificate (elpher-generate-throwaway-certificate)))
+      ("persistent"
+       (let* ((existing-certificates (elpher-list-existing-certificates))
+              (file-base (completing-read
+                          "Nickname for new or existing certificate (autocompletes, empty response aborts): "
+                          existing-certificates)))
+         (if (string-empty-p (string-trim file-base))
+             nil
+           (if (member file-base existing-certificates)
+               (setq elpher-client-certificate
+                     (elpher-get-existing-certificate file-base))
+             (pcase (read-answer "Generate new certificate or install externally-generated one? "
+                                 '(("new" ?n
+                                    "generate new certificate")
+                                   ("install" ?i
+                                    "install existing certificate")
+                                   ("abort" ?a
+                                    "stop immediately")))
+               ("new"
+                (let ((common-name (read-string "Common Name field for new certificate: "
+                                                file-base)))
+                  (message "New key and self-signed certificate written to %s"
+                           elpher-certificate-directory)
+                  (elpher-generate-persistent-certificate file-base common-name)))
+               ("install"
+                (let* ((cert-file (read-file-name "Certificate file: " nil nil t))
+                       (key-file (read-file-name "Key file: " nil nil t)))
+                  (message "Key and certificate installed in %s for future use"
+                           elpher-certificate-directory)
+                  (elpher-install-and-use-existing-certificate key-file
+                                                               cert-file
+                                                               file-base)))
+               ("abort" nil))))))
+      ("abort" nil))))
+
 (defun elpher-get-gemini-page (renderer)
   "Getter which retrieves and renders a Gemini page and renders it using RENDERER."
   (let* ((address (elpher-page-address elpher-current-page))
@@ -1029,7 +1248,6 @@ that the response was malformed."
       (error
        (elpher-network-error address the-error)))))
 
-
 (defun elpher-render-gemini (body &optional mime-type-string)
   "Render gemini response BODY with rendering MIME-TYPE-STRING."
   (if (not body)
@@ -1096,7 +1314,10 @@ For instance, the filename /a/b/../c/./d will reduce to /a/c/d"
     (string-join (reverse path-reversed-normalized) "/")))
 
 (defun elpher-address-from-gemini-url (url)
-  "Extract address from URL with defaults as per gemini map files."
+  "Extract address from URL with defaults as per gemini map files.
+While there's obviously some redundancy here between this function and
+`elpher-address-from-url', gemini map file URLs require enough special
+treatment that a separate function is warranted."
   (let ((address (url-generic-parse-url url))
         (current-address (elpher-page-address elpher-current-page)))
     (unless (and (url-type address) (not (url-fullness address))) ;avoid mangling mailto: urls
@@ -1112,10 +1333,10 @@ For instance, the filename /a/b/../c/./d will reduce to /a/c/d"
                         (url-filename address)))))
       (unless (url-type address)
         (setf (url-type address) "gemini"))
-      (if (equal (url-type address) "gemini")
-          (setf (url-filename address)
-                (elpher-collapse-dot-sequences (url-filename address)))))
-    address))
+      (when (equal (url-type address) "gemini")
+        (setf (url-filename address)
+              (elpher-collapse-dot-sequences (url-filename address)))))
+    (elpher-remove-redundant-ports address)))
 
 (defun elpher-gemini-insert-link (link-line)
   "Insert link described by LINK-LINE into a text/gemini document."
@@ -1205,6 +1426,7 @@ width defined by elpher-gemini-max-fill-width."
     (elpher-page-address elpher-current-page)
     (buffer-string))))
 
+
 ;; Finger page connection
 
 (defun elpher-get-finger-page (renderer)
@@ -1231,6 +1453,22 @@ The result is rendered using RENDERER."
          (elpher-network-error address the-error))))))
 
 
+;; Telnet page connection
+
+(defun elpher-get-telnet-page (renderer)
+  "Opens a telnet connection to the current page address (RENDERER must be nil)."
+  (when renderer
+    (elpher-visit-previous-page)
+    (error "Command not supported for telnet URLs"))
+  (let* ((address (elpher-page-address elpher-current-page))
+         (host (elpher-address-host address))
+         (port (elpher-address-port address)))
+    (elpher-visit-previous-page)
+    (if (> port 0)
+        (telnet host port)
+      (telnet host))))
+
+
 ;; Other URL page opening
 
 (defun elpher-get-other-url-page (renderer)
@@ -1247,20 +1485,6 @@ The result is rendered using RENDERER."
           (browse-web url)
         (browse-url url)))))
 
-;; Telnet page connection
-
-(defun elpher-get-telnet-page (renderer)
-  "Opens a telnet connection to the current page address (RENDERER must be nil)."
-  (when renderer
-    (elpher-visit-previous-page)
-    (error "Command not supported for telnet URLs"))
-  (let* ((address (elpher-page-address elpher-current-page))
-         (host (elpher-address-host address))
-         (port (elpher-address-port address)))
-    (elpher-visit-previous-page)
-    (if (> port 0)
-        (telnet host port)
-      (telnet host))))
 
 ;; Start page page retrieval
 
@@ -1293,6 +1517,7 @@ The result is rendered using RENDERER."
            " - R: reload current page (regenerates cache)\n"
            " - S: set character coding system for gopher (default is to autodetect)\n"
            " - T: toggle TLS gopher mode\n"
+           " - F: forget/discard current TLS client certificate\n"
            " - .: display the raw server response for the current page\n"
            "\n"
            "Start your exploration of gopher space and gemini:\n")
@@ -1462,7 +1687,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
   (interactive "sGopher or Gemini URL: ")
   (let* ((cleaned-host-or-url (string-trim host-or-url))
          (address (elpher-address-from-url cleaned-host-or-url))
-         (page (elpher-make-page cleaned-host-or-url address))) 
+         (page (elpher-make-page cleaned-host-or-url address)))
     (switch-to-buffer "*elpher*")
     (elpher-visit-page page)
     nil))
@@ -1732,6 +1957,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
     (define-key map (kbd "X") 'elpher-unbookmark-current)
     (define-key map (kbd "B") 'elpher-bookmarks)
     (define-key map (kbd "S") 'elpher-set-gopher-coding-system)
+    (define-key map (kbd "F") 'elpher-forget-current-certificate)
     (when (fboundp 'evil-define-key*)
       (evil-define-key* 'motion map
         (kbd "TAB") 'elpher-next-link
@@ -1758,7 +1984,8 @@ When run interactively HOST-OR-URL is read from the minibuffer."
         (kbd "x") 'elpher-unbookmark-link
         (kbd "X") 'elpher-unbookmark-current
         (kbd "B") 'elpher-bookmarks
-        (kbd "S") 'elpher-set-gopher-coding-system))
+        (kbd "S") 'elpher-set-gopher-coding-system
+        (kbd "F") 'elpher-forget-current-certificate))
     map)
   "Keymap for gopher client.")
 
index a78b750..7229c99 100644 (file)
@@ -1,7 +1,7 @@
 \input texinfo @c -*-texinfo-*-
 
 @setfilename elpher.info
-@settitle Elpher Manual v2.7.0
+@settitle Elpher Manual v2.10.0
 
 @dircategory Emacs
 @direntry
@@ -11,7 +11,7 @@
 @copying
 This manual documents Elpher, a gopher and gemini client for Emacs.
 
-Copyright @copyright{} 2019 Tim Vaughan
+Copyright @copyright{} 2019, 2020 Tim Vaughan
 
 @quotation
 The source and documentation of Elpher is free software.  You can
@@ -472,14 +472,14 @@ you can't accidentally make a non-TLS connection.
 @chapter Gemini support
 
 @uref{gopher://gemini.circumlunar.space, Gemini}
-is a new protocol being devloped by several members of
+is a new protocol being developed by several members of
 gopherspace.  It aims to solve some of the long-standing technical
-issues associated with gopher as a protocol, while keeping the major benifits.
+issues associated with gopher as a protocol, while keeping the major benefits.
 For instance, it _requires_ encrypted connections, it does away with
 the selector type, and allows servers to explicitly specify the
 character coding scheme used for text documents.
 
-The latest versions of Elpher aim to provide seemless navigation between
+The latest versions of Elpher aim to provide seamless transitions between
 gemini and gopher documents.  Basically you should be able to open,
 bookmark, download and otherwise interact with gemini pages in exactly
 the same way as you do with other non-gemini pages.  The only major
@@ -498,31 +498,98 @@ certificate verification by default.  This behaviour can be easily
 customized by setting the @code{elpher-gemini-TLS-cert-checks}
 customization variable to non-nil.
 
-Like gopher, the gemini specification concerns both the protocol 
- a simple text document format (mimetype text/gemini) which is
-like a mixture between gophermap files and markdown-formatted files but
-simpler than both.  Elpher renders gemini responses which are provided
-in this format in line with the rules in the spec.  This includes
-wrapping long lines at word boundaries.  The specific column at which
-this text is wrapped is defined by the customization variable
+The gemini specification concerns both the protocol and a simple text
+document format (mimetype text/gemini) which is like a mixture between
+gophermap files and markdown-formatted files but simpler than both.
+Elpher renders gemini responses which are provided in this format in
+line with the rules in the spec.  This includes wrapping long lines at
+word boundaries.  The specific column at which this text is wrapped is
+defined by the customization variable
 @code{elpher-gemini-max-fill-width}, which is set to 80 columns by
 default. (This is slightly wider than Emacs' default fill width of 70
 columns due to the fact that there are a significant amount of older
-gemini which, against the advice of the current spec, hard wraps at <80
-columns.  The larger default allows this to still look okay, while
-still keeping content without hard wraps looking pleasant.)
+gemini content which, against the advice of the current spec, hard wraps
+at <80 columns.  The larger default allows this to still look okay,
+while still keeping content without hard wraps looking pleasant.)
 
-The text/gemini format also posesses a section header syntax similar to
+The text/gemini format also possesses a section header syntax similar to
 markdown.  Elpher allows different header levels to be drawn with
 different, customizable, faces.  By default, on graphically-capable
 emacs systems, these faces are given different heights to distinguish
-amongst levels.  On terminal systems, the level is indicated by the
-number of preceeding # symbols.
+among levels.  On terminal systems, the level is indicated by the
+number of preceding # symbols.
 
 I should emphasize however that, while it is definitely functional,
 Elpher's gemini support is still experimental, and various aspects will
-change as the protocol develops further.  Additionally, the use of
-client TLS certicificates is still not yet supported.
+change as the protocol develops further.
+
+@section Client Certificates for Gemini
+
+Gemini makes explicit use of the client certificate mechanism that TLS
+provides for allowing clients to authenticate themselves with servers.
+The Gemini specification suggests two distinct classes of client
+certificates: short-lived certificates used to identify you for a single
+session, and more permanent certificates used to identify you over a
+longer time period.
+
+When Elpher receives a request for a client certificate from a server,
+it will present you with the option to create and use a single-use
+``throwaway'' certificate, or to use a ``persistent''
+certificate (optionally creating it or installing pre-existing key and
+certificate files).
+
+Certificate creation in Elpher requires an installation of OpenSSL, and
+---in particular---that Elpher be able to run the @command{openssl} command-line
+utility.  By default, Elpher assumes that the @command{openssl} is on the
+system path, but the precise location can be set by customizing the
+@code{elpher-openssl-command} variable.
+
+Each generated certificate results in the creation of a .key file and
+a .crt file.  In the case of a throwaway certificate, these files are
+stored in the temporary directory indicated by the Emacs variable
+@code{temporary-file-directory} and are deleted when ``forgotten''
+(as described below).
+
+In the case of persistent certificates, these files are stored in the
+folder defined by the Elpher variable
+@code{elpher-certificate-directory}, and are never deleted by Elpher.
+(Of course you can delete them yourself whenever you like.)
+The base name of the files (i.e. sans extension) is what Elpher uses
+to identify the certificate.
+
+Using throwaway certificates is as simple as pressing the @key{t} 
+key at the prompt which appears following a certificate request from
+a server.  There is nothing more to do.
+
+Using a persistent certificate requires instead selecting @key{p} from the same
+menu.  This will result in Elpher asking you for the name identifying
+the certificate.  This entry autocompletes to the list of known certificate
+names, so you can use @key{TAB} to display the list.
+
+In the case that you choose a name that does not belong to the list of
+known certificates, Elpher will offer to create one for you or to
+``install'' one from existing key and certificate files.
+Pressing the @key{n} key will cause Elpher to begin the process of
+creating a new persistent certificate, using some additional
+details for which you will be prompted.
+Alternatively, pressing the @key{i} key will cause Elpher to ask for the
+locations of edisting key and certificate files to add to
+@code{elpher-certificate-directory} under the chosen name.
+
+Once a certificate is selected, it will be used for all subsequent TLS
+transactions to the host for which the certificate was created.
+It is immediately ``forgotten'' when a TLS connection to another host
+is attempted, or the following command is issued:
+
+@table @asis
+@keycmd{@key{F},elpher-forget-certificate}
+Causes Elpher to immediately forget any currently-loaded client certificate.
+@end table
+
+In either case, ``forgetting'' means that the details of the key and
+certificate file pair are erased from memory.  Furthermore, in the case
+of throw-away certificates, the corresponding files are deleted.
+
 
 @node Finger support, Customization, Gemini support, Top
 @chapter Finger support