Add 'guix git authenticate'.

* guix/scripts/git.scm, guix/scripts/git/authenticate.scm,
tests/guix-git-authenticate.sh: New files.
* Makefile.am (MODULES): Add the *.scm files.
(SH_TESTS): Add 'tests/guix-git-authenticate.sh'.
* doc/guix.texi (Channels)[Specifying Channel Authorizations]: Mention
'guix git authenticate'.
(Invoking guix git authenticate): New node.
* po/guix/POTFILES.in: Add 'guix/scripts/git.scm' and
'guix/scripts/git/authenticate.scm'.
This commit is contained in:
Ludovic Courtès 2020-07-05 23:40:29 +02:00
parent 69db2993b5
commit a98712785e
No known key found for this signature in database
GPG Key ID: 090B11993D9AEBB5
6 changed files with 383 additions and 3 deletions

View File

@ -281,6 +281,8 @@ MODULES = \
guix/scripts/publish.scm \
guix/scripts/edit.scm \
guix/scripts/size.scm \
guix/scripts/git.scm \
guix/scripts/git/authenticate.scm \
guix/scripts/graph.scm \
guix/scripts/weather.scm \
guix/scripts/container.scm \
@ -463,6 +465,7 @@ SH_TESTS = \
tests/guix-build-branch.sh \
tests/guix-download.sh \
tests/guix-gc.sh \
tests/guix-git-authenticate.sh \
tests/guix-hash.sh \
tests/guix-pack.sh \
tests/guix-pack-localstatedir.sh \

View File

@ -3981,6 +3981,7 @@ Before that, some security considerations.
@subsection Channel Authentication
@anchor{channel-authentication}
@cindex authentication, of channel code
The @command{guix pull} and @command{guix time-machine} commands
@dfn{authenticate} the code retrieved from channels: they make sure each
@ -4200,6 +4201,7 @@ add a meta-data file @file{.guix-channel} that contains:
@cindex channel authorizations
@subsection Specifying Channel Authorizations
@anchor{channel-authorizations}
As we saw above, Guix ensures the source code it pulls from channels
comes from authorized developers. As a channel author, you need to
specify the list of authorized developers in the
@ -4259,6 +4261,18 @@ pair---i.e., the commit that introduced @file{.guix-authorizations}, and
the fingerprint of the OpenPGP used to sign it.
@end enumerate
Before pushing to your public Git repository, you can run @command{guix
git-authenticate} to verify that you did sign all the commits you are
about to push with an authorized key:
@example
guix git authenticate @var{commit} @var{signer}
@end example
@noindent
where @var{commit} and @var{signer} are your channel introduction.
@xref{Invoking guix git authenticate}, for details.
Publishing a signed channel requires discipline: any mistake, such as an
unsigned commit or a commit signed by an unauthorized key, will prevent
users from pulling from your channel---well, that's the whole point of
@ -4865,6 +4879,7 @@ easily distributed to users who do not run Guix.
* Invoking guix environment:: Setting up development environments.
* Invoking guix pack:: Creating software bundles.
* The GCC toolchain:: Working with languages supported by GCC.
* Invoking guix git authenticate:: Authenticating Git repositories.
@end menu
@node Invoking guix environment
@ -5602,6 +5617,68 @@ The package @code{gfortran-toolchain} provides a complete GCC toolchain
for Fortran development. For other languages, please use
@samp{guix search gcc toolchain} (@pxref{guix-search,, Invoking guix package}).
@node Invoking guix git authenticate
@section Invoking @command{guix git authenticate}
The @command{guix git authenticate} command authenticates a Git checkout
following the same rule as for channels (@pxref{channel-authentication,
channel authentication}). That is, starting from a given commit, it
ensures that all subsequent commits are signed by an OpenPGP key whose
fingerprint appears in the @file{.guix-authorizations} file of its
parent commit(s).
You will find this command useful if you maintain a channel. But in
fact, this authentication mechanism is useful in a broader context, so
you might want to use it for Git repositories that have nothing to do
with Guix.
The general syntax is:
@example
guix git authenticate @var{commit} @var{signer} [@var{options}@dots{}]
@end example
By default, this command authenticates the Git checkout in the current
directory; it outputs nothing and exits with exit code zero on success
and non-zero on failure. @var{commit} above denotes the first commit
where authentication takes place, and @var{signer} is the OpenPGP
fingerprint of public key used to sign @var{commit}. Together, they
form a ``channel introduction'' (@pxref{channel-authentication, channel
introduction}). The options below allow you to fine-tune the process.
@table @code
@item --repository=@var{directory}
@itemx -r @var{directory}
Open the Git repository in @var{directory} instead of the current
directory.
@item --keyring=@var{reference}
@itemx -k @var{reference}
Load OpenPGP keyring from @var{reference}, the reference of a branch
such as @code{origin/keyring} or @code{my-keyring}. The branch must
contain OpenPGP public keys in @file{.key} files, either in binary form
or ``ASCII-armored''. By default the keyring is loaded from the branch
named @code{keyring}.
@item --stats
Display commit signing statistics upon completion.
@item --cache-key=@var{key}
Previously-authenticated commits are cached in a file under
@file{~/.cache/guix/authentication}. This option forces the cache to be
stored in file @var{key} in that directory.
@item --historical-authorizations=@var{file}
By default, any commit whose parent commit(s) lack the
@file{.guix-authorizations} file is considered inauthentic. In
contrast, this option considers the authorizations in @var{file} for any
commit that lacks @file{.guix-authorizations}. The format of @var{file}
is the same as that of @file{.guix-authorizations}
(@pxref{channel-authorizations, @file{.guix-authorizations} format}).
@end table
@c *********************************************************************
@node Programming Interface
@chapter Programming Interface

