As part of my job, I often have to look at X.509 (SSL/TLS) certificates. I almost never want to see them in their raw state, a blob of unintelligible Base64 or binary. When opening an image in Emacs, the image is displayed in the window by default, and C-c C-c toggles between the image and its representation on disk. I want to have the same thing for X.509 certificates.

First we create the most important thing for any mode, the hook that facilitates customization.

(defvar x509-certificate-mode-hook nil)

Then we build a function that can take a buffer and pipe it through the openssl x509 command. This function needs to be smart enough to detect errors and undo any damage it causes on error.

(defun x509-certificate-parse-command (encoding)
  "Parse the buffer using OpenSSL's x509 command and the specified encoding."
  (let ((errbuf (generate-new-buffer-name "*x509-parse-error*")))
    ;; Try to parse buffer using specified encoding.
    (shell-command-on-region
     (point-min)
     (point-max)
     (format "openssl x509 -text -noout -inform %s" encoding)
     (buffer-name)
     t
     errbuf)
    ;; Check for failure, represented by the existence of errbuf.
    (if (get-buffer errbuf)
        ;; Restore buffer to original state.
        (progn
          (kill-buffer errbuf)
          (insert x509-certificate-mode-text)
          nil)
      t)))

Next we need a pair of functions that can transform the buffer between parsed and raw states. Two variables will be needed, which should be local to the buffer: one to hold the raw buffer contents, and another to track the current state of the buffer.

(defun x509-certificate-parse ()
  "Parse the buffer as a certificate, trying multiple encodings."
  (interactive)
  (if (not (eq x509-certificate-mode-display :raw))
      (error "The buffer is not in :raw mode, it's in %s mode."
             x509-certificate-mode-display)
    (let ((modified (buffer-modified-p)))
      ;; Save the contents of the buffer.
      (setq x509-certificate-mode-text (buffer-string))
      (read-only-mode -1)
      ;; Try to convert the buffer through different formats.
      (if (not (x509-certificate-parse-command "pem"))
          (if (not (x509-certificate-parse-command "der"))
              (error "Failed to parse buffer as X.509 certificate.")))
      (read-only-mode 1)
      ;; Restore previous modification state.
      (set-buffer-modified-p modified)
      (setq x509-certificate-mode-display :parsed))))

(defun x509-certificate-raw ()
  "Revert buffer to unparsed contents."
  (interactive)
  (if (not (eq x509-certificate-mode-display :parsed))
      (error "The buffer is not in :parsed mode, it's in %s mode."
             x509-certificate-mode-display)
    (let ((modified (buffer-modified-p)))
      ;; Delete the buffer, which currently contains the parsed format.
      (read-only-mode -1)
      (erase-buffer)
      ;; Convert the buffer into its raw format.
      (insert x509-certificate-mode-text)
      (read-only-mode 1)
      ;; Restore previous modification state.
      (set-buffer-modified-p modified)
      (setq x509-certificate-mode-display :raw))))

With the transformation functions in place, we would like a keybinding to easily toggle between them. This is done by making a function that dispatches based on the state variable. We also create a keybinding for this mode, instead of tainting the global key map with our function.

(defun x509-certificate-toggle-display ()
  "Toggle between raw and parsed displays of the buffer."
  (interactive)
  (cond ((eq x509-certificate-mode-display :parsed)
         (x509-certificate-raw))
        ((eq x509-certificate-mode-display :raw)
         (x509-certificate-parse))
        (t
         (error "Variable x509-certificate-mode-display is in an unknown state: %s"
                x509-certificate-mode-display))))

(defvar x509-certificate-mode-map
  (let ((map (make-keymap)))
    (define-key map (kbd "C-c C-c") 'x509-certificate-toggle-display)
    map)
  "Keymap for X.509 Certificate major mode")

Calling modes manually via M-x is a pain, so we add likely extensions to a list that maps them to our new major mode. Certificates often have wacky extensions, though, so we also provide a regular expression to match text at the beginning of the buffer.

(add-to-list 'auto-mode-alist '("\\.\\(der\\|crt\\|pem\\)$" . x509-certificate-mode))
(add-to-list 'magic-mode-alist '("-----BEGIN CERTIFICATE-----" . x509-certificate-mode))

Finally, we create our major mode and register it in the Emacs environment. Overly verbose comments are inline.

(defun x509-certificate-mode ()
  "Major mode for viewing X.509 certificates"
  ;; Ensure this function is callable by M-x.
  (interactive)
  ;; Clear the slate.
  (kill-all-local-variables)
  ;; Use our key map just for this buffer
  (use-local-map x509-certificate-mode-map)
  ;; Set the symbol (computer-recognizable) and name (human-visible).
  (setq major-mode 'x509-certificate-mode
        mode-name "X.509")
  ;; Create the two buffer-local variables on which our functions depend.
  (defvar-local x509-certificate-mode-display :raw
    "Current display mode of the data")
  (defvar-local x509-certificate-mode-text nil
    "Original text of the buffer")
  ;; Run the customization hooks.
  (run-hooks 'x509-certificate-mode-hook)
  ;; Perform the initial parse of the buffer
  (x509-certificate-parse))

(provide 'x509-certificate-mode)

And that's it, not much to it. From this point forward I will look at parsed certificates in Emacs by default. This makes me ridiculously happy.