aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/lispref/package.texi23
-rw-r--r--etc/NEWS5
-rw-r--r--lisp/calendar/iso8601.el1
-rw-r--r--lisp/emacs-lisp/package.el105
-rw-r--r--test/lisp/emacs-lisp/package-resources/archives/newer/archive-contents1
-rw-r--r--test/lisp/emacs-lisp/package-resources/archives/older/archive-contents1
-rw-r--r--test/lisp/emacs-lisp/package-tests.el61
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
332for packages.) So you should ensure that your archive is 332for packages.) So you should ensure that your archive is
333well-maintained and keep the hosting system secure. 333well-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
336them using a cryptographic key. If you have generated a 336package checksums in the package metadata file
337private/public gpg key pair, you can use gpg to sign the package like 337@file{archive-contents}. You should also @dfn{sign} the package
338this: 338metadata file using a cryptographic key. Finally, it is important to
339include creation and expiration timestamps information in that file.
340
341 Signing individual packages is also supported, but considered
342obsolete. It provides less security than package checksums, signing
343the @file{archive-contents} file, and creation and expiration
344timestamps does when used together. More specifically, signing
345individual packages does not protect against ``replay attacks''. Note
346that distributing signatures for individual packages is still
347recommended to support Emacs versions older than 28.1.
348
349 If you have generated a private/public gpg key pair, you can use gpg
350to 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
371of 'package-desc' structures (see @file{package.el}), except the first 383of 'package-desc' structures (see @file{package.el}), except the first
372element of the list is the archive version. 384element of the list is the archive version.
373 385
386@item archive-contents.sig
387Return the signature for @file{archive-contents}.
388
374@item <package name>-readme.txt 389@item <package name>-readme.txt
375Return the long description of the package. 390Return the long description of the package.
376 391
diff --git a/etc/NEWS b/etc/NEWS
index da18848bc4a..ead26984623 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -882,6 +882,11 @@ For improved security, you might want to set this to 't' or
882before setting these values, or you will be unable to install 882before setting these values, or you will be unable to install
883packages. 883packages.
884 884
885*** Support expiration of package archive metadata.
886When a package archive distributes a last-updated and expiration
887timestamp, they will automatically be used to verify that distributed
888packages 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
366Note that setting this to nil is intended for debugging, and
367should 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.
365More specifically the value can be: 374More 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.
1827This function assumes that the current buffer contains the
1828\"archive-contents\" file.
1829
1830A valid header looks like: \";; HEADER: <TIMESTAMP>\"
1831
1832Where <TIMESTAMP> is a valid ISO-8601 (RFC 3339) date. If there
1833is such a line but <TIMESTAMP> is invalid, show a warning and
1834return 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.
1854Signal error otherwise.
1855Warn 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.
1888Signal 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.
1899Compare the archive timestamp of the previously downloaded
1900\"archive-contents\" file to the timestamp in the current buffer.
1901Signal error if the old timestamp is more recent than the new one.
1902
1903Do nothing if there is no previously downloaded file, if such a
1904file 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.
1817ARCHIVE should be a cons cell of the form (NAME . LOCATION), 1921ARCHIVE 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