1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2020, 2022 Ludovic Courtès <ludo@gnu.org>
4 ;;; This file is part of GNU Guix.
6 ;;; GNU Guix is free software; you can redistribute it and/or modify it
7 ;;; under the terms of the GNU General Public License as published by
8 ;;; the Free Software Foundation; either version 3 of the License, or (at
9 ;;; your option) any later version.
11 ;;; GNU Guix is distributed in the hope that it will be useful, but
12 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ;;; GNU General Public License for more details.
16 ;;; You should have received a copy of the GNU General Public License
17 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19 (define-module (test-git-authenticate)
21 #:use-module (guix git)
22 #:use-module (guix git-authenticate)
23 #:use-module ((guix channels) #:select (openpgp-fingerprint))
24 #:use-module ((guix diagnostics)
25 #:select (formatted-message? formatted-message-arguments))
26 #:use-module (guix openpgp)
27 #:use-module ((guix tests) #:select (random-text))
28 #:use-module (guix tests git)
29 #:use-module (guix tests gnupg)
30 #:use-module (guix build utils)
31 #:use-module (srfi srfi-1)
32 #:use-module (srfi srfi-34)
33 #:use-module (srfi srfi-35)
34 #:use-module (srfi srfi-64)
35 #:use-module (rnrs bytevectors)
36 #:use-module (rnrs io ports))
38 ;; Test the (guix git-authenticate) tools.
40 (define (gpg+git-available?)
41 (and (which (git-command))
42 (which (gpg-command)) (which (gpgconf-command))))
45 (test-begin "git-authenticate")
47 (unless (which (git-command)) (test-skip 1))
48 (test-assert "unsigned commits"
49 (with-temporary-git-repository directory
51 (commit "first commit")
53 (commit "second commit"))
54 (with-repository directory repository
55 (let ((commit1 (find-commit repository "first"))
56 (commit2 (find-commit repository "second")))
57 (guard (c ((unsigned-commit-error? c)
58 (oid=? (git-authentication-error-commit c)
59 (commit-id commit1))))
60 (authenticate-commits repository (list commit1 commit2)
61 #:keyring-reference "master")
64 (unless (gpg+git-available?) (test-skip 1))
65 (test-assert "signed commits, SHA1 signature"
66 (with-fresh-gnupg-setup (list %ed25519-public-key-file
67 %ed25519-secret-key-file)
68 ;; Force use of SHA1 for signatures.
69 (call-with-output-file (string-append (getenv "GNUPGHOME") "/gpg.conf")
71 (display "digest-algo sha1" port)))
73 (with-temporary-git-repository directory
75 (add "signer.key" ,(call-with-input-file %ed25519-public-key-file
77 (add ".guix-authorizations"
79 `(authorizations (version 0)
80 ((,(key-fingerprint %ed25519-public-key-file)
82 (commit "first commit"
83 (signer ,(key-fingerprint %ed25519-public-key-file))))
84 (with-repository directory repository
85 (let ((commit (find-commit repository "first")))
86 (guard (c ((unsigned-commit-error? c)
87 (oid=? (git-authentication-error-commit c)
89 (authenticate-commits repository (list commit)
90 #:keyring-reference "master")
93 (unless (gpg+git-available?) (test-skip 1))
94 (test-assert "signed commits, default authorizations"
95 (with-fresh-gnupg-setup (list %ed25519-public-key-file
96 %ed25519-secret-key-file)
97 (with-temporary-git-repository directory
98 `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
100 (commit "zeroth commit")
102 (commit "first commit"
103 (signer ,(key-fingerprint %ed25519-public-key-file)))
105 (commit "second commit"
106 (signer ,(key-fingerprint %ed25519-public-key-file))))
107 (with-repository directory repository
108 (let ((commit1 (find-commit repository "first"))
109 (commit2 (find-commit repository "second")))
110 (authenticate-commits repository (list commit1 commit2)
111 #:default-authorizations
112 (list (openpgp-public-key-fingerprint
114 %ed25519-public-key-file)))
115 #:keyring-reference "master"))))))
117 (unless (gpg+git-available?) (test-skip 1))
118 (test-assert "signed commits, .guix-authorizations"
119 (with-fresh-gnupg-setup (list %ed25519-public-key-file
120 %ed25519-secret-key-file)
121 (with-temporary-git-repository directory
122 `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
124 (add ".guix-authorizations"
126 `(authorizations (version 0)
128 %ed25519-public-key-file)
129 (name "Charlie"))))))
130 (commit "zeroth commit")
132 (commit "first commit"
133 (signer ,(key-fingerprint %ed25519-public-key-file)))
134 (add ".guix-authorizations"
135 ,(object->string `(authorizations (version 0) ()))) ;empty
136 (commit "second commit"
137 (signer ,(key-fingerprint %ed25519-public-key-file)))
139 (commit "third commit"
140 (signer ,(key-fingerprint %ed25519-public-key-file))))
141 (with-repository directory repository
142 (let ((commit1 (find-commit repository "first"))
143 (commit2 (find-commit repository "second"))
144 (commit3 (find-commit repository "third")))
145 ;; COMMIT1 and COMMIT2 are fine.
146 (and (authenticate-commits repository (list commit1 commit2)
147 #:keyring-reference "master")
149 ;; COMMIT3 is signed by an unauthorized key according to its
150 ;; parent's '.guix-authorizations' file.
151 (guard (c ((unauthorized-commit-error? c)
152 (and (oid=? (git-authentication-error-commit c)
155 (openpgp-public-key-fingerprint
156 (unauthorized-commit-error-signing-key c))
157 (openpgp-public-key-fingerprint
159 %ed25519-public-key-file))))))
160 (authenticate-commits repository
161 (list commit1 commit2 commit3)
162 #:keyring-reference "master")
165 (unless (gpg+git-available?) (test-skip 1))
166 (test-assert "signed commits, .guix-authorizations, unauthorized merge"
167 (with-fresh-gnupg-setup (list %ed25519-public-key-file
168 %ed25519-secret-key-file
169 %ed25519-2-public-key-file
170 %ed25519-2-secret-key-file)
171 (with-temporary-git-repository directory
173 ,(call-with-input-file %ed25519-public-key-file
176 ,(call-with-input-file %ed25519-2-public-key-file
178 (add ".guix-authorizations"
180 `(authorizations (version 0)
182 %ed25519-public-key-file)
184 (commit "zeroth commit")
186 (commit "first commit"
187 (signer ,(key-fingerprint %ed25519-public-key-file)))
190 (add "devel/1.txt" "1")
191 (commit "first devel commit"
192 (signer ,(key-fingerprint %ed25519-2-public-key-file)))
195 (commit "second commit"
196 (signer ,(key-fingerprint %ed25519-public-key-file)))
197 (merge "devel" "merge"
198 (signer ,(key-fingerprint %ed25519-public-key-file))))
199 (with-repository directory repository
200 (let ((master1 (find-commit repository "first commit"))
201 (master2 (find-commit repository "second commit"))
202 (devel1 (find-commit repository "first devel commit"))
203 (merge (find-commit repository "merge")))
204 (define (correct? c commit)
205 (and (oid=? (git-authentication-error-commit c)
208 (openpgp-public-key-fingerprint
209 (unauthorized-commit-error-signing-key c))
210 (openpgp-public-key-fingerprint
211 (read-openpgp-packet %ed25519-2-public-key-file)))))
213 (and (authenticate-commits repository (list master1 master2)
214 #:keyring-reference "master")
216 ;; DEVEL1 is signed by an unauthorized key according to its
217 ;; parent's '.guix-authorizations' file.
218 (guard (c ((unauthorized-commit-error? c)
219 (correct? c devel1)))
220 (authenticate-commits repository
221 (list master1 devel1)
222 #:keyring-reference "master")
225 ;; MERGE is authorized but one of its ancestors is not.
226 (guard (c ((unauthorized-commit-error? c)
227 (correct? c devel1)))
228 (authenticate-commits repository
229 (list master1 master2
231 #:keyring-reference "master")
234 (unless (gpg+git-available?) (test-skip 1))
235 (test-assert "signed commits, .guix-authorizations, authorized merge"
236 (with-fresh-gnupg-setup (list %ed25519-public-key-file
237 %ed25519-secret-key-file
238 %ed25519-2-public-key-file
239 %ed25519-2-secret-key-file)
240 (with-temporary-git-repository directory
242 ,(call-with-input-file %ed25519-public-key-file
245 ,(call-with-input-file %ed25519-2-public-key-file
247 (add ".guix-authorizations"
249 `(authorizations (version 0)
251 %ed25519-public-key-file)
253 (commit "zeroth commit")
255 (commit "first commit"
256 (signer ,(key-fingerprint %ed25519-public-key-file)))
259 (add ".guix-authorizations"
260 ,(object->string ;add the second signer
261 `(authorizations (version 0)
263 %ed25519-public-key-file)
266 %ed25519-2-public-key-file))))))
267 (commit "first devel commit"
268 (signer ,(key-fingerprint %ed25519-public-key-file)))
269 (add "devel/2.txt" "2")
270 (commit "second devel commit"
271 (signer ,(key-fingerprint %ed25519-2-public-key-file)))
274 (commit "second commit"
275 (signer ,(key-fingerprint %ed25519-public-key-file)))
276 (merge "devel" "merge"
277 (signer ,(key-fingerprint %ed25519-public-key-file)))
278 ;; After the merge, the second signer is authorized.
280 (commit "third commit"
281 (signer ,(key-fingerprint %ed25519-2-public-key-file))))
282 (with-repository directory repository
283 (let ((master1 (find-commit repository "first commit"))
284 (master2 (find-commit repository "second commit"))
285 (devel1 (find-commit repository "first devel commit"))
286 (devel2 (find-commit repository "second devel commit"))
287 (merge (find-commit repository "merge"))
288 (master3 (find-commit repository "third commit")))
289 (authenticate-commits repository
290 (list master1 master2 devel1 devel2
292 #:keyring-reference "master"))))))
294 (unless (gpg+git-available?) (test-skip 1))
295 (test-assert "signed commits, .guix-authorizations removed"
296 (with-fresh-gnupg-setup (list %ed25519-public-key-file
297 %ed25519-secret-key-file)
298 (with-temporary-git-repository directory
299 `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
301 (add ".guix-authorizations"
303 `(authorizations (version 0)
305 %ed25519-public-key-file)
306 (name "Charlie"))))))
307 (commit "zeroth commit")
309 (commit "first commit"
310 (signer ,(key-fingerprint %ed25519-public-key-file)))
311 (remove ".guix-authorizations")
312 (commit "second commit"
313 (signer ,(key-fingerprint %ed25519-public-key-file)))
315 (commit "third commit"
316 (signer ,(key-fingerprint %ed25519-public-key-file))))
317 (with-repository directory repository
318 (let ((commit1 (find-commit repository "first"))
319 (commit2 (find-commit repository "second"))
320 (commit3 (find-commit repository "third")))
321 ;; COMMIT1 and COMMIT2 are fine.
322 (and (authenticate-commits repository (list commit1 commit2)
323 #:keyring-reference "master")
325 ;; COMMIT3 is rejected because COMMIT2 removes
326 ;; '.guix-authorizations'.
327 (guard (c ((unauthorized-commit-error? c)
328 (oid=? (git-authentication-error-commit c)
329 (commit-id commit2))))
330 (authenticate-commits repository
331 (list commit1 commit2 commit3)
332 #:keyring-reference "master")
335 (unless (gpg+git-available?) (test-skip 1))
336 (test-assert "introductory commit, valid signature"
337 (with-fresh-gnupg-setup (list %ed25519-public-key-file
338 %ed25519-secret-key-file)
339 (let ((fingerprint (key-fingerprint %ed25519-public-key-file)))
340 (with-temporary-git-repository directory
341 `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
343 (add ".guix-authorizations"
345 `(authorizations (version 0)
347 %ed25519-public-key-file)
348 (name "Charlie"))))))
349 (commit "zeroth commit" (signer ,fingerprint))
351 (commit "first commit" (signer ,fingerprint)))
352 (with-repository directory repository
353 (let ((commit0 (find-commit repository "zero"))
354 (commit1 (find-commit repository "first")))
355 ;; COMMIT0 is signed with the right key, and COMMIT1 is fine.
356 (authenticate-repository repository
358 (openpgp-fingerprint fingerprint)
359 #:keyring-reference "master"
360 #:cache-key (random-text))))))))
362 (unless (gpg+git-available?) (test-skip 1))
363 (test-equal "introductory commit, missing signature"
364 'intro-lacks-signature
365 (with-fresh-gnupg-setup (list %ed25519-public-key-file
366 %ed25519-secret-key-file)
367 (let ((fingerprint (key-fingerprint %ed25519-public-key-file)))
368 (with-temporary-git-repository directory
369 `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
371 (add ".guix-authorizations"
373 `(authorizations (version 0)
375 %ed25519-public-key-file)
376 (name "Charlie"))))))
377 (commit "zeroth commit") ;unsigned!
379 (commit "first commit" (signer ,fingerprint)))
380 (with-repository directory repository
381 (let ((commit0 (find-commit repository "zero")))
382 ;; COMMIT0 is not signed.
383 (guard (c ((formatted-message? c)
384 ;; Message like "commit ~a lacks a signature".
385 (and (equal? (formatted-message-arguments c)
386 (list (oid->string (commit-id commit0))))
387 'intro-lacks-signature)))
388 (authenticate-repository repository
390 (openpgp-fingerprint fingerprint)
391 #:keyring-reference "master"
392 #:cache-key (random-text)))))))))
394 (unless (gpg+git-available?) (test-skip 1))
395 (test-equal "introductory commit, wrong signature"
396 'wrong-intro-signing-key
397 (with-fresh-gnupg-setup (list %ed25519-public-key-file
398 %ed25519-secret-key-file
399 %ed25519-2-public-key-file
400 %ed25519-2-secret-key-file)
401 (let ((fingerprint (key-fingerprint %ed25519-public-key-file))
402 (wrong-fingerprint (key-fingerprint %ed25519-2-public-key-file)))
403 (with-temporary-git-repository directory
404 `((add "signer1.key" ,(call-with-input-file %ed25519-public-key-file
406 (add "signer2.key" ,(call-with-input-file %ed25519-2-public-key-file
408 (add ".guix-authorizations"
410 `(authorizations (version 0)
412 %ed25519-public-key-file)
413 (name "Charlie"))))))
414 (commit "zeroth commit" (signer ,wrong-fingerprint))
416 (commit "first commit" (signer ,fingerprint)))
417 (with-repository directory repository
418 (let ((commit0 (find-commit repository "zero"))
419 (commit1 (find-commit repository "first")))
420 ;; COMMIT0 is signed with the wrong key--not the one passed as the
421 ;; SIGNER argument to 'authenticate-repository'.
422 (guard (c ((formatted-message? c)
423 ;; Message like "commit ~a signed by ~a instead of ~a".
424 (and (equal? (formatted-message-arguments c)
425 (list (oid->string (commit-id commit0))
426 wrong-fingerprint fingerprint))
427 'wrong-intro-signing-key)))
428 (authenticate-repository repository
430 (openpgp-fingerprint fingerprint)
431 #:keyring-reference "master"
432 #:cache-key (random-text)))))))))
434 (unless (gpg+git-available?) (test-skip 1))
435 (test-equal "authenticate-repository, target not a descendant of intro"
436 'target-commit-not-a-descendant-of-intro
437 (with-fresh-gnupg-setup (list %ed25519-public-key-file
438 %ed25519-secret-key-file)
439 (let ((fingerprint (key-fingerprint %ed25519-public-key-file)))
440 (with-temporary-git-repository directory
441 `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
443 (add ".guix-authorizations"
445 `(authorizations (version 0)
447 %ed25519-public-key-file)
448 (name "Charlie"))))))
449 (commit "zeroth commit" (signer ,fingerprint))
450 (branch "pre-intro-branch")
451 (checkout "pre-intro-branch")
453 (commit "alternate commit" (signer ,fingerprint))
456 (commit "first commit" (signer ,fingerprint))
458 (commit "second commit" (signer ,fingerprint)))
459 (with-repository directory repository
460 (let ((commit1 (find-commit repository "first"))
462 (commit-lookup repository
464 (branch-lookup repository
465 "pre-intro-branch")))))
466 (guard (c ((formatted-message? c)
467 (and (equal? (formatted-message-arguments c)
468 (list (oid->string (commit-id commit-alt))
469 (oid->string (commit-id commit1))))
470 'target-commit-not-a-descendant-of-intro)))
471 (authenticate-repository repository
473 (openpgp-fingerprint fingerprint)
474 #:end (commit-id commit-alt)
475 #:keyring-reference "master"
476 #:cache-key (random-text)))))))))
478 (test-end "git-authenticate")