aboutsummaryrefslogtreecommitdiffstats
path: root/lisp
diff options
context:
space:
mode:
authorStefan Kangas2020-09-07 07:31:56 +0200
committerStefan Kangas2020-11-22 00:38:35 +0100
commitbcde5f86c5a7f3a84115807520631a4f12fb6f70 (patch)
tree6254f8d19ef474f965567ce7922b84ed93312a19 /lisp
parent733e674af4f66ba7e9f0614b931c44484acce2b9 (diff)
downloademacs-scratch/package-security.tar.gz
emacs-scratch/package-security.zip
Support expiration of metadata by package archivesscratch/package-security
Expiring package metadata is done by checking the timestamp in package archive file. This is intended to limit the effectiveness of a replay attack. The onus is on the package archives to implement a secure and reasonable policy. (Debian uses 7 days before metadata expires.) Together with package checksums, this adds sufficient protection against metadata replay attacks. (Bug#19479) * lisp/emacs-lisp/package.el (package-check-timestamp): New defcustom. (bad-timestamp): New error. (package--parse-header-from-buffer) (package--parse-valid-until-from-buffer) (package--parse-last-updated-from-buffer) (package--archive-verify-timestamp) (package--archive-verify-not-expired) (package--compare-archive-timestamps) (package--check-archive-timestamp): New defuns. (package--download-one-archive): Check timestamp of the 'archive-contents' file using above functions. It is only checked if it exists, which makes this change backwards compatible. * lisp/calendar/iso8601.el (iso8601-parse): Add autoload cookie. * test/lisp/emacs-lisp/package-tests.el (package-test-parse-valid-until-from-buffer) (package-test-parse-last-updated-from-buffer) (package-test-archive-verify-timestamp) (package-test-check-archive-timestamp) (package-test-check-archive-timestamp/not-expired) (package-test-check-archive-timestamp/expired): New tests. * test/lisp/emacs-lisp/package-resources/archives/older/archive-contents: * test/lisp/emacs-lisp/package-resources/archives/newer/archive-contents: New files. * doc/lispref/package.texi (Package Archives, Archive Web Server): Document how to increase the security of a package archive using checksums, signing and timestamps.
Diffstat (limited to 'lisp')
-rw-r--r--lisp/calendar/iso8601.el1
-rw-r--r--lisp/emacs-lisp/package.el105
2 files changed, 106 insertions, 0 deletions
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))