Skip to content

Commit 8220bd1

Browse files
committed
OpenPGP/SHA checking updates.
- Rework package layout - Check client and version updates - Make missing keys an error
1 parent ead3458 commit 8220bd1

File tree

9 files changed

+154
-77
lines changed

9 files changed

+154
-77
lines changed

quicklisp/checked-fetch.lisp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
;;;; checked-fetch.lisp
2+
3+
(in-package #:ql-http)
4+
5+
(define-condition verification-error (error) ())
6+
(define-condition unexpected-sha256-error (verification-error) ())
7+
(define-condition signature-verification-error (verification-error) ())
8+
9+
(defun openpgp-signature-url (url)
10+
(format nil "~A.asc" url))
11+
12+
(defun fetch-digest-checked (url output expected-digest &key quietly)
13+
"Fetch the data at URL and save to the file OUTPUT. The
14+
EXPECTED-DIGEST value of OBJECT is checked against the actual SHA256
15+
digest of the retrieved file, and if they match, the output file is
16+
returned, otherwise an UNEXPECTED-SHA256-ERROR is signaled."
17+
(with-temp-output-file (file "digest-checked.dat")
18+
(fetch url file :quietly quietly)
19+
(let ((actual-digest (file-sha-string 'sha256 file)))
20+
(unless (equalp actual-digest expected-digest)
21+
(error 'unexpected-sha256-error)))
22+
(rename-mundanely file output)
23+
(probe-file output)))
24+
25+
(defun fetch-openpgp-checked (url output &key quietly)
26+
(with-temp-output-files ((file "openpgp-checked.dat")
27+
(sig "openpgp-signature.asc"))
28+
(let ((sig-url (openpgp-signature-url url)))
29+
(fetch sig-url sig :quietly quietly)
30+
(fetch url file :quietly quietly)
31+
(let* ((signature (ql-openpgp:load-signature sig))
32+
(id (ql-openpgp:key-id-string signature))
33+
(key (ql-openpgp:find-key id)))
34+
(unless key
35+
(error "No key available for id ~S" id))
36+
(let ((result (ql-openpgp:verify-signature file signature key)))
37+
(unless result
38+
(error "Signature failed for file ~A"
39+
output))
40+
(unless quietly
41+
(format t "~&; Signature check result: ~A~%" result)))
42+
(rename-mundanely file output)))))

quicklisp/client-info.lisp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@
8383
(badly-sized-client-file-expected-size condition)
8484
(badly-sized-client-file-actual-size condition)))))
8585

86+
(define-condition sha-mismatched-client-file (invalid-client-file)
87+
()
88+
(:report (lambda (condition stream)
89+
(format stream "SHA digest mismatch on client file ~A"
90+
(invalid-client-file-file condition)))))
91+
8692
(defun check-client-file-size (file expected-size)
8793
(let ((actual-size (file-size file)))
8894
(unless (eql expected-size actual-size)
@@ -99,6 +105,10 @@
99105
metadata in CLIENT-FILE-INFO.")
100106
(:method (file client-file-info)
101107
(check-client-file-size file (size client-file-info))
108+
(let ((actual-sha (ql-sha:file-sha-string 'sha256 file ))
109+
(expected-sha (sha256 client-file-info)))
110+
(unless (equalp expected-sha actual-sha)
111+
(error "SHA mismatch on ~A" client-file-info)))
102112
client-file-info))
103113

104114
;;; Structuring and loading information about the Quicklisp client
@@ -207,9 +217,8 @@
207217
'client-tar-file-info))))
208218

209219
(defun fetch-client-info (url)
210-
(let ((info-file (qmerge "tmp/client-info.sexp")))
211-
(delete-file-if-exists info-file)
212-
(fetch url info-file :quietly t)
220+
(with-temp-output-file (info-file "client-info.sexp")
221+
(ql-http:fetch-openpgp-checked url info-file :quietly t)
213222
(handler-case
214223
(load-client-info info-file)
215224
;; FIXME: So many other things could go wrong here; I think it

quicklisp/client-update.lisp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
(in-package #:quicklisp-client)
44

55
(defun fetch-client-file-info (client-file-info output-file)
6-
(maybe-fetch-gzipped (file-url client-file-info) output-file)
7-
(check-client-file output-file client-file-info)
8-
(probe-file output-file))
6+
(with-temp-output-file (temp "client-info.dat")
7+
(maybe-fetch-gzipped (file-url client-file-info) temp)
8+
(check-client-file temp client-file-info)
9+
(rename-mundanely temp output-file)
10+
(probe-file output-file)))
911

1012
(defun retirement-directory (base)
1113
(let ((suffix 0))

quicklisp/dist-update.lisp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
(delete-directory-tree (qmerge "tmp/distinfo-update/")))
3131
(when url
3232
(ensure-directories-exist target)
33-
(fetch url target :quietly t)
33+
(fetch-openpgp-checked url target :quietly t)
3434
(let ((new (make-dist-from-file target)))
3535
(setf (base-directory new)
3636
(make-pathname :name nil
@@ -135,7 +135,7 @@
135135
(let ((temp-file (qmerge "tmp/install-dist-distinfo.txt")))
136136
(ensure-directories-exist temp-file)
137137
(delete-file-if-exists temp-file)
138-
(fetch url temp-file)
138+
(fetch-openpgp-checked url temp-file)
139139
(let* ((new-dist (make-dist-from-file temp-file))
140140
(old-dist (find-dist (name new-dist))))
141141
(when old-dist

quicklisp/dist.lisp

Lines changed: 37 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -456,33 +456,10 @@
456456
(setf (available-versions-url dist)
457457
(make-versions-url (distinfo-subscription-url dist))))
458458

459-
(defun fetch-signed-index-file (url target)
460-
(let ((signature-url (format nil "~A.asc" url))
461-
(temp-file (temp-output-file "index.txt"))
462-
(temp-signature-file (temp-output-file "signature.asc")))
463-
(unwind-protect
464-
(progn
465-
(fetch signature-url temp-signature-file)
466-
(fetch url temp-file)
467-
(let* ((signature (ql-openpgp:load-signature temp-signature-file))
468-
(id (ql-openpgp:key-id-string signature))
469-
(key (ql-openpgp:find-key id)))
470-
(unless key
471-
(error "No key available for id ~S" id))
472-
(let ((result (ql-openpgp:verify-signature temp-file signature key)))
473-
(unless result
474-
(error "Signature failed for index file ~A"
475-
target))
476-
(format t "~&; Signature check result: ~A~%" result)))
477-
(rename-file temp-file target)
478-
(delete-file temp-signature-file))
479-
(delete-file-if-exists temp-file)
480-
(delete-file-if-exists temp-signature-file))))
481-
482459
(defmethod ensure-system-index-file ((dist dist))
483460
(let ((pathname (relative-to dist "systems.txt")))
484461
(or (probe-file pathname)
485-
(fetch-signed-index-file (system-index-url dist) pathname))))
462+
(fetch-openpgp-checked (system-index-url dist) pathname))))
486463

487464
(defmethod ensure-system-cdb-file ((dist dist))
488465
(let* ((system-file (ensure-system-index-file dist))
@@ -495,7 +472,7 @@
495472
(defmethod ensure-release-index-file ((dist dist))
496473
(let ((pathname (relative-to dist "releases.txt")))
497474
(or (probe-file pathname)
498-
(fetch-signed-index-file (release-index-url dist) pathname))))
475+
(fetch-openpgp-checked (release-index-url dist) pathname))))
499476

500477
(defmethod ensure-release-cdb-file ((dist dist))
501478
(let* ((release-file (ensure-release-index-file dist))
@@ -752,6 +729,21 @@ the given NAME."
752729
(file-namestring (invalid-local-archive-file condition))
753730
(name (invalid-local-archive-release condition))))))
754731

732+
(defmethod archive-digest ((release release))
733+
(let* ((dist (dist release))
734+
(index (relative-to dist "digests.txt"))
735+
(cdb (relative-to dist "digests.cdb"))
736+
(key (format nil "release/~A" (name release))))
737+
(unless (probe-file index)
738+
(error "Digest index file is missing"))
739+
(unless (probe-file cdb)
740+
(ql-cdb:convert-index-file index :cdb-file cdb))
741+
(let ((value (ql-cdb:lookup key cdb)))
742+
(destructuring-bind (k sha256)
743+
(split-spaces value)
744+
(declare (ignore k))
745+
sha256))))
746+
755747
(defmethod check-local-archive-file ((release release))
756748
(let ((file (local-archive-file release)))
757749
(unless (probe-file file)
@@ -787,7 +779,9 @@ the given NAME."
787779
(or (probe-file pathname)
788780
(progn
789781
(ensure-directories-exist pathname)
790-
(fetch (archive-url release) pathname)))
782+
(fetch-digest-checked (archive-url release)
783+
pathname
784+
(archive-digest release))))
791785
(restart-case
792786
(check-local-archive-file release)
793787
(delete-and-retry (&optional v)
@@ -871,16 +865,6 @@ the given NAME."
871865
(unless (ignorable line)
872866
(funcall fun line))))))
873867

874-
875-
(defmethod archive-digest ((release release))
876-
(let ((entry (cdb-lookup (dist release) (name release)
877-
"archive-digests.cdb")))
878-
(unless entry
879-
(error "No digest indexed for ~S. That's pretty weird!" release))
880-
(let ((digest-position (position #\Space entry)))
881-
(subseq entry (1+ digest-position)))))
882-
883-
884868
(defmethod slot-unbound (class (dist dist) (slot (eql 'release-index)))
885869
(declare (ignore class))
886870
(setf (slot-value dist 'release-index)
@@ -1177,23 +1161,23 @@ FUN."
11771161
;;;
11781162

11791163
(defmethod available-versions ((dist dist))
1180-
(let ((temp (qmerge "tmp/dist-versions.txt"))
1181-
(versions '())
1182-
(url (available-versions-url dist)))
1183-
(when url
1184-
(ensure-directories-exist temp)
1185-
(delete-file-if-exists temp)
1186-
(handler-case
1187-
(fetch url temp)
1188-
(unexpected-http-status ()
1189-
(return-from available-versions nil)))
1190-
(with-open-file (stream temp)
1191-
(loop for line = (read-line stream nil)
1192-
while line do
1193-
(destructuring-bind (version url)
1194-
(split-spaces line)
1195-
(setf versions (acons version url versions)))))
1196-
versions)))
1164+
(with-temp-output-file (temp "dist-versions.txt")
1165+
(let ((versions '())
1166+
(url (available-versions-url dist)))
1167+
(when url
1168+
(ensure-directories-exist temp)
1169+
(delete-file-if-exists temp)
1170+
(handler-case
1171+
(fetch-openpgp-checked url temp)
1172+
(unexpected-http-status ()
1173+
(return-from available-versions nil)))
1174+
(with-open-file (stream temp)
1175+
(loop for line = (read-line stream nil)
1176+
while line do
1177+
(destructuring-bind (version url)
1178+
(split-spaces line)
1179+
(setf versions (acons version url versions)))))
1180+
versions))))
11971181

11981182

11991183
;;;

quicklisp/fetch-gzipped.lisp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
(defun fetch-gzipped-version (url file &key quietly)
1010
(let ((gzipped (gzipped-url url))
1111
(gzipped-temp (merge-pathnames "gzipped.tmp" file)))
12-
(fetch gzipped gzipped-temp :quietly quietly)
12+
(fetch-openpgp-checked gzipped gzipped-temp :quietly quietly)
1313
(gunzip gzipped-temp file)
1414
(delete-file-if-exists gzipped-temp)
1515
(probe-file file)))

quicklisp/package.lisp

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
(:export #:write-line-to-file
88
#:without-prompting
99
#:press-enter-to-continue
10+
#:rename-mundanely
1011
#:replace-file
1112
#:copy-file
1213
#:delete-file-if-exists
@@ -18,7 +19,9 @@
1819
#:safely-read-file
1920
#:make-versions-url
2021
#:random-pathname-string
21-
#:temp-output-file))
22+
#:temp-output-file
23+
#:with-temp-output-file
24+
#:with-temp-output-files))
2225

2326
(defpackage #:ql-setup
2427
(:documentation
@@ -93,10 +96,25 @@
9396
#:update-progress
9497
#:finish-display))
9598

99+
(defpackage #:ql-sha
100+
(:documentation
101+
"Compute SHA digests. Used for verification.")
102+
(:use #:cl)
103+
(:export #:file-sha
104+
#:file-sha-string
105+
#:sha1
106+
#:sha256
107+
#:sha512)
108+
(:export #:update-sha-from-file
109+
#:update-sha
110+
#:finish-sha))
111+
96112
(defpackage #:ql-http
97113
(:documentation
98114
"A simple HTTP client.")
99-
(:use #:cl #:ql-network #:ql-progress #:ql-config)
115+
(:use #:cl
116+
#:ql-network #:ql-progress #:ql-config #:ql-util
117+
#:ql-sha)
100118
(:export #:*proxy-url*
101119
#:fetch
102120
#:http-fetch
@@ -108,13 +126,17 @@
108126
#:url
109127
#:*maximum-redirects*
110128
#:*default-url-defaults*)
129+
(:export #:fetch-digest-checked
130+
#:fetch-openpgp-checked)
111131
(:export #:fetch-error
112132
#:unexpected-http-status
113133
#:unexpected-http-status-code
114134
#:unexpected-http-status-url
115135
#:too-many-redirects
116136
#:too-many-redirects-url
117-
#:too-many-redirects-count))
137+
#:too-many-redirects-count
138+
#:digest-check-error
139+
#:openpgp-check-error))
118140

119141
(defpackage #:ql-minitar
120142
(:documentation
@@ -136,18 +158,7 @@
136158
#:map-cdb
137159
#:convert-index-file))
138160

139-
(defpackage #:ql-sha
140-
(:documentation
141-
"Compute SHA digests. Used for verification.")
142-
(:use #:cl)
143-
(:export #:file-sha
144-
#:file-sha-string
145-
#:sha1
146-
#:sha256
147-
#:sha512)
148-
(:export #:update-sha-from-file
149-
#:update-sha
150-
#:finish-sha))
161+
151162

152163
(defpackage #:ql-openpgp
153164
(:documentation

quicklisp/quicklisp.asd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
(:file "sha")
2828
(:file "openpgp")
2929
(:file "key-management")
30+
(:file "checked-fetch")
3031
(:file "dist")
3132
(:file "setup")
3233
(:file "client")

quicklisp/utils.lisp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
(let ((result (read-line *query-io*)))
2626
(zerop (length result))))
2727

28+
(defun rename-mundanely (from to)
29+
"Renames file FROM to TO, but inhibit's CL:RENAME-FILE's merging behavior."
30+
(setf from (merge-pathnames from (make-pathname :type :unspecific
31+
:name :unspecific
32+
:version :unspecific)))
33+
(rename-file from to))
34+
2835
(defun replace-file (from to)
2936
"Like RENAME-FILE, but deletes TO if it exists, first."
3037
(when (probe-file to)
@@ -142,3 +149,24 @@ http://foo/bar-versions.txt."
142149
(merge-pathnames (make-pathname :name temp-name
143150
:type (pathname-type template-pathname))
144151
(ql-setup:qmerge "tmp/"))))
152+
153+
(defun call-with-temp-output-file (template-pathname fun)
154+
(let ((file (temp-output-file template-pathname)))
155+
(ensure-directories-exist file)
156+
(unwind-protect
157+
(funcall fun file)
158+
(delete-file-if-exists file))))
159+
160+
(defmacro with-temp-output-file ((var template-pathname) &body body)
161+
`(call-with-temp-output-file ,template-pathname (lambda (,var) ,@body)))
162+
163+
(defmacro with-temp-output-files (bindings &body body)
164+
(labels ((expand (bindings body)
165+
(let ((binding (first bindings)))
166+
(if (rest bindings)
167+
`(with-temp-output-file (,(first binding) ,(second binding))
168+
,(expand (rest bindings) body))
169+
`(with-temp-output-file (,(first binding) ,(second binding))
170+
,@body)))))
171+
(expand bindings body)))
172+

0 commit comments

Comments
 (0)