wish to use third party package archives---but do so at your own risk,
and use only third parties that you think you can trust!
-@anchor{Package Signing}
+@anchor{Package Checksums}
@cindex package security
+@cindex package checksums
+ To improve security, maintainers of package archive can add two
+important measures: checksums and signatures of metadata. When used
+together, they can increase your trust that a downloaded package has
+not been tampered with, and is not out of date. Valid checksums and
+signatures are not a cast-iron guarantee that a package is not
+malicious, so you should still exercise caution. Only install
+packages from package archives that you trust.
+
+ When installing a package from an archive providing package
+checksums, the package system will automatically verify that they
+match the downloaded package. By default, Emacs will refuse to
+install a package with an invalid checksum, but still allow installing
+a package if checksums are missing. To disable installing packages
+from archives without checksums, you can set user the user option
+@code{package-verify-checksums} to @code{t}. This improves security,
+but requires that all package archives you use distribute checksums.
+
+@anchor{Package Signing}
@cindex package signing
The maintainers of package archives can increase the trust that you
-can have in their packages by @dfn{signing} them. They generate a
-private/public pair of cryptographic keys, and use the private key to
-create a @dfn{signature file} for each package. With the public key, you
-can use the signature files to verify the package creator and make sure
-the package has not been tampered with. Signature verification uses
-@uref{https://www.gnupg.org/, the GnuPG package} via the EasyPG
-interface (@pxref{Top,, EasyPG, epa, Emacs EasyPG Assistant Manual}).
-A valid signature is not a cast-iron
-guarantee that a package is not malicious, so you should still
-exercise caution. Package archives should provide instructions
+can have in their packages and package listings by @dfn{signing} them.
+They generate a private/public pair of cryptographic keys, and use the
+private key to create a @dfn{signature file} for the package listing
+itself or each individual package. With the public key, you can use
+the signature files to verify the files have not been tampered with.
+Signature verification uses @uref{https://www.gnupg.org/, the GnuPG
+package} via the EasyPG interface (@pxref{Top,, EasyPG, epa, Emacs
+EasyPG Assistant Manual}).
+
+The public key for the GNU package archive is distributed with Emacs,
+in the @file{etc/package-keyring.gpg}. Emacs uses it automatically.
+Other package archives should provide instructions
on how you can obtain their public key. One way is to download the
key from a server such as @url{https://pgp.mit.edu/}.
Use @kbd{M-x package-import-keyring} to import the key into Emacs.
GnuPG with the option @samp{--homedir} when verifying signatures.
If @code{package-gnupghome-dir} is @code{nil}, GnuPG's option
@samp{--homedir} is omitted.
-The public key for the GNU package archive is distributed with Emacs,
-in the @file{etc/package-keyring.gpg}. Emacs uses it automatically.
@vindex package-check-signature
@vindex package-unsigned-archives
'package-version-column-width', 'package-status-column-width', and
'package-archive-column-width'.
+*** Support for package checksums.
+This improves the security of the Emacs package system. If the
+package archives you use implements package checksums, you will
+automatically benefit from this by default.
+
+The user option 'package-verify-checksums' controls how and when the
+package system will use checksums. The default is 'allow-missing',
+which will check them when they are available yet allow installation
+if they are missing.
+
+For improved security, you might want to set this to 't' or
+'all'. Make sure that the package archives you use support checksums
+before setting these values, or you will be unable to install
+packages.
+
** gdb-mi
+++
(let ((contents (or (package--archive-contents-from-url archive-url)
(package--archive-contents-from-file)))
(new-desc (package-make-ac-desc
- split-version requires desc file-type extras)))
+ split-version requires desc file-type extras
+ ;; FIXME: Use better values than nil nil.
+ nil nil)))
(if (> (car contents) package-archive-version)
(error "Unrecognized archive version %d" (car contents)))
(let ((elt (assq pkg-name (cdr contents))))
:risky t
:version "26.1")
+(defcustom package-verify-checksums 'allow-missing
+ "Non-nil means to verify the checksum of a package before installing it.
+
+This can be one of:
+- t Require a valid checksum; refuse to install
+ package if the checksum is missing or invalid.
+ Verify only one checksum.
+- `all' Same as t, but verify all available (and supported)
+ checksums.
+- `allow-missing' Same as t if a checksum exists, but install a
+ package even if there is no checksum.
+- nil Ignore checksums.
+
+The package checksums are automatically fetched from package
+archives with the package data on `package-refresh-contents'.
+
+Note that setting this to nil is intended for debugging, and
+should normally not be used since it will decrease security."
+ :type '(choice (const nil :tag "Never")
+ (const allow-missing :tag "Allow missing")
+ (const t :tag "Require valid checksum")
+ (const t :tag "Require valid checksum, and check all"))
+ :risky t
+ :version "28.1")
+
(defcustom package-check-signature 'allow-unsigned
"Non-nil means to check package signatures when installing.
More specifically the value can be:
:type 'number
:version "28.1")
+\f
+;;; Errors
+
+(define-error 'package-error "Unknown package error")
+(define-error 'bad-size "Package size mismatch" 'package-error)
+(define-error 'bad-signature "Failed to verify signature" 'package-error)
+(define-error 'bad-checksum "Failed to verify checksum" 'package-error)
+
\f
;;; `package-desc' object definition
;; This is the struct used internally to represent packages.
requirements)))
(kind (plist-get rest-plist :kind))
(archive (plist-get rest-plist :archive))
+ (checksums (plist-get rest-plist :checksums))
+ (size (plist-get rest-plist :size))
(extras (let (alist)
(while rest-plist
(unless (memq (car rest-plist) '(:kind :archive))
`extras' Optional alist of additional keyword-value pairs.
+`size' Size of the package in bytes.
+
+`checksums' Checksums for the package file. Alist of ((ALGORITHM
+ . CHECKSUM)) where ALGORITHM is a symbol specifying a
+ `secure-hash' algorithm, and CHECKSUM is a string
+ containing the checksum.
+
`signed' Flag to indicate that the package is signed by provider."
name
version
archive
dir
extras
- signed)
+ signed
+ size
+ checksums)
(defun package--from-builtin (bi-desc)
"Create a `package-desc' object from BI-DESC.
('dir "")
(kind (error "Unknown package kind: %s" kind))))
+(defun package-desc-filename (pkg-desc)
+ "Return file-name of package-desc object PKG-DESC.
+This is the concatenation of `package-desc-full-name' and
+`package-desc-suffix'."
+ (concat (package-desc-full-name pkg-desc)
+ (package-desc-suffix pkg-desc)))
+
(defun package-desc--keywords (pkg-desc)
"Return keywords of package-desc object PKG-DESC.
These keywords come from the foo-pkg.el file, and in general
url))
(insert-file-contents-literally url)))))
-(define-error 'bad-signature "Failed to verify signature")
+(defun package--show-verify-checksum-error (pkg-desc details)
+ "Show error on failed checksum verification of PKG-DESC with DETAILS.
+Error is displayed in a new buffer named \"*Error*\"."
+ (with-output-to-temp-buffer "*Error*"
+ (with-current-buffer standard-output
+ (insert (format "Failed to verify checksum of package `%s':\n\n"
+ (package-desc-name pkg-desc)))
+ (insert details))))
+
+(defconst package-insecure-hash-algorithms '(md5 sha1)
+ "List of hash algorithms that are not considered secure.")
+
+(defun package--verify-package-checksum (pkg-desc)
+ "Verify checksums of `package-desc' object PKG-DESC.
+This assumes that the we are in a buffer containing package.
+
+The value of `package-verify-checksums' decides what this
+function does:
+- t Verify that there is at least one valid checksum.
+- `all' Like t, but check all supported checksums.
+- `allow-missing' Verify checksum if it exists, otherwise do
+ nothing.
+- nil Do nothing.
+
+Signal an error of type `bad-checksum' if the verification."
+ (cl-flet*
+ ((supported-hashes
+ (lambda ()
+ (or (seq-filter
+ (lambda (h)
+ (and (memql (car h) (secure-hash-algorithms))
+ (not (memql (car h) package-insecure-hash-algorithms))))
+ (package-desc-checksums pkg-desc))
+ ;; Failed; signal error.
+ (package--show-verify-checksum-error
+ pkg-desc
+ (concat
+ (if (package-desc-checksums pkg-desc)
+ (concat
+ "No supported checksums found\n\n"
+ (format-message "Package archive had: %s\n"
+ (package-desc-checksums pkg-desc))
+ (format-message "Emacs supports: %s\n"
+ (secure-hash-algorithms)))
+ "Package archive had no checksums for this package\n")))
+ (signal 'bad-checksum "no supported checksums found"))))
+ (do-check
+ (lambda (&optional all)
+ (dolist (hash (seq-take (supported-hashes)
+ (if all most-positive-fixnum 1)))
+ (let* ((algorithm (car hash))
+ (expected (cdr hash))
+ (actual (secure-hash algorithm (current-buffer))))
+ (if (equal expected actual) t
+ ;; Failed; signal error.
+ (package--show-verify-checksum-error
+ pkg-desc
+ (concat
+ (format-message "\nChecksum mismatch (%s)\n\n" algorithm)
+ (format-message "Expected: %s\n" expected)
+ (format-message "Result: %s\n" actual)))
+ (signal 'bad-checksum (list "checksum mismatch" expected actual))))))))
+ (pcase package-verify-checksums
+ ('nil nil)
+ ('allow-missing (when (package-desc-checksums pkg-desc) (do-check)))
+ ('t (do-check))
+ ('all (do-check 'all))
+ (_ (user-error "Value of `package-verify-checksums' is invalid: `%s'"
+ package-verify-checksums)))))
+
+(defun package--verify-package-size (pkg-desc)
+ "Verify package size of `package-desc' object PKG-DESC.
+This assumes that the we are in a buffer containing package."
+ (when-let ((expected (package-desc-size pkg-desc))
+ (actual (string-bytes (buffer-string))))
+ (unless (equal expected actual)
+ (with-output-to-temp-buffer "*Error*"
+ (with-current-buffer standard-output
+ (insert (format "Mismatch in package size for `%s':\n"
+ (package-desc-name pkg-desc)))
+ (insert (format "Expected %s bytes, but received %s" expected actual))))
+ (signal 'bad-size (list "size mismatch" expected actual)))))
(defun package--check-signature-content (content string &optional sig-file)
"Check signature CONTENT against STRING.
(version-list-< table-version version))
(puthash name version package--compatibility-table)))))
-;; Package descriptor objects used inside the "archive-contents" file.
-;; Changing this defstruct implies changing the format of the
-;; "archive-contents" files.
(cl-defstruct (package--ac-desc
- (:constructor package-make-ac-desc (version reqs summary kind extras))
+ (:constructor
+ package-make-ac-desc (version reqs summary kind extras size checksums))
(:copier nil)
(:type vector))
- version reqs summary kind extras)
+ "Package descriptor object used inside the \"archive-contents\" file.
+Changing this defstruct implies changing the format of the
+\"archive-contents\" files.
+
+This is mainly used in `package--add-to-archive-contents' to make
+the code that parses the \"archive-contents\" file more
+readable."
+ version reqs summary kind extras size checksums)
(defun package--append-to-alist (pkg-desc alist)
"Append an entry for PKG-DESC to the start of ALIST and return it.
:summary (package--ac-desc-summary (cdr package))
:kind (package--ac-desc-kind (cdr package))
:archive archive
+ ;; Older "archive-contents" files might not have the
+ ;; below elements.
:extras (and (> (length (cdr package)) 4)
- ;; Older archive-contents files have only 4
- ;; elements here.
- (package--ac-desc-extras (cdr package)))))
+ (package--ac-desc-extras (cdr package)))
+ :size (and (> (length (cdr package)) 5)
+ (package--ac-desc-size (cdr package)))
+ :checksums (and (> (length (cdr package)) 6)
+ (package--ac-desc-checksums (cdr package)))))
(pinned-to-archive (assoc name package-pinned-packages)))
;; Skip entirely if pinned to another archive.
(when (not (and pinned-to-archive
(when (eq (package-desc-kind pkg-desc) 'dir)
(error "Can't install directory package from archive"))
(let* ((location (package-archive-base pkg-desc))
- (file (concat (package-desc-full-name pkg-desc)
- (package-desc-suffix pkg-desc))))
+ (file (package-desc-filename pkg-desc)))
(package--with-response-buffer location :file file
+ (package--verify-package-size pkg-desc)
+ (package--verify-package-checksum pkg-desc)
(if (or (not (package-check-signature))
(member (package-desc-archive pkg-desc)
package-unsigned-archives))
(multi-file .
[(0 2 3)
nil "Example of a multi-file tar package" tar
- ((:url . "http://puddles.li"))]))
+ ((:url . "http://puddles.li"))])
+ (checksum-valid .
+ [(123)
+ nil "A single-file package with a valid checksum." single
+ nil
+ 343
+ ((sha512 . "a889917427569cc6817db5db08a88390d44ec010acdf6810c2dfaba04b9a03f00315378c3f03d5f4d531833028ad61db54c4c56106662585da6a0dde602f5c0d"))])
+ (checksum-valid-tar .
+ [(0 99)
+ nil "A multi-file package with a valid checksum." tar
+ nil
+ 10240
+ ((sha512 . "2be7c37a16db32a2b08fc917ed5f4241814e2665bda1bd15328c2e5a842e45b81f6f31274697248ffaabf8010796685acb3342c5920af53ddd1e75d7fd764bd1"))])
+ (checksum-invalid .
+ [(1 0)
+ nil "A single-file package with an invalid checksum." single
+ nil
+ 365
+ ((sha512 . "not-a-valid-checksum"))])
+ (checksum-invalid-tar .
+ [(0 1)
+ nil "A multi-file package with an invalid checksum." tar
+ nil
+ 10240
+ ((sha512 . "not-a-valid-checksum"))])
+ (wrong-size-single .
+ [(1 0)
+ nil "A single-file package with an invalid size." single
+ nil
+ 1])
+ (wrong-size-tar .
+ [(1 0)
+ nil "A multi-file package with an invalid size." tar
+ nil
+ 1]))
--- /dev/null
+;;; checksum-invalid.el --- A package with an invalid checksum in archive-contents
+
+;; Version: 1.0
+
+;;; Commentary:
+
+;; This package has an invalid checksum in archive-contents and is
+;; just used to verify that package.el refuses to install.
+
+;;; Code:
+
+(defun p-equal-to-np-p ()
+ (error "FIXME"))
+
+(provide 'checksum-invalid)
+
+;;; checksum-invalid.el ends here
--- /dev/null
+;;; checksum-valid.el --- A package with an valid checksum in archive-contents
+
+;; Version: 123
+
+;;; Commentary:
+
+;; This package has an valid checksum in archive-contents and is
+;; used to verify that package.el installs it.
+
+;;; Code:
+
+(defun p-equal-to-np-p ()
+ (error "FIXME"))
+
+(provide 'checksum-valid)
+
+;;; checksum-valid.el ends here
--- /dev/null
+;; This file just has the wrong size (i.e. not 1 as specified).
(setq package-menu-async nil)
+;; Silence byte-compiler.
+(defvar epg-config--program-alist)
+
(defvar package-test-user-dir nil
"Directory to use for installing packages during testing.")
(with-package-test ()
(package-initialize)
(package-refresh-contents)
- (should (eq 4 (length package-archive-contents)))))
+ (should (eq 10 (length package-archive-contents)))))
(ert-deftest package-test-install-single-from-archive ()
"Install a single package from a package archive."
(with-package-test ()
(package-initialize)
(package-refresh-contents)
- (package-install 'simple-single)))
+ (package-install 'simple-single)
+ (should (package-installed-p 'simple-single))))
+
+(ert-deftest package-test-install-wrong-size-single ()
+ "Install a tar package with invalid size."
+ (should-error
+ (with-package-test ()
+ (package-initialize)
+ (package-refresh-contents)
+ (package-install 'wrong-size-single))
+ :type 'bad-size))
+
+(ert-deftest package-test-install-wrong-size-tar ()
+ "Install a tar package with invalid size."
+ (should-error
+ (with-package-test ()
+ (package-initialize)
+ (package-refresh-contents)
+ (package-install 'wrong-size-tar))
+ :type 'bad-size))
(ert-deftest package-test-install-prioritized ()
"Install a lower version from a higher-prioritized archive."
;; the testing environment currently only has one.
(package-menu-filter-by-archive "gnu")
(goto-char (point-min))
- (should (looking-at "^\\s-+multi-file"))
- (should (= (count-lines (point-min) (point-max)) 4))
+ (should (looking-at "^\\s-+checksum-invalid"))
+ (should (= (count-lines (point-min) (point-max)) 10))
(should-error (package-menu-filter-by-archive "non-existent archive"))))
(ert-deftest package-test-list-filter-by-keyword ()
(package-menu-filter-by-status "available")
(goto-char (point-min))
(should (re-search-forward "^\\s-+multi-file" nil t))
- (should (= (count-lines (point-min) (point-max)) 4))
+ (should (= (count-lines (point-min) (point-max)) 10))
;; No installed packages in default environment.
(should-error (package-menu-filter-by-status "installed"))))
"Status: Installed in ['`‘]signed-good-1.0/['’]."
nil t))))))
+\f
+;;; Tests for package checksum verification.
+
+(defmacro with-install-using-checksum (ok fail package)
+ "Test installing PACKAGE while setting `package-verify-checksums'."
+ (declare (indent 2))
+ `(progn
+ (dolist (opt ,ok)
+ (let ((package-verify-checksums opt))
+ (with-package-test ()
+ (package-initialize)
+ (package-refresh-contents)
+ (package-install ,package)
+ (package-installed-p ,package))))
+ (dolist (opt ,fail)
+ (let ((package-verify-checksums opt))
+ (should-error
+ (with-package-test ()
+ (package-initialize)
+ (package-refresh-contents)
+ (package-install ,package))
+ :type 'bad-checksum)))))
+
+(ert-deftest package-test-install-with-checksum/single-valid ()
+ "Install a single package with valid checksum."
+ (with-install-using-checksum '(nil allow-missing t all) '() 'checksum-valid))
+
+(ert-deftest package-test-install-with-checksum/single-invalid ()
+ "Install a tar package with invalid checksum."
+ (with-install-using-checksum '(nil) '(allow-missing t all) 'checksum-invalid))
+
+(ert-deftest package-test-install-with-checksum/tar-valid ()
+ "Install a tar package with valid checksum."
+ (with-install-using-checksum '(nil allow-missing t all) '() 'checksum-valid-tar))
+
+(ert-deftest package-test-install-with-checksum/tar-invalid ()
+ "Install a tar package with invalid checksum."
+ (with-install-using-checksum '(nil) '(allow-missing t all) 'checksum-invalid-tar))
+
+(defconst package-test-verification-text
+ "Example text for testing checksum verification.")
+(defconst package-tests-valid-md5-checksum
+ ;; (secure-hash 'md5 package-test-verification-text)
+ "abe6375809e532f081b808b3aa052dfb")
+(defconst package-tests-valid-sha256-checksum
+ ;; (secure-hash 'sha256 package-test-verification-text)
+ "6875aa4523e45ddef627b4edf1296f1d7dd0c22ddd6a6584f0228215d25eefcd")
+(defconst package-tests-valid-sha512-checksum
+ ;; (secure-hash 'sha512 package-test-verification-text)
+ (concat "bdc631f9e675b1ea34570f0a4bb44568dc5cecac905eea737f5f451bc52fd0c6"
+ "81b0d8b3dc2a942b9950fbe9096ebdf517668245c9b5a7bbdea8487a8f9cdce6"))
+
+(defmacro package-tests--run-verify-checksums-test (verify-checksums checksums)
+ "Run a test for `package-verify-checksums'."
+ (declare (indent 1))
+ `(with-temp-buffer
+ (insert package-test-verification-text)
+ (let ((package-verify-checksums ,verify-checksums)
+ (pkg (package-desc-create :name 'foobar
+ :version '(1 0)
+ :summary "Just a package with checksum."
+ :kind 'single
+ :checksums ,checksums)))
+ (package--verify-package-checksum pkg))))
+
+(ert-deftest package-test-verify-package-checksums-nil/ignore-invalid ()
+ "Ignore all checksums even when invalid."
+ (package-tests--run-verify-checksums-test nil
+ '((sha512 . "invalid")
+ (invalid . "invalid"))))
+
+(ert-deftest package-test-verify-package-checksums-nil/ignore-empty ()
+ "Ignore all checksums even when empty."
+ (package-tests--run-verify-checksums-test nil
+ nil))
+
+(ert-deftest package-test-verify-package-checksums-allow-missing ()
+ "Verify checksums (allow-missing) -- verify if available."
+ (package-tests--run-verify-checksums-test 'allow-missing
+ `((sha512 . ,package-tests-valid-sha512-checksum))))
+
+(ert-deftest package-test-verify-package-checksums-allow-missing/missing ()
+ "Verify checksums (allow-missing) -- allow missing."
+ (package-tests--run-verify-checksums-test 'allow-missing
+ nil))
+
+(ert-deftest package-test-verify-package-checksums-allow-missing/ignore-unsupported ()
+ "Verify checksums (t) -- ignore unsupported algorithm."
+ (package-tests--run-verify-checksums-test 'allow-missing
+ `((ignore . "not supported")
+ (sha512 . ,package-tests-valid-sha512-checksum))))
+
+(ert-deftest package-test-verify-package-checksums-t ()
+ "Verify checksums (t) -- succeed when valid."
+ (package-tests--run-verify-checksums-test t
+ `((sha512 . ,package-tests-valid-sha512-checksum))))
+
+(ert-deftest package-test-verify-package-checksums-t/invalid-fails ()
+ "Verify checksums (t) -- fail on invalid."
+ (should-error
+ (package-tests--run-verify-checksums-test t
+ '((sha512 . "invalid")))
+ :type 'bad-checksum))
+
+(ert-deftest package-test-verify-package-checksums-t/missing-fails ()
+ "Verify checksums (t) -- fail on missing."
+ (should-error
+ (package-tests--run-verify-checksums-test t
+ nil)
+ :type 'bad-checksum))
+
+(ert-deftest package-test-verify-package-checksums-t/ignore-unsupported ()
+ "Verify checksums (t) -- ignore unsupported algorithm."
+ (package-tests--run-verify-checksums-test t
+ `((ignore . "not supported")
+ (sha512 . ,package-tests-valid-sha512-checksum))))
+
+(ert-deftest package-test-verify-package-checksums-all ()
+ "Verify checksums (all) -- succeed on valid."
+ (package-tests--run-verify-checksums-test 'all
+ `((md5 . ,package-tests-valid-md5-checksum)
+ (sha256 . ,package-tests-valid-sha256-checksum)
+ (sha512 . ,package-tests-valid-sha512-checksum))))
+
+(ert-deftest package-test-verify-package-checksums-all/invalid-fails ()
+ "Verify checksums (all) -- fail if one checksum is invalid."
+ (should-error
+ (package-tests--run-verify-checksums-test 'all
+ `((md5 . ,package-tests-valid-md5-checksum)
+ (sha256 . "invalid")
+ (sha512 . ,package-tests-valid-sha512-checksum)))
+ :type 'bad-checksum))
+
+(ert-deftest package-test-verify-package-checksums-all/missing-fails ()
+ "Verify checksums (all) -- fail on missing checksums."
+ (should-error
+ (package-tests--run-verify-checksums-test 'all
+ nil)
+ :type 'bad-checksum))
+
+(ert-deftest package-test-verify-package-checksums-all/no-supported-hash-fails ()
+ "Verify checksums (all) -- fail if we have no supported hash."
+ (should-error
+ (package-tests--run-verify-checksums-test 'all
+ '((unsupported . "invalid")))
+ :type 'bad-checksum))
+
+(ert-deftest package-test-verify-package-checksums-all/ignore-unsupported ()
+ "Verify checksums (all) -- succed if one hash algorithm is unsupported.
+If the rest succeed, just ignore the unsupported one."
+ (package-tests--run-verify-checksums-test 'all
+ `((md5 . ,package-tests-valid-md5-checksum)
+ (sha256 . ,package-tests-valid-sha256-checksum)
+ (sha512 . ,package-tests-valid-sha512-checksum)
+ (ignore . "not supported"))))
+
+(ert-deftest package-test-verify-package-size ()
+ (with-temp-buffer
+ (let ((pkg-desc (package-desc-create :size 6)))
+ (insert "123456")
+ (package--verify-package-size pkg-desc)
+ (insert "7")
+ (should-error (package--verify-package-size pkg-desc)))))
\f
;;; Tests for package-x features.
'single
'((:authors ("J. R. Hacker" . "jrh@example.com"))
(:maintainer "J. R. Hacker" . "jrh@example.com")
- (:url . "http://doodles.au"))))
+ (:url . "http://doodles.au"))
+ nil
+ nil))
"Expected contents of the archive entry from the \"simple-single\" package.")
(defvar package-x-test--single-archive-entry-1-4
"A single-file package with no dependencies"
'single
'((:authors ("J. R. Hacker" . "jrh@example.com"))
- (:maintainer "J. R. Hacker" . "jrh@example.com"))))
+ (:maintainer "J. R. Hacker" . "jrh@example.com"))
+ nil
+ nil))
"Expected contents of the archive entry from the updated \"simple-single\" package.")
(ert-deftest package-x-test-upload-buffer ()