]> git.eshelyaron.com Git - emacs.git/commitdiff
Support package checksum verification
authorStefan Kangas <stefankangas@gmail.com>
Fri, 4 Oct 2019 08:36:14 +0000 (10:36 +0200)
committerStefan Kangas <stefan@marxist.se>
Sat, 21 Nov 2020 23:38:35 +0000 (00:38 +0100)
Package checksum verification is the first step towards protecting
users of package.el against replay attacks.  Signing individual
packages still allows a hostile actor to distribute an out-of-date
package containing a known security defect.  To avoid that, we need to
distribute signed package metadata (the ELPA "archive-contents" file)
and checksums for the individual packages together.  (Bug#19479)

A subsequent patch will add support for last-update and expiration
timestamps in "archive-contents", without which the protection against
replay attacks will be largely ineffective.

Taken together, this feature will make signing individual packages
obsolete.  We will instead rely on signing the metadata, package
checksums and timestamps.  Note that individual package signatures
should still be distributed for a long time still to support old
versions of Emacs.

For more on replay attacks, see:
https://www2.cs.arizona.edu/stork/packagemanagersecurity/attacks-on-package-managers.html

* lisp/emacs-lisp/package.el (package-verify-checksums): New
defcustom.
(package-desc, package--ac-desc)
(package--add-to-archive-contents, package-install-from-archive): New
fields 'size' and 'checksums'.
(package-desc-filename): New function.

(package-error): New error type.
(bad-signature): Inherit from error type 'package-error'.
(bad-checksum, bad-size): New error types.
(package-insecure-hash-algorithms): New constant.
(package--verify-package-checksum)
(package--verify-package-size): New function to verify that the
checksum and size of a package corresponds to the checksum and size
data in the "archive-contents" file on the package archive.
(package--show-verify-checksum-error): New function to show
details of an error on checksum verification.

* lisp/emacs-lisp/package-x.el (package-upload-buffer-internal):
Update to use above new fields 'size' and 'checksums'.

* test/lisp/emacs-lisp/package-tests.el (package-test-refresh-contents)
(package-test-install-single-from-archive)
(package-test-list-filter-by-archive)
(package-test-list-filter-by-status): Update tests.
(with-install-using-checksum): New macro.
(package-test-install-wrong-size-single)
(package-test-install-wrong-size-tar): New tests for size checking.
(package-test-install-with-checksum/single-valid)
(package-test-install-with-checksum/single-invalid)
(package-test-install-with-checksum/tar-valid)
(package-test-install-with-checksum/tar-invalid): New tests for
installing packages with checksums.
(package-test-verification-text)
(package-tests-valid-md5-checksum)
(package-tests-valid-sha256-checksum)
(package-tests-valid-sha512-checksum): New variables.
(package-tests--run-verify-checksums-test): New macro.
(package-test-verify-package-checksums-nil/ignore-invalid)
(package-test-verify-package-checksums-allow-missing)
(package-test-verify-package-checksums-allow-missing/missing)
(package-test-verify-package-checksums-allow-missing/ignore-unsupported)
(package-test-verify-package-checksums-t)
(package-test-verify-package-checksums-t/invalid-fails)
(package-test-verify-package-checksums-t/missing-fails)
(package-test-verify-package-checksums-all)
(package-test-verify-package-checksums-all/invalid-fails)
(package-test-verify-package-checksums-all/missing-fails)
(package-test-verify-package-checksums-all/no-supported-hash-fails)
(package-test-verify-package-checksums-all/ignore-unsupported)
(package-test-verify-package-size): New tests for the checksum
support.

* test/lisp/emacs-lisp/package-resources/archive-contents:
* test/lisp/emacs-lisp/package-resources/checksum-invalid-1.0.el:
* test/lisp/emacs-lisp/package-resources/checksum-valid-123.el:
* test/lisp/emacs-lisp/package-resources/checksum-valid-tar-0.99.tar:
* test/lisp/emacs-lisp/package-resources/checksum-valid-tar-0.99.tar:
New test data files.

* doc/emacs/package.texi (Package Installation): Document package
checksum checking.
* etc/NEWS: Announce it.

12 files changed:
doc/emacs/package.texi
etc/NEWS
lisp/emacs-lisp/package-x.el
lisp/emacs-lisp/package.el
test/lisp/emacs-lisp/package-resources/archive-contents
test/lisp/emacs-lisp/package-resources/checksum-invalid-1.0.el [new file with mode: 0644]
test/lisp/emacs-lisp/package-resources/checksum-invalid-tar-0.1.tar [new file with mode: 0644]
test/lisp/emacs-lisp/package-resources/checksum-valid-123.el [new file with mode: 0644]
test/lisp/emacs-lisp/package-resources/checksum-valid-tar-0.99.tar [new file with mode: 0644]
test/lisp/emacs-lisp/package-resources/wrong-size-single-1.0.el [new file with mode: 0644]
test/lisp/emacs-lisp/package-resources/wrong-size-tar-1.0.tar [new file with mode: 0644]
test/lisp/emacs-lisp/package-tests.el

index 4981dd50c7527c5215141decf2df0c653de71c32..9a583561903f2ea1ae2b4bb92c13c1e979c10273 100644 (file)
@@ -338,20 +338,41 @@ name of the package archive directory.  You can alter this list if you
 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.
@@ -361,8 +382,6 @@ subdirectory of @code{package-user-dir}, which causes Emacs to invoke
 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
index 9361cff38691b67836da3ae61d9c1a3635956a4a..da18848bc4a3b28dfb356c696dc330a540d4d82a 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -867,6 +867,21 @@ See the new user options 'package-name-column-width',
 '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
 
 +++
index c01b6efd4efe12bedc68008568c646afd78fcc4c..964cf07cd6e28eb3811f73bc58817fc6827d7980 100644 (file)
@@ -219,7 +219,9 @@ if it exists."
          (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))))
