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 kDjTJ18U9V6VZwAA0tVLHw (envelope-from ) for ; Thu, 25 Jun 2020 21:17:19 +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 oPa9I18U9V7FdQAAB5/wlQ (envelope-from ) for ; Thu, 25 Jun 2020 21:17:19 +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 41C849403E6 for ; Thu, 25 Jun 2020 21:17:19 +0000 (UTC) Received: from localhost ([::1]:35838 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1joZFG-0006Fv-3C for larch@yhetil.org; Thu, 25 Jun 2020 17:17:18 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:49082) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1joZF1-0006AE-8r for guix-patches@gnu.org; Thu, 25 Jun 2020 17:17:04 -0400 Received: from debbugs.gnu.org ([209.51.188.43]:58051) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1joZF0-0000lc-Th for guix-patches@gnu.org; Thu, 25 Jun 2020 17:17:03 -0400 Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1joZF0-0002W4-Py for guix-patches@gnu.org; Thu, 25 Jun 2020 17:17:02 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#42048] [PATCH 2/6] channels: Make channel introductions public. Resent-From: Ludovic =?UTF-8?Q?Court=C3=A8s?= Original-Sender: "Debbugs-submit" Resent-CC: guix-patches@gnu.org Resent-Date: Thu, 25 Jun 2020 21:17:02 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 42048 X-GNU-PR-Package: guix-patches X-GNU-PR-Keywords: patch To: 42048@debbugs.gnu.org Cc: Ludovic =?UTF-8?Q?Court=C3=A8s?= Received: via spool by 42048-submit@debbugs.gnu.org id=B42048.15931197889558 (code B ref 42048); Thu, 25 Jun 2020 21:17:02 +0000 Received: (at 42048) by debbugs.gnu.org; 25 Jun 2020 21:16:28 +0000 Received: from localhost ([127.0.0.1]:41354 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1joZES-0002Tz-2q for submit@debbugs.gnu.org; Thu, 25 Jun 2020 17:16:28 -0400 Received: from eggs.gnu.org ([209.51.188.92]:34500) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1joZEN-0002T9-W4 for 42048@debbugs.gnu.org; Thu, 25 Jun 2020 17:16:25 -0400 Received: from fencepost.gnu.org ([2001:470:142:3::e]:45250) by eggs.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1joZEI-0000Uq-PM; Thu, 25 Jun 2020 17:16:18 -0400 Received: from [2a01:e0a:1d:7270:af76:b9b:ca24:c465] (port=59608 helo=gnu.org) by fencepost.gnu.org with esmtpsa (TLS1.2:DHE_RSA_AES_256_CBC_SHA1:256) (Exim 4.82) (envelope-from ) id 1joZEG-0006HK-IB; Thu, 25 Jun 2020 17:16:17 -0400 From: Ludovic =?UTF-8?Q?Court=C3=A8s?= Date: Thu, 25 Jun 2020 23:16:01 +0200 Message-Id: <20200625211605.29316-2-ludo@gnu.org> X-Mailer: git-send-email 2.26.2 In-Reply-To: <20200625211605.29316-1-ludo@gnu.org> References: <20200625211605.29316-1-ludo@gnu.org> MIME-Version: 1.0 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: -3.3 (---) 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: 3.99 X-TUID: ETRDLUDCp3ON * guix/channels.scm (): Rename constructor to '%make-channel-introduction'. (make-channel-introduction): New procedure. * tests/channels.scm ("authenticate-channel, wrong first commit signer") ("authenticate-channel, .guix-authorizations"): Use 'make-channel-introduction' without '@@' and without third argument. * doc/guix.texi (Channels)[Channel Authentication, Specifying Channel Authorizations]: New subsections. --- doc/guix.texi | 117 ++++++++++++++++++++++++++++++++++++++++++++- guix/channels.scm | 14 ++++-- tests/channels.scm | 10 ++-- 3 files changed, 130 insertions(+), 11 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index 5b854ccbd4..a4bb52bb24 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -3975,8 +3975,52 @@ deploys Guix itself from the official GNU@tie{}Guix repository. This can be customized by defining @dfn{channels} in the @file{~/.config/guix/channels.scm} file. A channel specifies a URL and branch of a Git repository to be deployed, and @command{guix pull} can be instructed -to pull from one or more channels. In other words, channels can be used to -@emph{customize} and to @emph{extend} Guix, as we will see below. +to pull from one or more channels. In other words, channels can be used +to @emph{customize} and to @emph{extend} Guix, as we will see below. +Before that, some security considerations. + +@subsection 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 +commit that is fetched is signed by an authorized developer. The goal +is to protect from unauthorized modifications to the channel that would +lead users to run malicious code. + +As a user, you must be @dfn{introduced} to a channel so you can start +pulling from it and authenticate its code. The @dfn{channel +introduction} tells Guix how to authenticate the first commit of that +channel: + +As a user, you must provide a @dfn{channel introduction} in your +channels file so that Guix knows how to authenticate its first commit. +A channel specification, including its introduction, looks something +along these lines: + +@lisp +(channel + (name 'my-channel) + (url "https://example.org/my-channel.git") + (introduction + (make-channel-introduction + "6f0d8cc0d88abb59c324b2990bfee2876016bb86" + (openpgp-fingerprint + "CABB A931 C0FF EEC6 900D 0CFB 090B 1199 3D9A EBB5")))) +@end lisp + +The specification above shows the name and URL of the channel. The call +to @code{make-channel-introduction} above specifies that authentication +of this channel starts at commit @code{6f0d8cc@dots{}}, which is signed +by the OpenPGP key with fingerprint @code{CABB A931@dots{}}. + +For the main channel, called @code{guix}, you automatically get that +information from your Guix installation. For other channels, include +the channel introduction provided by the channel authors in your +@file{channels.scm} file. Make sure you retrieve the channel +introduction from a trusted source since that is the root of your trust. + +If you're curious about the authentication mechanics, read on! @subsection Using a Custom Guix Channel @@ -4150,6 +4194,75 @@ add a meta-data file @file{.guix-channel} that contains: (directory "guix")) @end lisp +@cindex channel authorizations +@subsection Specifying 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 +@file{.guix-authorizations} file in the channel's Git repository. The +authentication rule is simple: each commit must be signed by a key +listed in the @file{.guix-authorizations} file of its parent +commit(s)@footnote{Git commits form a @dfn{directed acyclic graph} +(DAG). Each commit can have zero or more parents; ``regular'' commits +have one parent and merge commits have two parent commits. Read +@uref{https://eagain.net/articles/git-for-computer-scientists/, @i{Git +for Computer Scientists}} for a great overview.} The +@file{.guix-authorizations} file looks like this: + +@lisp +;; Example '.guix-authorizations' file. + +(authorizations + (version 0) ;current file format version + + (("AD17 A21E F8AE D8F1 CC02 DBD9 F8AE D8F1 765C 61E3" + (name "alice")) + ("2A39 3FFF 68F4 EF7A 3D29 12AF 68F4 EF7A 22FB B2D5" + (name "bob")) + ("CABB A931 C0FF EEC6 900D 0CFB 090B 1199 3D9A EBB5" + (name "charlie")))) +@end lisp + +Each fingerprint is followed by optional key/value pairs, as in the +example above. Currently these key/value pairs are ignored. + +This authentication rule creates a chicken-and-egg issue: how do we +authenticate the first commit? Related to that: how do we deal with +channels whose repository history contains unsigned commits and lack +@file{.guix-authorizations}? And how do we fork existing channels? + +@cindex channel introduction +Channel introductions answer these questions by describing the first +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. + +To summarize, as the author of a channel, there are two things you have +to do to allow users to authenticate your code: + +@enumerate +@item +Introduce an initial @file{.guix-authorizations} in the channel's +repository. Do that in a signed commit (@pxref{Commit Access}, for +information on how to sign Git commits.) + +@item +Advertise the channel introduction, for instance on your channel's web +page. The channel introduction, as we saw above, is the commit/key +pair---i.e., the commit that introduced @file{.guix-authorizations}, and +the fingerprint of the OpenPGP used to sign it. +@end enumerate + +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 +authentication! Pay attention to merges in particular: merge commits +are considered authentic if and only if they are signed by a key present +in the @file{.guix-authorizations} file of @emph{both} branches. + @cindex primary URL, channels @subsection Primary URL diff --git a/guix/channels.scm b/guix/channels.scm index 1d4b50aa48..9859bfdda8 100644 --- a/guix/channels.scm +++ b/guix/channels.scm @@ -69,7 +69,9 @@ channel-location channel-introduction? - ;; accessors purposefully omitted for now. + make-channel-introduction + channel-introduction-first-signed-commit + channel-introduction-first-commit-signer openpgp-fingerprint->bytevector openpgp-fingerprint @@ -130,13 +132,19 @@ ;; commit so that only them may emit this introduction. Introductions are ;; used to bootstrap trust in a channel. (define-record-type - (make-channel-introduction first-signed-commit first-commit-signer - signature) + (%make-channel-introduction first-signed-commit first-commit-signer + signature) channel-introduction? (first-signed-commit channel-introduction-first-signed-commit) ;hex string (first-commit-signer channel-introduction-first-commit-signer) ;bytevector (signature channel-introduction-signature)) ;string +(define (make-channel-introduction commit signer) + "Return a new channel introduction: COMMIT is the introductory where +authentication starts, and SIGNER is the OpenPGP fingerprint (a bytevector) of +the signer of that commit." + (%make-channel-introduction commit signer #f)) + (define (openpgp-fingerprint->bytevector str) "Convert STR, an OpenPGP fingerprint (hexadecimal string with whitespace), to the corresponding bytevector." diff --git a/tests/channels.scm b/tests/channels.scm index 3a2c1d429b..016c3ad9db 100644 --- a/tests/channels.scm +++ b/tests/channels.scm @@ -430,12 +430,11 @@ (with-repository directory repository (let* ((commit1 (find-commit repository "first")) (commit2 (find-commit repository "second")) - (intro ((@@ (guix channels) make-channel-introduction) + (intro (make-channel-introduction (commit-id-string commit1) (openpgp-public-key-fingerprint (read-openpgp-packet - %ed25519bis-public-key-file)) ;different key - #f)) ;no signature + %ed25519bis-public-key-file)))) ;different key (channel (channel (name 'example) (url (string-append "file://" directory)) (introduction intro)))) @@ -486,12 +485,11 @@ (let* ((commit1 (find-commit repository "first")) (commit2 (find-commit repository "second")) (commit3 (find-commit repository "third")) - (intro ((@@ (guix channels) make-channel-introduction) + (intro (make-channel-introduction (commit-id-string commit1) (openpgp-public-key-fingerprint (read-openpgp-packet - %ed25519-public-key-file)) - #f)) ;no signature + %ed25519-public-key-file)))) (channel (channel (name 'example) (url (string-append "file://" directory)) (introduction intro)))) -- 2.26.2