diff options
| -rw-r--r-- | doc/lispref/package.texi | 23 | ||||
| -rw-r--r-- | etc/NEWS | 5 | ||||
| -rw-r--r-- | lisp/calendar/iso8601.el | 1 | ||||
| -rw-r--r-- | lisp/emacs-lisp/package.el | 105 | ||||
| -rw-r--r-- | test/lisp/emacs-lisp/package-resources/archives/newer/archive-contents | 1 | ||||
| -rw-r--r-- | test/lisp/emacs-lisp/package-resources/archives/older/archive-contents | 1 | ||||
| -rw-r--r-- | test/lisp/emacs-lisp/package-tests.el | 61 |
7 files changed, 193 insertions, 4 deletions
diff --git a/doc/lispref/package.texi b/doc/lispref/package.texi index af87479c7d2..725fecd8952 100644 --- a/doc/lispref/package.texi +++ b/doc/lispref/package.texi | |||
| @@ -332,10 +332,22 @@ installing user. (This is true for Emacs code in general, not just | |||
| 332 | for packages.) So you should ensure that your archive is | 332 | for packages.) So you should ensure that your archive is |
| 333 | well-maintained and keep the hosting system secure. | 333 | well-maintained and keep the hosting system secure. |
| 334 | 334 | ||
| 335 | One way to increase the security of your packages is to @dfn{sign} | 335 | To increase the security of your packages, you should distribute |
| 336 | them using a cryptographic key. If you have generated a | 336 | package checksums in the package metadata file |
| 337 | private/public gpg key pair, you can use gpg to sign the package like | 337 | @file{archive-contents}. You should also @dfn{sign} the package |
| 338 | this: | 338 | metadata file using a cryptographic key. Finally, it is important to |
| 339 | include creation and expiration timestamps information in that file. | ||
| 340 | |||
| 341 | Signing individual packages is also supported, but considered | ||
| 342 | obsolete. It provides less security than package checksums, signing | ||
| 343 | the @file{archive-contents} file, and creation and expiration | ||
| 344 | timestamps does when used together. More specifically, signing | ||
| 345 | individual packages does not protect against ``replay attacks''. Note | ||
| 346 | that distributing signatures for individual packages is still | ||
| 347 | recommended to support Emacs versions older than 28.1. | ||
| 348 | |||
| 349 | If you have generated a private/public gpg key pair, you can use gpg | ||
| 350 | to sign a package or the @file{archive-contents} file like this: | ||
| 339 | 351 | ||
| 340 | @c FIXME EasyPG / package-x way to do this. | 352 | @c FIXME EasyPG / package-x way to do this. |
| 341 | @example | 353 | @example |
| @@ -371,6 +383,9 @@ Return a lisp form describing the archive contents. The form is a list | |||
| 371 | of 'package-desc' structures (see @file{package.el}), except the first | 383 | of 'package-desc' structures (see @file{package.el}), except the first |
| 372 | element of the list is the archive version. | 384 | element of the list is the archive version. |
| 373 | 385 | ||
| 386 | @item archive-contents.sig | ||
| 387 | Return the signature for @file{archive-contents}. | ||
| 388 | |||
| 374 | @item <package name>-readme.txt | 389 | @item <package name>-readme.txt |
| 375 | Return the long description of the package. | 390 | Return the long description of the package. |
| 376 | 391 | ||
| @@ -882,6 +882,11 @@ For improved security, you might want to set this to 't' or | |||
| 882 | before setting these values, or you will be unable to install | 882 | before setting these values, or you will be unable to install |
| 883 | packages. | 883 | packages. |
| 884 | 884 | ||
| 885 | *** Support expiration of package archive metadata. | ||
| 886 | When a package archive distributes a last-updated and expiration | ||
| 887 | timestamp, they will automatically be used to verify that distributed | ||
| 888 | packages are not out of date. | ||
| 889 | |||
| 885 | ** gdb-mi | 890 | ** gdb-mi |
| 886 | 891 | ||
| 887 | +++ | 892 | +++ |
diff --git a/lisp/calendar/iso8601.el b/lisp/calendar/iso8601.el index 906c29b15f4..7a6f14a858f 100644 --- a/lisp/calendar/iso8601.el +++ b/lisp/calendar/iso8601.el | |||
| @@ -114,6 +114,7 @@ | |||
| 114 | iso8601--duration-week-match | 114 | iso8601--duration-week-match |
| 115 | iso8601--duration-combined-match))) | 115 | iso8601--duration-combined-match))) |
| 116 | 116 | ||
| 117 | ;;;###autoload | ||
| 117 | (defun iso8601-parse (string &optional form) | 118 | (defun iso8601-parse (string &optional form) |
| 118 | "Parse an ISO 8601 date/time string and return a `decode-time' structure. | 119 | "Parse an ISO 8601 date/time string and return a `decode-time' structure. |
| 119 | 120 | ||
diff --git a/lisp/emacs-lisp/package.el b/lisp/emacs-lisp/package.el index 308f9eb3a63..1e73a1690cc 100644 --- a/lisp/emacs-lisp/package.el +++ b/lisp/emacs-lisp/package.el | |||
| @@ -360,6 +360,15 @@ should normally not be used since it will decrease security." | |||
| 360 | :risky t | 360 | :risky t |
| 361 | :version "28.1") | 361 | :version "28.1") |
| 362 | 362 | ||
| 363 | (defcustom package-check-timestamp t | ||
| 364 | "Non-nil means to verify the package archive timestamp. | ||
| 365 | |||
| 366 | Note that setting this to nil is intended for debugging, and | ||
| 367 | should normally not be used since it will decrease security." | ||
| 368 | :type 'boolean | ||
| 369 | :risky t | ||
| 370 | :version "28.1") | ||
| 371 | |||
| 363 | (defcustom package-check-signature 'allow-unsigned | 372 | (defcustom package-check-signature 'allow-unsigned |
| 364 | "Non-nil means to check package signatures when installing. | 373 | "Non-nil means to check package signatures when installing. |
| 365 | More specifically the value can be: | 374 | More specifically the value can be: |
| @@ -449,6 +458,7 @@ synchronously." | |||
| 449 | (define-error 'bad-size "Package size mismatch" 'package-error) | 458 | (define-error 'bad-size "Package size mismatch" 'package-error) |
| 450 | (define-error 'bad-signature "Failed to verify signature" 'package-error) | 459 | (define-error 'bad-signature "Failed to verify signature" 'package-error) |
| 451 | (define-error 'bad-checksum "Failed to verify checksum" 'package-error) | 460 | (define-error 'bad-checksum "Failed to verify checksum" 'package-error) |
| 461 | (define-error 'bad-timestamp "Failed to verify timestamp" 'package-error) | ||
| 452 | 462 | ||
| 453 | 463 | ||
| 454 | ;;; `package-desc' object definition | 464 | ;;; `package-desc' object definition |
| @@ -1812,6 +1822,100 @@ Once it's empty, run `package--post-download-archives-hook'." | |||
| 1812 | (message "Package refresh done") | 1822 | (message "Package refresh done") |
| 1813 | (run-hooks 'package--post-download-archives-hook))) | 1823 | (run-hooks 'package--post-download-archives-hook))) |
| 1814 | 1824 | ||
| 1825 | (defun package--parse-header-from-buffer (header name) | ||
| 1826 | "Find and return \"archive-contents\" HEADER for archive NAME. | ||
| 1827 | This function assumes that the current buffer contains the | ||
| 1828 | \"archive-contents\" file. | ||
| 1829 | |||
| 1830 | A valid header looks like: \";; HEADER: <TIMESTAMP>\" | ||
| 1831 | |||
| 1832 | Where <TIMESTAMP> is a valid ISO-8601 (RFC 3339) date. If there | ||
| 1833 | is such a line but <TIMESTAMP> is invalid, show a warning and | ||
| 1834 | return nil. If there is no valid header, return nil." | ||
| 1835 | (save-excursion | ||
| 1836 | (goto-char (point-min)) | ||
| 1837 | (when (re-search-forward (concat "^;; " header ": *\\(.+?\\) *$") nil t) | ||
| 1838 | (condition-case-unless-debug nil | ||
| 1839 | (encode-time (iso8601-parse (match-string 1))) | ||
| 1840 | (lwarn '(package timestamp) | ||
| 1841 | (list (format "Malformed timestamp for archive `%s': `%s'" | ||
| 1842 | name (match-string 1)))))))) | ||
| 1843 | |||
| 1844 | (defun package--parse-valid-until-from-buffer (name) | ||
| 1845 | "Find and return \"Valid-Until\" header for archive NAME." | ||
| 1846 | (package--parse-header-from-buffer "Valid-Until" name)) | ||
| 1847 | |||
| 1848 | (defun package--parse-last-updated-from-buffer (name) | ||
| 1849 | "Find and return \"Last-Updated\" header for archive NAME." | ||
| 1850 | (package--parse-header-from-buffer "Last-Updated" name)) | ||
| 1851 | |||
| 1852 | (defun package--archive-verify-timestamp (new old name) | ||
| 1853 | "Return t if timestamp NEW is more recent than OLD for archive NAME. | ||
| 1854 | Signal error otherwise. | ||
| 1855 | Warn if NEW is in the future." | ||
| 1856 | ;; If timestamp is missing on cached (old) file, do nothing here. | ||
| 1857 | ;; This package archive recently introduced support for timestamps. | ||
| 1858 | ;; We will require a timestamp for that archive in future updates. | ||
| 1859 | (if old | ||
| 1860 | (cond | ||
| 1861 | ((not new) | ||
| 1862 | (signal 'bad-timestamp | ||
| 1863 | (list (format-message | ||
| 1864 | (concat | ||
| 1865 | "New archive contents for `%s' missing " | ||
| 1866 | "timestamp, refusing to proceed") | ||
| 1867 | name)))) | ||
| 1868 | ((time-less-p new old) | ||
| 1869 | (signal 'bad-timestamp | ||
| 1870 | (list (format-message | ||
| 1871 | (concat | ||
| 1872 | "New archive contents for `%s' older than " | ||
| 1873 | "cached, refusing to proceed") | ||
| 1874 | name)))) | ||
| 1875 | ((time-less-p (current-time) new) | ||
| 1876 | (signal 'bad-timestamp | ||
| 1877 | (list (format-message | ||
| 1878 | (concat | ||
| 1879 | "New archive contents for `%s' is " | ||
| 1880 | "in the future: %s") | ||
| 1881 | name (format-time-string "%c" new))))) | ||
| 1882 | ;; Check ok, return t. | ||
| 1883 | (t)) | ||
| 1884 | t)) | ||
| 1885 | |||
| 1886 | (defun package--archive-verify-not-expired (timestamp name) | ||
| 1887 | "Return t if TIMESTAMP has not yet expired for archive NAME. | ||
| 1888 | Signal error otherwise." | ||
| 1889 | (unless (time-less-p (current-time) timestamp) | ||
| 1890 | (signal 'bad-timestamp | ||
| 1891 | (list (format-message | ||
| 1892 | (concat | ||
| 1893 | "Package archive `%s' has sent " | ||
| 1894 | "an expired `archive-contents' file") | ||
| 1895 | name))))) | ||
| 1896 | |||
| 1897 | (defun package--check-archive-timestamp (name) | ||
| 1898 | "Verify timestamp of \"archive-contents\" file for archive NAME. | ||
| 1899 | Compare the archive timestamp of the previously downloaded | ||
| 1900 | \"archive-contents\" file to the timestamp in the current buffer. | ||
| 1901 | Signal error if the old timestamp is more recent than the new one. | ||
| 1902 | |||
| 1903 | Do nothing if there is no previously downloaded file, if such a | ||
| 1904 | file exists but does not contain any timestamp, or if | ||
| 1905 | `package-check-timestamp' is nil." | ||
| 1906 | (let ((old-file (expand-file-name | ||
| 1907 | (concat "archives/" name "/archive-contents") | ||
| 1908 | package-user-dir))) | ||
| 1909 | (when (and package-check-timestamp | ||
| 1910 | (file-readable-p old-file)) | ||
| 1911 | (let ((old (with-temp-buffer | ||
| 1912 | (insert-file-contents old-file) | ||
| 1913 | (package--parse-last-updated-from-buffer name))) | ||
| 1914 | (new (package--parse-last-updated-from-buffer name)) | ||
| 1915 | (new-expires (package--parse-valid-until-from-buffer name))) | ||
| 1916 | (package--archive-verify-timestamp new old name) | ||
| 1917 | (package--archive-verify-not-expired new-expires name))))) | ||
| 1918 | |||
| 1815 | (defun package--download-one-archive (archive file &optional async) | 1919 | (defun package--download-one-archive (archive file &optional async) |
| 1816 | "Retrieve an archive file FILE from ARCHIVE, and cache it. | 1920 | "Retrieve an archive file FILE from ARCHIVE, and cache it. |
| 1817 | ARCHIVE should be a cons cell of the form (NAME . LOCATION), | 1921 | ARCHIVE should be a cons cell of the form (NAME . LOCATION), |
| @@ -1825,6 +1929,7 @@ similar to an entry in `package-alist'. Save the cached copy to | |||
| 1825 | (content (buffer-string)) | 1929 | (content (buffer-string)) |
| 1826 | (dir (expand-file-name (concat "archives/" name) package-user-dir)) | 1930 | (dir (expand-file-name (concat "archives/" name) package-user-dir)) |
| 1827 | (local-file (expand-file-name file dir))) | 1931 | (local-file (expand-file-name file dir))) |
| 1932 | (package--check-archive-timestamp name) | ||
| 1828 | (when (listp (read content)) | 1933 | (when (listp (read content)) |
| 1829 | (make-directory dir t) | 1934 | (make-directory dir t) |
| 1830 | (if (or (not (package-check-signature)) | 1935 | (if (or (not (package-check-signature)) |
diff --git a/test/lisp/emacs-lisp/package-resources/archives/newer/archive-contents b/test/lisp/emacs-lisp/package-resources/archives/newer/archive-contents new file mode 100644 index 00000000000..59a79970b6b --- /dev/null +++ b/test/lisp/emacs-lisp/package-resources/archives/newer/archive-contents | |||
| @@ -0,0 +1 @@ | |||
| ;; Last-Updated: 2020-06-01T00:00:00.000Z | |||
diff --git a/test/lisp/emacs-lisp/package-resources/archives/older/archive-contents b/test/lisp/emacs-lisp/package-resources/archives/older/archive-contents new file mode 100644 index 00000000000..193a6b5ab94 --- /dev/null +++ b/test/lisp/emacs-lisp/package-resources/archives/older/archive-contents | |||
| @@ -0,0 +1 @@ | |||
| ;; Last-Updated: 2019-01-01T00:00:00.000Z | |||
diff --git a/test/lisp/emacs-lisp/package-tests.el b/test/lisp/emacs-lisp/package-tests.el index a81506d626b..b0da54a3015 100644 --- a/test/lisp/emacs-lisp/package-tests.el +++ b/test/lisp/emacs-lisp/package-tests.el | |||
| @@ -857,6 +857,67 @@ If the rest succeed, just ignore the unsupported one." | |||
| 857 | (insert "7") | 857 | (insert "7") |
| 858 | (should-error (package--verify-package-size pkg-desc))))) | 858 | (should-error (package--verify-package-size pkg-desc))))) |
| 859 | 859 | ||
| 860 | (ert-deftest package-test-parse-valid-until-from-buffer () | ||
| 861 | (with-temp-buffer | ||
| 862 | (insert ";; Valid-Until: 2020-05-01T15:43:35.000Z\n(foo bar baz)") | ||
| 863 | (should (equal (package--parse-valid-until-from-buffer "foo") | ||
| 864 | '(24236 17319))))) | ||
| 865 | |||
| 866 | (ert-deftest package-test-parse-last-updated-from-buffer () | ||
| 867 | (with-temp-buffer | ||
| 868 | (insert ";; Last-Updated: 2020-05-01T15:43:35.000Z\n(foo bar baz)") | ||
| 869 | (should (equal (package--parse-last-updated-from-buffer "foo") | ||
| 870 | '(24236 17319))))) | ||
| 871 | |||
| 872 | (defun package-tests--parse-last-updated (timestamp) | ||
| 873 | (with-temp-buffer | ||
| 874 | (insert timestamp) | ||
| 875 | (package--parse-last-updated-from-buffer "test"))) | ||
| 876 | |||
| 877 | (ert-deftest package-test-archive-verify-timestamp () | ||
| 878 | (let ((a (package-tests--parse-last-updated | ||
| 879 | ";; Last-Updated: 2020-05-01T15:43:35.000Z\n")) | ||
| 880 | (b (package-tests--parse-last-updated | ||
| 881 | ";; Last-Updated: 2020-06-01T15:43:35.000Z\n")) | ||
| 882 | (c (package-tests--parse-last-updated | ||
| 883 | ";; Last-Updated: 2020-07-01T15:43:35.000Z\n"))) | ||
| 884 | (should (package--archive-verify-timestamp b nil "foo")) | ||
| 885 | (should (package--archive-verify-timestamp b a "foo")) | ||
| 886 | (should (package--archive-verify-timestamp c a "foo")) | ||
| 887 | (should (package--archive-verify-timestamp c b "foo")) | ||
| 888 | ;; Signal error. | ||
| 889 | (should-error (package--archive-verify-timestamp a b "foo") | ||
| 890 | :type 'bad-timestamp) | ||
| 891 | (should-error (package--archive-verify-timestamp a c "foo") | ||
| 892 | :type 'bad-timestamp) | ||
| 893 | (should-error (package--archive-verify-timestamp b c "foo") | ||
| 894 | :type 'bad-timestamp) | ||
| 895 | (should-error (package--archive-verify-timestamp nil a "foo") | ||
| 896 | :type 'bad-timestamp))) | ||
| 897 | |||
| 898 | (ert-deftest package-test-check-archive-timestamp () | ||
| 899 | (let ((package-user-dir package-test-data-dir)) | ||
| 900 | (with-temp-buffer | ||
| 901 | (insert ";; Last-Updated: 2020-01-01T00:00:00.000Z\n") | ||
| 902 | (package--check-archive-timestamp "older") | ||
| 903 | (package--check-archive-timestamp "missing") | ||
| 904 | (should-error (package--check-archive-timestamp "newer") | ||
| 905 | :type 'bad-timestamp)))) | ||
| 906 | |||
| 907 | (ert-deftest package-test-check-archive-timestamp/not-expired () | ||
| 908 | (let ((package-user-dir package-test-data-dir)) | ||
| 909 | (with-temp-buffer | ||
| 910 | (insert ";; Last-Updated: 2020-01-01T00:00:00.000Z\n" | ||
| 911 | ";; Valid-Until: 2999-01-02T00:00:00.000Z\n") | ||
| 912 | (should-not (package--check-archive-timestamp "older"))))) | ||
| 913 | |||
| 914 | (ert-deftest package-test-check-archive-timestamp/expired () | ||
| 915 | (let ((package-user-dir package-test-data-dir)) | ||
| 916 | (with-temp-buffer | ||
| 917 | (insert ";; Last-Updated: 2020-01-01T00:00:00.000Z\n" | ||
| 918 | ";; Valid-Until: 2020-01-02T00:00:00.000Z\n") | ||
| 919 | (should-error (package--check-archive-timestamp "older"))))) | ||
| 920 | |||
| 860 | 921 | ||
| 861 | ;;; Tests for package-x features. | 922 | ;;; Tests for package-x features. |
| 862 | 923 | ||