index 9c37ce429a7bcea62a3873fd971042aa28f9fb02..308f9eb3a6367babe69da0c616c120b98279add7 100644 (file)
@@ -335,6 +335,31 @@ default directory."
   :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:
@@ -417,6 +442,14 @@ synchronously."
   :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.
@@ -449,6 +482,8 @@ synchronously."
                                  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))
@@ -486,6 +521,13 @@ Slots:
 
 `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
@@ -495,7 +537,9 @@ Slots:
   archive
   dir
   extras
-  signed)
+  signed
+  size
+  checksums)
 
 (defun package--from-builtin (bi-desc)
   "Create a `package-desc' object from BI-DESC.
@@ -558,6 +602,13 @@ Signal an error if the kind is none of the above."
     ('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
@@ -1334,7 +1385,88 @@ errors signaled by ERROR-FORM or by BODY).
                    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.
@@ -1461,14 +1593,19 @@ the table."
                 (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.
@@ -1506,10 +1643,14 @@ Also, add the originating archive to the `package-desc' structure."
            :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
@@ -1979,9 +2120,10 @@ if all the in-between dependencies are also in PACKAGE-LIST."
   (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))
index e2f92304f8628d3e2283b96d11062aa1ac0e0afb..724972dd6b57cd9badd0ef5fb4546a773b8c90ba 100644 (file)
  (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]))
diff --git a/test/lisp/emacs-lisp/package-resources/checksum-invalid-1.0.el b/test/lisp/emacs-lisp/package-resources/checksum-invalid-1.0.el
new file mode 100644 (file)
index 0000000..c3d4790
--- /dev/null
@@ -0,0 +1,17 @@
+;;; 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
diff --git a/test/lisp/emacs-lisp/package-resources/checksum-invalid-tar-0.1.tar b/test/lisp/emacs-lisp/package-resources/checksum-invalid-tar-0.1.tar
new file mode 100644 (file)
index 0000000..8adc7f5
Binary files /dev/null and b/test/lisp/emacs-lisp/package-resources/checksum-invalid-tar-0.1.tar differ
diff --git a/test/lisp/emacs-lisp/package-resources/checksum-valid-123.el b/test/lisp/emacs-lisp/package-resources/checksum-valid-123.el
new file mode 100644 (file)
index 0000000..8cfc387
--- /dev/null
@@ -0,0 +1,17 @@
+;;; 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
diff --git a/test/lisp/emacs-lisp/package-resources/checksum-valid-tar-0.99.tar b/test/lisp/emacs-lisp/package-resources/checksum-valid-tar-0.99.tar
new file mode 100644 (file)
index 0000000..e468754
Binary files /dev/null and b/test/lisp/emacs-lisp/package-resources/checksum-valid-tar-0.99.tar differ
diff --git a/test/lisp/emacs-lisp/package-resources/wrong-size-single-1.0.el b/test/lisp/emacs-lisp/package-resources/wrong-size-single-1.0.el
new file mode 100644 (file)
index 0000000..a4e3daf
--- /dev/null
@@ -0,0 +1 @@
+;; This file just has the wrong size (i.e. not 1 as specified).
diff --git a/test/lisp/emacs-lisp/package-resources/wrong-size-tar-1.0.tar b/test/lisp/emacs-lisp/package-resources/wrong-size-tar-1.0.tar
new file mode 100644 (file)
index 0000000..61d47c6
Binary files /dev/null and b/test/lisp/emacs-lisp/package-resources/wrong-size-tar-1.0.tar differ
index 23267545f830ab3d29a30751e920e268b69ed817..a81506d626b0e89525e48a48332831f5741e3623 100644 (file)
@@ -44,6 +44,9 @@
 
 (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.")
 
@@ -304,14 +307,33 @@ Must called from within a `tar-mode' buffer."
   (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."
@@ -389,8 +411,8 @@ Must called from within a `tar-mode' buffer."
     ;;       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 ()
@@ -416,7 +438,7 @@ Must called from within a `tar-mode' buffer."
     (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"))))
 
@@ -671,6 +693,169 @@ Must called from within a `tar-mode' buffer."
                "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.
@@ -684,7 +869,9 @@ Must called from within a `tar-mode' buffer."
                               '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
@@ -693,7 +880,9 @@ Must called from within a `tar-mode' buffer."
                               "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 ()