From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp2 ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms11 with LMTPS id VT2mIpvkAl+xZwAA0tVLHw (envelope-from ) for ; Mon, 06 Jul 2020 08:45:15 +0000 Received: from aspmx1.migadu.com ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp2 with LMTPS id UKgMHpvkAl/RZgAAB5/wlQ (envelope-from ) for ; Mon, 06 Jul 2020 08:45:15 +0000 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id 31BAC9408E8 for ; Mon, 6 Jul 2020 08:45:14 +0000 (UTC) Received: from localhost ([::1]:55726 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1jsMkS-0004f9-OU for larch@yhetil.org; Mon, 06 Jul 2020 04:45:12 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:37798) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1jsMkJ-0004dG-SX for guix-patches@gnu.org; Mon, 06 Jul 2020 04:45:04 -0400 Received: from debbugs.gnu.org ([209.51.188.43]:50268) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1jsMkJ-0000bu-Io for guix-patches@gnu.org; Mon, 06 Jul 2020 04:45:03 -0400 Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1jsMkJ-0005rM-HX for guix-patches@gnu.org; Mon, 06 Jul 2020 04:45:03 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#42225] [PATCH 4/5] Add 'guix git-authenticate'. Resent-From: Ludovic =?UTF-8?Q?Court=C3=A8s?= Original-Sender: "Debbugs-submit" Resent-CC: guix-patches@gnu.org Resent-Date: Mon, 06 Jul 2020 08:45:03 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 42225 X-GNU-PR-Package: guix-patches X-GNU-PR-Keywords: patch To: 42225@debbugs.gnu.org Cc: Ludovic =?UTF-8?Q?Court=C3=A8s?= Received: via spool by 42225-submit@debbugs.gnu.org id=B42225.159402506422425 (code B ref 42225); Mon, 06 Jul 2020 08:45:03 +0000 Received: (at 42225) by debbugs.gnu.org; 6 Jul 2020 08:44:24 +0000 Received: from localhost ([127.0.0.1]:33575 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1jsMjY-0005pU-N1 for submit@debbugs.gnu.org; Mon, 06 Jul 2020 04:44:24 -0400 Received: from eggs.gnu.org ([209.51.188.92]:51392) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1jsMjS-0005oL-I9 for 42225@debbugs.gnu.org; Mon, 06 Jul 2020 04:44:15 -0400 Received: from fencepost.gnu.org ([2001:470:142:3::e]:50388) by eggs.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1jsMjN-0000Tl-8s; Mon, 06 Jul 2020 04:44:05 -0400 Received: from [2a01:e0a:1d:7270:af76:b9b:ca24:c465] (port=50318 helo=gnu.org) by fencepost.gnu.org with esmtpsa (TLS1.2:DHE_RSA_AES_256_CBC_SHA1:256) (Exim 4.82) (envelope-from ) id 1jsMjM-0000pX-M3; Mon, 06 Jul 2020 04:44:05 -0400 From: Ludovic =?UTF-8?Q?Court=C3=A8s?= Date: Mon, 6 Jul 2020 10:43:55 +0200 Message-Id: <20200706084356.13013-4-ludo@gnu.org> X-Mailer: git-send-email 2.27.0 In-Reply-To: <20200706084356.13013-1-ludo@gnu.org> References: <20200706084356.13013-1-ludo@gnu.org> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Spam-Score: -2.3 (--) X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-Spam-Score: -1.0 (-) X-BeenThere: guix-patches@gnu.org List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: guix-patches-bounces+larch=yhetil.org@gnu.org Sender: "Guix-patches" X-Scanner: scn0 Authentication-Results: aspmx1.migadu.com; dkim=none; dmarc=none; spf=pass (aspmx1.migadu.com: domain of guix-patches-bounces@gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=guix-patches-bounces@gnu.org X-Spam-Score: -0.01 X-TUID: eEWHNgwqIny0 * guix/scripts/git-authenticate.scm: New file. * tests/guix-git-authenticate.sh: New files. * Makefile.am (MODULES): Add the former. (SH_TESTS): Add the latter. * 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-authenticate.scm'. --- Makefile.am | 2 + doc/guix.texi | 83 +++++++++++++- guix/scripts/git-authenticate.scm | 176 ++++++++++++++++++++++++++++++ po/guix/POTFILES.in | 1 + tests/guix-git-authenticate.sh | 56 ++++++++++ 5 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 guix/scripts/git-authenticate.scm create mode 100644 tests/guix-git-authenticate.sh diff --git a/Makefile.am b/Makefile.am index e125e07e99..2058f9d1ea 100644 --- a/Makefile.am +++ b/Makefile.am @@ -281,6 +281,7 @@ MODULES = \ guix/scripts/publish.scm \ guix/scripts/edit.scm \ guix/scripts/size.scm \ + guix/scripts/git-authenticate.scm \ guix/scripts/graph.scm \ guix/scripts/weather.scm \ guix/scripts/container.scm \ @@ -463,6 +464,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 \ diff --git a/doc/guix.texi b/doc/guix.texi index ce17c2de8e..77e2971acd 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -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 @@ -4862,9 +4876,10 @@ pack} command allows you to create @dfn{application bundles} that can be easily distributed to users who do not run Guix. @menu -* Invoking guix environment:: Setting up development environments. -* Invoking guix pack:: Creating software bundles. -* The GCC toolchain:: Working with languages supported by GCC. +* 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 diff --git a/guix/scripts/git-authenticate.scm b/guix/scripts/git-authenticate.scm new file mode 100644 index 0000000000..e3ba767675 --- /dev/null +++ b/guix/scripts/git-authenticate.scm @@ -0,0 +1,176 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2020 Ludovic Courtès +;;; +;;; 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 . + +(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)))))))) diff --git a/po/guix/POTFILES.in b/po/guix/POTFILES.in index 62b3cbf4e4..bd2aacd164 100644 --- a/po/guix/POTFILES.in +++ b/po/guix/POTFILES.in @@ -53,6 +53,7 @@ guix/scripts/upgrade.scm guix/scripts/search.scm guix/scripts/show.scm guix/scripts/gc.scm +guix/scripts/git-authenticate.scm guix/scripts/hash.scm guix/scripts/import.scm guix/scripts/import/cran.scm diff --git a/tests/guix-git-authenticate.sh b/tests/guix-git-authenticate.sh new file mode 100644 index 0000000000..a149584421 --- /dev/null +++ b/tests/guix-git-authenticate.sh @@ -0,0 +1,56 @@ +# GNU Guix --- Functional package management for GNU +# Copyright © 2020 Ludovic Courtès +# +# 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 . + +# +# 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" -- 2.26.2