git-authenticate.scm 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2020, 2022 Ludovic Courtès <ludo@gnu.org>
  3. ;;;
  4. ;;; This file is part of GNU Guix.
  5. ;;;
  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.
  10. ;;;
  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.
  15. ;;;
  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/>.
  18. (define-module (test-git-authenticate)
  19. #:use-module (git)
  20. #:use-module (guix git)
  21. #:use-module (guix git-authenticate)
  22. #:use-module ((guix channels) #:select (openpgp-fingerprint))
  23. #:use-module ((guix diagnostics)
  24. #:select (formatted-message? formatted-message-arguments))
  25. #:use-module (guix openpgp)
  26. #:use-module ((guix tests) #:select (random-text))
  27. #:use-module (guix tests git)
  28. #:use-module (guix tests gnupg)
  29. #:use-module (guix build utils)
  30. #:use-module (srfi srfi-1)
  31. #:use-module (srfi srfi-34)
  32. #:use-module (srfi srfi-35)
  33. #:use-module (srfi srfi-64)
  34. #:use-module (rnrs bytevectors)
  35. #:use-module (rnrs io ports))
  36. ;; Test the (guix git-authenticate) tools.
  37. (define (gpg+git-available?)
  38. (and (which (git-command))
  39. (which (gpg-command)) (which (gpgconf-command))))
  40. (test-begin "git-authenticate")
  41. (unless (which (git-command)) (test-skip 1))
  42. (test-assert "unsigned commits"
  43. (with-temporary-git-repository directory
  44. '((add "a.txt" "A")
  45. (commit "first commit")
  46. (add "b.txt" "B")
  47. (commit "second commit"))
  48. (with-repository directory repository
  49. (let ((commit1 (find-commit repository "first"))
  50. (commit2 (find-commit repository "second")))
  51. (guard (c ((unsigned-commit-error? c)
  52. (oid=? (git-authentication-error-commit c)
  53. (commit-id commit1))))
  54. (authenticate-commits repository (list commit1 commit2)
  55. #:keyring-reference "master")
  56. 'failed)))))
  57. (unless (gpg+git-available?) (test-skip 1))
  58. (test-assert "signed commits, SHA1 signature"
  59. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  60. %ed25519-secret-key-file)
  61. ;; Force use of SHA1 for signatures.
  62. (call-with-output-file (string-append (getenv "GNUPGHOME") "/gpg.conf")
  63. (lambda (port)
  64. (display "digest-algo sha1" port)))
  65. (with-temporary-git-repository directory
  66. `((add "a.txt" "A")
  67. (add "signer.key" ,(call-with-input-file %ed25519-public-key-file
  68. get-string-all))
  69. (add ".guix-authorizations"
  70. ,(object->string
  71. `(authorizations (version 0)
  72. ((,(key-fingerprint %ed25519-public-key-file)
  73. (name "Charlie"))))))
  74. (commit "first commit"
  75. (signer ,(key-fingerprint %ed25519-public-key-file))))
  76. (with-repository directory repository
  77. (let ((commit (find-commit repository "first")))
  78. (guard (c ((unsigned-commit-error? c)
  79. (oid=? (git-authentication-error-commit c)
  80. (commit-id commit))))
  81. (authenticate-commits repository (list commit)
  82. #:keyring-reference "master")
  83. 'failed))))))
  84. (unless (gpg+git-available?) (test-skip 1))
  85. (test-assert "signed commits, default authorizations"
  86. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  87. %ed25519-secret-key-file)
  88. (with-temporary-git-repository directory
  89. `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
  90. get-string-all))
  91. (commit "zeroth commit")
  92. (add "a.txt" "A")
  93. (commit "first commit"
  94. (signer ,(key-fingerprint %ed25519-public-key-file)))
  95. (add "b.txt" "B")
  96. (commit "second commit"
  97. (signer ,(key-fingerprint %ed25519-public-key-file))))
  98. (with-repository directory repository
  99. (let ((commit1 (find-commit repository "first"))
  100. (commit2 (find-commit repository "second")))
  101. (authenticate-commits repository (list commit1 commit2)
  102. #:default-authorizations
  103. (list (openpgp-public-key-fingerprint
  104. (read-openpgp-packet
  105. %ed25519-public-key-file)))
  106. #:keyring-reference "master"))))))
  107. (unless (gpg+git-available?) (test-skip 1))
  108. (test-assert "signed commits, .guix-authorizations"
  109. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  110. %ed25519-secret-key-file)
  111. (with-temporary-git-repository directory
  112. `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
  113. get-string-all))
  114. (add ".guix-authorizations"
  115. ,(object->string
  116. `(authorizations (version 0)
  117. ((,(key-fingerprint
  118. %ed25519-public-key-file)
  119. (name "Charlie"))))))
  120. (commit "zeroth commit")
  121. (add "a.txt" "A")
  122. (commit "first commit"
  123. (signer ,(key-fingerprint %ed25519-public-key-file)))
  124. (add ".guix-authorizations"
  125. ,(object->string `(authorizations (version 0) ()))) ;empty
  126. (commit "second commit"
  127. (signer ,(key-fingerprint %ed25519-public-key-file)))
  128. (add "b.txt" "B")
  129. (commit "third commit"
  130. (signer ,(key-fingerprint %ed25519-public-key-file))))
  131. (with-repository directory repository
  132. (let ((commit1 (find-commit repository "first"))
  133. (commit2 (find-commit repository "second"))
  134. (commit3 (find-commit repository "third")))
  135. ;; COMMIT1 and COMMIT2 are fine.
  136. (and (authenticate-commits repository (list commit1 commit2)
  137. #:keyring-reference "master")
  138. ;; COMMIT3 is signed by an unauthorized key according to its
  139. ;; parent's '.guix-authorizations' file.
  140. (guard (c ((unauthorized-commit-error? c)
  141. (and (oid=? (git-authentication-error-commit c)
  142. (commit-id commit3))
  143. (bytevector=?
  144. (openpgp-public-key-fingerprint
  145. (unauthorized-commit-error-signing-key c))
  146. (openpgp-public-key-fingerprint
  147. (read-openpgp-packet
  148. %ed25519-public-key-file))))))
  149. (authenticate-commits repository
  150. (list commit1 commit2 commit3)
  151. #:keyring-reference "master")
  152. 'failed)))))))
  153. (unless (gpg+git-available?) (test-skip 1))
  154. (test-assert "signed commits, .guix-authorizations, unauthorized merge"
  155. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  156. %ed25519-secret-key-file
  157. %ed25519-2-public-key-file
  158. %ed25519-2-secret-key-file)
  159. (with-temporary-git-repository directory
  160. `((add "signer1.key"
  161. ,(call-with-input-file %ed25519-public-key-file
  162. get-string-all))
  163. (add "signer2.key"
  164. ,(call-with-input-file %ed25519-2-public-key-file
  165. get-string-all))
  166. (add ".guix-authorizations"
  167. ,(object->string
  168. `(authorizations (version 0)
  169. ((,(key-fingerprint
  170. %ed25519-public-key-file)
  171. (name "Alice"))))))
  172. (commit "zeroth commit")
  173. (add "a.txt" "A")
  174. (commit "first commit"
  175. (signer ,(key-fingerprint %ed25519-public-key-file)))
  176. (branch "devel")
  177. (checkout "devel")
  178. (add "devel/1.txt" "1")
  179. (commit "first devel commit"
  180. (signer ,(key-fingerprint %ed25519-2-public-key-file)))
  181. (checkout "master")
  182. (add "b.txt" "B")
  183. (commit "second commit"
  184. (signer ,(key-fingerprint %ed25519-public-key-file)))
  185. (merge "devel" "merge"
  186. (signer ,(key-fingerprint %ed25519-public-key-file))))
  187. (with-repository directory repository
  188. (let ((master1 (find-commit repository "first commit"))
  189. (master2 (find-commit repository "second commit"))
  190. (devel1 (find-commit repository "first devel commit"))
  191. (merge (find-commit repository "merge")))
  192. (define (correct? c commit)
  193. (and (oid=? (git-authentication-error-commit c)
  194. (commit-id commit))
  195. (bytevector=?
  196. (openpgp-public-key-fingerprint
  197. (unauthorized-commit-error-signing-key c))
  198. (openpgp-public-key-fingerprint
  199. (read-openpgp-packet %ed25519-2-public-key-file)))))
  200. (and (authenticate-commits repository (list master1 master2)
  201. #:keyring-reference "master")
  202. ;; DEVEL1 is signed by an unauthorized key according to its
  203. ;; parent's '.guix-authorizations' file.
  204. (guard (c ((unauthorized-commit-error? c)
  205. (correct? c devel1)))
  206. (authenticate-commits repository
  207. (list master1 devel1)
  208. #:keyring-reference "master")
  209. #f)
  210. ;; MERGE is authorized but one of its ancestors is not.
  211. (guard (c ((unauthorized-commit-error? c)
  212. (correct? c devel1)))
  213. (authenticate-commits repository
  214. (list master1 master2
  215. devel1 merge)
  216. #:keyring-reference "master")
  217. #f)))))))
  218. (unless (gpg+git-available?) (test-skip 1))
  219. (test-assert "signed commits, .guix-authorizations, authorized merge"
  220. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  221. %ed25519-secret-key-file
  222. %ed25519-2-public-key-file
  223. %ed25519-2-secret-key-file)
  224. (with-temporary-git-repository directory
  225. `((add "signer1.key"
  226. ,(call-with-input-file %ed25519-public-key-file
  227. get-string-all))
  228. (add "signer2.key"
  229. ,(call-with-input-file %ed25519-2-public-key-file
  230. get-string-all))
  231. (add ".guix-authorizations"
  232. ,(object->string
  233. `(authorizations (version 0)
  234. ((,(key-fingerprint
  235. %ed25519-public-key-file)
  236. (name "Alice"))))))
  237. (commit "zeroth commit")
  238. (add "a.txt" "A")
  239. (commit "first commit"
  240. (signer ,(key-fingerprint %ed25519-public-key-file)))
  241. (branch "devel")
  242. (checkout "devel")
  243. (add ".guix-authorizations"
  244. ,(object->string ;add the second signer
  245. `(authorizations (version 0)
  246. ((,(key-fingerprint
  247. %ed25519-public-key-file)
  248. (name "Alice"))
  249. (,(key-fingerprint
  250. %ed25519-2-public-key-file))))))
  251. (commit "first devel commit"
  252. (signer ,(key-fingerprint %ed25519-public-key-file)))
  253. (add "devel/2.txt" "2")
  254. (commit "second devel commit"
  255. (signer ,(key-fingerprint %ed25519-2-public-key-file)))
  256. (checkout "master")
  257. (add "b.txt" "B")
  258. (commit "second commit"
  259. (signer ,(key-fingerprint %ed25519-public-key-file)))
  260. (merge "devel" "merge"
  261. (signer ,(key-fingerprint %ed25519-public-key-file)))
  262. ;; After the merge, the second signer is authorized.
  263. (add "c.txt" "C")
  264. (commit "third commit"
  265. (signer ,(key-fingerprint %ed25519-2-public-key-file))))
  266. (with-repository directory repository
  267. (let ((master1 (find-commit repository "first commit"))
  268. (master2 (find-commit repository "second commit"))
  269. (devel1 (find-commit repository "first devel commit"))
  270. (devel2 (find-commit repository "second devel commit"))
  271. (merge (find-commit repository "merge"))
  272. (master3 (find-commit repository "third commit")))
  273. (authenticate-commits repository
  274. (list master1 master2 devel1 devel2
  275. merge master3)
  276. #:keyring-reference "master"))))))
  277. (unless (gpg+git-available?) (test-skip 1))
  278. (test-assert "signed commits, .guix-authorizations removed"
  279. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  280. %ed25519-secret-key-file)
  281. (with-temporary-git-repository directory
  282. `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
  283. get-string-all))
  284. (add ".guix-authorizations"
  285. ,(object->string
  286. `(authorizations (version 0)
  287. ((,(key-fingerprint
  288. %ed25519-public-key-file)
  289. (name "Charlie"))))))
  290. (commit "zeroth commit")
  291. (add "a.txt" "A")
  292. (commit "first commit"
  293. (signer ,(key-fingerprint %ed25519-public-key-file)))
  294. (remove ".guix-authorizations")
  295. (commit "second commit"
  296. (signer ,(key-fingerprint %ed25519-public-key-file)))
  297. (add "b.txt" "B")
  298. (commit "third commit"
  299. (signer ,(key-fingerprint %ed25519-public-key-file))))
  300. (with-repository directory repository
  301. (let ((commit1 (find-commit repository "first"))
  302. (commit2 (find-commit repository "second"))
  303. (commit3 (find-commit repository "third")))
  304. ;; COMMIT1 and COMMIT2 are fine.
  305. (and (authenticate-commits repository (list commit1 commit2)
  306. #:keyring-reference "master")
  307. ;; COMMIT3 is rejected because COMMIT2 removes
  308. ;; '.guix-authorizations'.
  309. (guard (c ((unauthorized-commit-error? c)
  310. (oid=? (git-authentication-error-commit c)
  311. (commit-id commit2))))
  312. (authenticate-commits repository
  313. (list commit1 commit2 commit3)
  314. #:keyring-reference "master")
  315. 'failed)))))))
  316. (unless (gpg+git-available?) (test-skip 1))
  317. (test-assert "introductory commit, valid signature"
  318. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  319. %ed25519-secret-key-file)
  320. (let ((fingerprint (key-fingerprint %ed25519-public-key-file)))
  321. (with-temporary-git-repository directory
  322. `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
  323. get-string-all))
  324. (add ".guix-authorizations"
  325. ,(object->string
  326. `(authorizations (version 0)
  327. ((,(key-fingerprint
  328. %ed25519-public-key-file)
  329. (name "Charlie"))))))
  330. (commit "zeroth commit" (signer ,fingerprint))
  331. (add "a.txt" "A")
  332. (commit "first commit" (signer ,fingerprint)))
  333. (with-repository directory repository
  334. (let ((commit0 (find-commit repository "zero"))
  335. (commit1 (find-commit repository "first")))
  336. ;; COMMIT0 is signed with the right key, and COMMIT1 is fine.
  337. (authenticate-repository repository
  338. (commit-id commit0)
  339. (openpgp-fingerprint fingerprint)
  340. #:keyring-reference "master"
  341. #:cache-key (random-text))))))))
  342. (unless (gpg+git-available?) (test-skip 1))
  343. (test-equal "introductory commit, missing signature"
  344. 'intro-lacks-signature
  345. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  346. %ed25519-secret-key-file)
  347. (let ((fingerprint (key-fingerprint %ed25519-public-key-file)))
  348. (with-temporary-git-repository directory
  349. `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
  350. get-string-all))
  351. (add ".guix-authorizations"
  352. ,(object->string
  353. `(authorizations (version 0)
  354. ((,(key-fingerprint
  355. %ed25519-public-key-file)
  356. (name "Charlie"))))))
  357. (commit "zeroth commit") ;unsigned!
  358. (add "a.txt" "A")
  359. (commit "first commit" (signer ,fingerprint)))
  360. (with-repository directory repository
  361. (let ((commit0 (find-commit repository "zero")))
  362. ;; COMMIT0 is not signed.
  363. (guard (c ((formatted-message? c)
  364. ;; Message like "commit ~a lacks a signature".
  365. (and (equal? (formatted-message-arguments c)
  366. (list (oid->string (commit-id commit0))))
  367. 'intro-lacks-signature)))
  368. (authenticate-repository repository
  369. (commit-id commit0)
  370. (openpgp-fingerprint fingerprint)
  371. #:keyring-reference "master"
  372. #:cache-key (random-text)))))))))
  373. (unless (gpg+git-available?) (test-skip 1))
  374. (test-equal "introductory commit, wrong signature"
  375. 'wrong-intro-signing-key
  376. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  377. %ed25519-secret-key-file
  378. %ed25519-2-public-key-file
  379. %ed25519-2-secret-key-file)
  380. (let ((fingerprint (key-fingerprint %ed25519-public-key-file))
  381. (wrong-fingerprint (key-fingerprint %ed25519-2-public-key-file)))
  382. (with-temporary-git-repository directory
  383. `((add "signer1.key" ,(call-with-input-file %ed25519-public-key-file
  384. get-string-all))
  385. (add "signer2.key" ,(call-with-input-file %ed25519-2-public-key-file
  386. get-string-all))
  387. (add ".guix-authorizations"
  388. ,(object->string
  389. `(authorizations (version 0)
  390. ((,(key-fingerprint
  391. %ed25519-public-key-file)
  392. (name "Charlie"))))))
  393. (commit "zeroth commit" (signer ,wrong-fingerprint))
  394. (add "a.txt" "A")
  395. (commit "first commit" (signer ,fingerprint)))
  396. (with-repository directory repository
  397. (let ((commit0 (find-commit repository "zero"))
  398. (commit1 (find-commit repository "first")))
  399. ;; COMMIT0 is signed with the wrong key--not the one passed as the
  400. ;; SIGNER argument to 'authenticate-repository'.
  401. (guard (c ((formatted-message? c)
  402. ;; Message like "commit ~a signed by ~a instead of ~a".
  403. (and (equal? (formatted-message-arguments c)
  404. (list (oid->string (commit-id commit0))
  405. wrong-fingerprint fingerprint))
  406. 'wrong-intro-signing-key)))
  407. (authenticate-repository repository
  408. (commit-id commit0)
  409. (openpgp-fingerprint fingerprint)
  410. #:keyring-reference "master"
  411. #:cache-key (random-text)))))))))
  412. (unless (gpg+git-available?) (test-skip 1))
  413. (test-equal "authenticate-repository, target not a descendant of intro"
  414. 'target-commit-not-a-descendant-of-intro
  415. (with-fresh-gnupg-setup (list %ed25519-public-key-file
  416. %ed25519-secret-key-file)
  417. (let ((fingerprint (key-fingerprint %ed25519-public-key-file)))
  418. (with-temporary-git-repository directory
  419. `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
  420. get-string-all))
  421. (add ".guix-authorizations"
  422. ,(object->string
  423. `(authorizations (version 0)
  424. ((,(key-fingerprint
  425. %ed25519-public-key-file)
  426. (name "Charlie"))))))
  427. (commit "zeroth commit" (signer ,fingerprint))
  428. (branch "pre-intro-branch")
  429. (checkout "pre-intro-branch")
  430. (add "b.txt" "B")
  431. (commit "alternate commit" (signer ,fingerprint))
  432. (checkout "master")
  433. (add "a.txt" "A")
  434. (commit "first commit" (signer ,fingerprint))
  435. (add "c.txt" "C")
  436. (commit "second commit" (signer ,fingerprint)))
  437. (with-repository directory repository
  438. (let ((commit1 (find-commit repository "first"))
  439. (commit-alt
  440. (commit-lookup repository
  441. (reference-target
  442. (branch-lookup repository
  443. "pre-intro-branch")))))
  444. (guard (c ((formatted-message? c)
  445. (and (equal? (formatted-message-arguments c)
  446. (list (oid->string (commit-id commit-alt))
  447. (oid->string (commit-id commit1))))
  448. 'target-commit-not-a-descendant-of-intro)))
  449. (authenticate-repository repository
  450. (commit-id commit1)
  451. (openpgp-fingerprint fingerprint)
  452. #:end (commit-id commit-alt)
  453. #:keyring-reference "master"
  454. #:cache-key (random-text)))))))))
  455. (test-end "git-authenticate")