aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2022-01-28 17:20:43 +0100
committerLudovic Courtès <ludo@gnu.org>2022-02-14 11:23:08 +0100
commitca87601dd97dd9d356409827802eb0f8a3a535f0 (patch)
treee3a6c9e4abb95e45c27aa80dcdc9d99a2f661a6e
parent87d49346f3072f7b4343b6fb387ee5f9311493b7 (diff)
downloadguix-ca87601dd97dd9d356409827802eb0f8a3a535f0.tar
guix-ca87601dd97dd9d356409827802eb0f8a3a535f0.tar.gz
git-authenticate: Ensure the target is a descendant of the introductory commit.
Fixes a bug whereby authentication of a commit *not* descending from the introductory commit could succeed, provided the commit verifies the authorization invariant. In the example below, A is a common ancestor of the introductory commit I and of commit X. Authentication of X would succeed, even though it is not a descendant of I, as long as X is authorized according to the '.guix-authorizations' in A: X I \ / A This is because, 'authenticate-repository' would not check whether X descends from I, and the call (commit-difference X I) would return X. In practice that only affects forks because it means that ancestors of the introductory commit already contain a '.guix-authorizations' file. * guix/git-authenticate.scm (authenticate-repository): Add call to 'commit-descendant?'. * tests/channels.scm ("authenticate-channel, not a descendant of introductory commit"): New test. * tests/git-authenticate.scm ("authenticate-repository, target not a descendant of intro"): New test. * tests/guix-git-authenticate.sh: Expect earlier test to fail since 9549f0283a78fe36f2d4ff2a04ef8ad6b0c02604 is not a descendant of $intro_commit. Add new test targeting an ancestor of the introductory commit, and another test targeting the v1.2.0 commit. * doc/guix.texi (Specifying Channel Authorizations): Add a sentence.
-rw-r--r--doc/guix.texi4
-rw-r--r--guix/git-authenticate.scm17
-rw-r--r--tests/channels.scm60
-rw-r--r--tests/git-authenticate.scm44
-rw-r--r--tests/guix-git-authenticate.sh17
5 files changed, 136 insertions, 6 deletions
diff --git a/doc/guix.texi b/doc/guix.texi
index 86dbe9f201..c116be8907 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -5448,7 +5448,9 @@ commit of a channel that should be authenticated. The first time a
channel is fetched with @command{guix pull} or @command{guix
time-machine}, the command looks up the introductory commit and verifies
that it is signed by the specified OpenPGP key. From then on, it
-authenticates commits according to the rule above.
+authenticates commits according to the rule above. Authentication fails
+if the target commit is neither a descendant nor an ancestor of the
+introductory commit.
Additionally, your channel must provide all the OpenPGP keys that were
ever mentioned in @file{.guix-authorizations}, stored as @file{.key}
diff --git a/guix/git-authenticate.scm b/guix/git-authenticate.scm
index ab3fcd8b2f..419cb85afc 100644
--- a/guix/git-authenticate.scm
+++ b/guix/git-authenticate.scm
@@ -1,5 +1,5 @@
;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2019, 2020, 2021, 2022 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -22,7 +22,9 @@
#:use-module (guix base16)
#:autoload (guix base64) (base64-encode)
#:use-module ((guix git)
- #:select (commit-difference false-if-git-not-found))
+ #:select (commit-difference
+ commit-descendant?
+ false-if-git-not-found))
#:use-module (guix i18n)
#:use-module ((guix diagnostics) #:select (formatted-message))
#:use-module (guix openpgp)
@@ -426,6 +428,17 @@ denoting the authorized keys for commits whose parent lack the
(verify-introductory-commit repository keyring
start-commit signer))
+ ;; Make sure END-COMMIT is a descendant of START-COMMIT or of one of
+ ;; AUTHENTICATED-COMMITS, which are known to be descendants of
+ ;; START-COMMIT.
+ (unless (commit-descendant? end-commit
+ (cons start-commit
+ authenticated-commits))
+ (raise (formatted-message
+ (G_ "commit ~a is not a descendant of introductory commit ~a")
+ (oid->string (commit-id end-commit))
+ (oid->string (commit-id start-commit)))))
+
(let ((stats (call-with-progress-reporter reporter
(lambda (report)
(authenticate-commits repository commits
diff --git a/tests/channels.scm b/tests/channels.scm
index d45c450241..0fe870dbaf 100644
--- a/tests/channels.scm
+++ b/tests/channels.scm
@@ -1,6 +1,6 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net>
-;;; Copyright © 2019, 2020 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2019, 2020, 2022 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -526,6 +526,64 @@
'failed))))))
(unless (gpg+git-available?) (test-skip 1))
+(test-equal "authenticate-channel, not a descendant of introductory commit"
+ #t
+ (with-fresh-gnupg-setup (list %ed25519-public-key-file
+ %ed25519-secret-key-file
+ %ed25519-2-public-key-file
+ %ed25519-2-secret-key-file)
+ (with-temporary-git-repository directory
+ `((add ".guix-channel"
+ ,(object->string
+ '(channel (version 0)
+ (keyring-reference "master"))))
+ (add ".guix-authorizations"
+ ,(object->string
+ `(authorizations (version 0)
+ ((,(key-fingerprint
+ %ed25519-public-key-file)
+ (name "Charlie"))))))
+ (add "signer.key" ,(call-with-input-file %ed25519-public-key-file
+ get-string-all))
+ (commit "first commit"
+ (signer ,(key-fingerprint %ed25519-public-key-file)))
+ (branch "alternate-branch")
+ (checkout "alternate-branch")
+ (add "something.txt" ,(random-text))
+ (commit "intro commit"
+ (signer ,(key-fingerprint %ed25519-public-key-file)))
+ (checkout "master")
+ (add "random" ,(random-text))
+ (commit "second commit"
+ (signer ,(key-fingerprint %ed25519-public-key-file))))
+ (with-repository directory repository
+ (let* ((commit1 (find-commit repository "first"))
+ (commit2 (find-commit repository "second"))
+ (commit0 (commit-lookup
+ repository
+ (reference-target
+ (branch-lookup repository "alternate-branch"))))
+ (intro (make-channel-introduction
+ (commit-id-string commit0)
+ (openpgp-public-key-fingerprint
+ (read-openpgp-packet
+ %ed25519-public-key-file))))
+ (channel (channel (name 'example)
+ (url (string-append "file://" directory))
+ (introduction intro))))
+ (guard (c ((formatted-message? c)
+ (and (string-contains (formatted-message-string c)
+ "not a descendant")
+ (equal? (formatted-message-arguments c)
+ (list
+ (oid->string (commit-id commit2))
+ (oid->string (commit-id commit0)))))))
+ (authenticate-channel channel directory
+ (commit-id-string commit2)
+ #:keyring-reference-prefix "")
+ 'failed))))))
+
+(unless (gpg+git-available?) (test-skip 1))
(test-equal "authenticate-channel, .guix-authorizations"
#t
(with-fresh-gnupg-setup (list %ed25519-public-key-file
diff --git a/tests/git-authenticate.scm b/tests/git-authenticate.scm
index 6ec55fb2e5..c063920c12 100644
--- a/tests/git-authenticate.scm
+++ b/tests/git-authenticate.scm
@@ -431,4 +431,48 @@
#:keyring-reference "master"
#:cache-key (random-text)))))))))
+(unless (gpg+git-available?) (test-skip 1))
+(test-equal "authenticate-repository, target not a descendant of intro"
+ 'target-commit-not-a-descendant-of-intro
+ (with-fresh-gnupg-setup (list %ed25519-public-key-file
+ %ed25519-secret-key-file)
+ (let ((fingerprint (key-fingerprint %ed25519-public-key-file)))
+ (with-temporary-git-repository directory
+ `((add "signer.key" ,(call-with-input-file %ed25519-public-key-file
+ get-string-all))
+ (add ".guix-authorizations"
+ ,(object->string
+ `(authorizations (version 0)
+ ((,(key-fingerprint
+ %ed25519-public-key-file)
+ (name "Charlie"))))))
+ (commit "zeroth commit" (signer ,fingerprint))
+ (branch "pre-intro-branch")
+ (checkout "pre-intro-branch")
+ (add "b.txt" "B")
+ (commit "alternate commit" (signer ,fingerprint))
+ (checkout "master")
+ (add "a.txt" "A")
+ (commit "first commit" (signer ,fingerprint))
+ (add "c.txt" "C")
+ (commit "second commit" (signer ,fingerprint)))
+ (with-repository directory repository
+ (let ((commit1 (find-commit repository "first"))
+ (commit-alt
+ (commit-lookup repository
+ (reference-target
+ (branch-lookup repository
+ "pre-intro-branch")))))
+ (guard (c ((formatted-message? c)
+ (and (equal? (formatted-message-arguments c)
+ (list (oid->string (commit-id commit-alt))
+ (oid->string (commit-id commit1))))
+ 'target-commit-not-a-descendant-of-intro)))
+ (authenticate-repository repository
+ (commit-id commit1)
+ (openpgp-fingerprint fingerprint)
+ #:end (commit-id commit-alt)
+ #:keyring-reference "master"
+ #:cache-key (random-text)))))))))
+
(test-end "git-authenticate")
diff --git a/tests/guix-git-authenticate.sh b/tests/guix-git-authenticate.sh
index 8ebbea398b..2b90d8a4af 100644
--- a/tests/guix-git-authenticate.sh
+++ b/tests/guix-git-authenticate.sh
@@ -1,5 +1,5 @@
# GNU Guix --- Functional package management for GNU
-# Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
+# Copyright © 2020, 2022 Ludovic Courtès <ludo@gnu.org>
#
# This file is part of GNU Guix.
#
@@ -34,10 +34,18 @@ intro_signer="BBB0 2DDF 2CEA F6A8 0D1D E643 A2A0 6DF2 A33A 54FA"
cache_key="test-$$"
-guix git authenticate "$intro_commit" "$intro_signer" \
+# This must fail because the end commit is not a descendant of $intro_commit.
+! guix git authenticate "$intro_commit" "$intro_signer" \
--cache-key="$cache_key" --stats \
--end=9549f0283a78fe36f2d4ff2a04ef8ad6b0c02604
+# The v1.2.0 commit is a descendant of $intro_commit and it satisfies the
+# authorization invariant.
+v1_2_0_commit="a099685659b4bfa6b3218f84953cbb7ff9e88063"
+guix git authenticate "$intro_commit" "$intro_signer" \
+ --cache-key="$cache_key" --stats \
+ --end="$v1_2_0_commit"
+
rm "$XDG_CACHE_HOME/guix/authentication/$cache_key"
# Commit and signer of the 'v1.0.0' tag.
@@ -45,6 +53,11 @@ v1_0_0_commit="6298c3ffd9654d3231a6f25390b056483e8f407c"
v1_0_0_signer="3CE4 6455 8A84 FDC6 9DB4 0CFB 090B 1199 3D9A EBB5" # civodul
v1_0_1_commit="d68de958b60426798ed62797ff7c96c327a672ac"
+# This should succeed because v1.0.0 is an ancestor of $intro_commit.
+guix git authenticate "$intro_commit" "$intro_signer" \
+ --cache-key="$cache_key" --stats \
+ --end="$v1_0_0_commit"
+
# This should fail because these commits lack '.guix-authorizations'.
! guix git authenticate "$v1_0_0_commit" "$v1_0_0_signer" \
--cache-key="$cache_key" --end="$v1_0_1_commit"