63
guix/scripts/git.scm Normal file
View File

@ -0,0 +1,63 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
(define-module (guix scripts git)
#:use-module (ice-9 match)
#:use-module (guix ui)
#:export (guix-git))
(define (show-help)
(display (G_ "Usage: guix git COMMAND ARGS...
Operate on Git repositories.\n"))
(newline)
(display (G_ "The valid values for ACTION are:\n"))
(newline)
(display (G_ "\
authenticate verify commit signatures and authorizations\n"))
(newline)
(display (G_ "
-h, --help display this help and exit"))
(display (G_ "
-V, --version display version information and exit"))
(newline)
(show-bug-report-information))
(define %sub-commands '("authenticate"))
(define (resolve-sub-command name)
(let ((module (resolve-interface
`(guix scripts git ,(string->symbol name))))
(proc (string->symbol (string-append "guix-git-" name))))
(module-ref module proc)))
(define (guix-git . args)
(with-error-handling
(match args
(()
(format (current-error-port)
(G_ "guix git: missing sub-command~%")))
((or ("-h") ("--help"))
(show-help)
(exit 0))
((or ("-V") ("--version"))
(show-version-and-exit "guix git"))
((sub-command args ...)
(if (member sub-command %sub-commands)
(apply (resolve-sub-command sub-command) args)
(format (current-error-port)
(G_ "guix git: invalid sub-command~%")))))))

View File

@ -0,0 +1,179 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
(define-module (guix scripts git authenticate)
#:use-module (git)
#:use-module (guix ui)
#:use-module (guix scripts)
#:use-module (guix git-authenticate)
#:autoload (guix openpgp) (openpgp-format-fingerprint
openpgp-public-key-fingerprint)
#:use-module ((guix channels) #:select (openpgp-fingerprint))
#:use-module ((guix git) #:select (with-git-error-handling))
#:use-module (guix progress)
#:use-module (guix base64)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-26)
#:use-module (srfi srfi-37)
#:use-module (ice-9 format)
#:use-module (ice-9 match)
#:export (guix-git-authenticate))
;;; Commentary:
;;;
;;; Authenticate a Git checkout by reading '.guix-authorizations' files and
;;; following the "authorizations invariant" also used by (guix channels).
;;;
;;; Code:
(define %options
;; Specifications of the command-line options.
(list (option '(#\h "help") #f #f
(lambda args
(show-help)
(exit 0)))
(option '(#\V "version") #f #f
(lambda args
(show-version-and-exit "guix git authenticate")))
(option '(#\r "repository") #t #f
(lambda (opt name arg result)
(alist-cons 'directory arg result)))
(option '(#\e "end") #t #f
(lambda (opt name arg result)
(alist-cons 'end-commit (string->oid arg) result)))
(option '(#\k "keyring") #t #f
(lambda (opt name arg result)
(alist-cons 'keyring-reference arg result)))
(option '("cache-key") #t #f
(lambda (opt name arg result)
(alist-cons 'cache-key arg result)))
(option '("historical-authorizations") #t #f
(lambda (opt name arg result)
(alist-cons 'historical-authorizations arg
result)))
(option '("stats") #f #f
(lambda (opt name arg result)
(alist-cons 'show-stats? #t result)))))
(define %default-options
'((directory . ".")
(keyring-reference . "keyring")))
(define (show-stats stats)
"Display STATS, an alist containing commit signing stats as returned by
'authenticate-repository'."
(format #t (G_ "Signing statistics:~%"))
(for-each (match-lambda
((signer . count)
(format #t " ~a ~10d~%"
(openpgp-format-fingerprint
(openpgp-public-key-fingerprint signer))
count)))
(sort stats
(match-lambda*
(((_ . count1) (_ . count2))
(> count1 count2))))))
(define (show-help)
(display (G_ "Usage: guix git authenticate COMMIT SIGNER [OPTIONS...]
Authenticate the given Git checkout using COMMIT/SIGNER as its introduction.\n"))
(display (G_ "
-r, --repository=DIRECTORY
open the Git repository at DIRECTORY"))
(display (G_ "
-k, --keyring=REFERENCE
load keyring from REFERENCE, a Git branch"))
(display (G_ "
--stats display commit signing statistics upon completion"))
(display (G_ "
--cache-key=KEY cache authenticated commits under KEY"))
(display (G_ "
--historical-authorizations=FILE
read historical authorizations from FILE"))
(newline)
(display (G_ "
-h, --help display this help and exit"))
(display (G_ "
-V, --version display version information and exit"))
(newline)
(show-bug-report-information))
;;;
;;; Entry point.
;;;
(define (guix-git-authenticate . args)
(define options
(parse-command-line args %options (list %default-options)
#:build-options? #f))
(define (command-line-arguments lst)
(reverse (filter-map (match-lambda
(('argument . arg) arg)
(_ #f))
lst)))
(define commit-short-id
(compose (cut string-take <> 7) oid->string commit-id))
(define (make-reporter start-commit end-commit commits)
(format (current-error-port)
(G_ "Authenticating commits ~a to ~a (~h new \
commits)...~%")
(commit-short-id start-commit)
(commit-short-id end-commit)
(length commits))
(if (isatty? (current-error-port))
(progress-reporter/bar (length commits))
progress-reporter/silent))
(with-error-handling
(with-git-error-handling
(match (command-line-arguments options)
((commit signer)
(let* ((directory (assoc-ref options 'directory))
(show-stats? (assoc-ref options 'show-stats?))
(keyring (assoc-ref options 'keyring-reference))
(repository (repository-open directory))
(end (match (assoc-ref options 'end-commit)
(#f (reference-target
(repository-head repository)))
(oid oid)))
(history (match (assoc-ref options 'historical-authorizations)
(#f '())
(file (call-with-input-file file
read-authorizations))))
(cache-key (or (assoc-ref options 'cache-key)
(repository-cache-key repository))))
(define stats
(authenticate-repository repository (string->oid commit)
(openpgp-fingerprint signer)
#:end end
#:keyring-reference keyring
#:historical-authorizations history
#:cache-key cache-key
#:make-reporter make-reporter))
(when (and show-stats? (not (null? stats)))
(show-stats stats))))
(_
(leave (G_ "wrong number of arguments; \
expected COMMIT and SIGNER~%")))))))

View File

@ -53,6 +53,8 @@ guix/scripts/upgrade.scm
guix/scripts/search.scm
guix/scripts/show.scm
guix/scripts/gc.scm
guix/scripts/git.scm
guix/scripts/git/authenticate.scm
guix/scripts/hash.scm
guix/scripts/import.scm
guix/scripts/import/cran.scm

View File

@ -0,0 +1,56 @@
# GNU Guix --- Functional package management for GNU
# Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
#
# This file is part of GNU Guix.
#
# GNU Guix is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or (at
# your option) any later version.
#
# GNU Guix is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
#
# Test the 'guix git authenticate' command-line utility.
#
# Skip if we're not in a Git checkout.
[ -d "$abs_top_srcdir/.git" ] || exit 77
# Skip if there's no 'keyring' branch.
guile -c '(use-modules (git))
(member "refs/heads/keyring" (branch-list (repository-open ".")))' || \
exit 77
# Keep in sync with '%default-channels' in (guix channels)!
intro_commit="9edb3f66fd807b096b48283debdcddccfea34bad"
intro_signer="BBB0 2DDF 2CEA F6A8 0D1D E643 A2A0 6DF2 A33A 54FA"
cache_key="test-$$"
guix git authenticate "$intro_commit" "$intro_signer" \
--cache-key="$cache_key" --stats \
--end=9549f0283a78fe36f2d4ff2a04ef8ad6b0c02604
rm "$XDG_CACHE_HOME/guix/authentication/$cache_key"
# Commit and signer of the 'v1.0.0' tag.
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 fail because these commits lack '.guix-authorizations'.
if guix git authenticate "$v1_0_0_commit" "$v1_0_0_signer" \
--cache-key="$cache_key" --end="$v1_0_1_commit";
then false; else true; fi
# This should work thanks to '--historical-authorizations'.
guix git authenticate "$v1_0_0_commit" "$v1_0_0_signer" \
--cache-key="$cache_key" --end="$v1_0_1_commit" --stats \
--historical-authorizations="$abs_top_srcdir/etc/historical-authorizations"