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.
This commit is contained in:
Ludovic Courtès 2022-01-28 17:20:43 +01:00
parent 87d49346f3
commit ca87601dd9
No known key found for this signature in database
GPG Key ID: 090B11993D9AEBB5
5 changed files with 136 additions and 6 deletions

@ -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}

@ -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

@ -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.
;;;
@ -525,6 +525,64 @@
#:keyring-reference-prefix "")
'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

@ -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")

@ -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"