From debbugs-submit-bounces@debbugs.gnu.org Mon Jun 01 10:07:13 2020 Received: (at 22883) by debbugs.gnu.org; 1 Jun 2020 14:07:13 +0000 Received: from localhost ([127.0.0.1]:36678 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1jfl5t-00061a-4p for submit@debbugs.gnu.org; Mon, 01 Jun 2020 10:07:13 -0400 Received: from eggs.gnu.org ([209.51.188.92]:57102) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1jfl5r-00061N-Ei for 22883@debbugs.gnu.org; Mon, 01 Jun 2020 10:07:12 -0400 Received: from fencepost.gnu.org ([2001:470:142:3::e]:49480) by eggs.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1jfl5m-000534-92 for 22883@debbugs.gnu.org; Mon, 01 Jun 2020 10:07:06 -0400 Received: from [2a01:e0a:1d:7270:af76:b9b:ca24:c465] (port=51086 helo=ribbon) by fencepost.gnu.org with esmtpsa (TLS1.2:RSA_AES_256_CBC_SHA1:256) (Exim 4.82) (envelope-from ) id 1jfl5l-000688-PF for 22883@debbugs.gnu.org; Mon, 01 Jun 2020 10:07:06 -0400 From: =?utf-8?Q?Ludovic_Court=C3=A8s?= To: 22883@debbugs.gnu.org Subject: Channel introductions References: <87io14sqoa.fsf@dustycloud.org> <87h9ep8gxk.fsf@gnu.org> <20160426001359.GA23088@jasmine> <874majg0z8.fsf@gnu.org> <87bn3iz1xc.fsf_-_@gnu.org> <87wpket748.fsf@gnu.org> <87bmkwm8ed.fsf@gnu.org> <87png9o8i2.fsf@elephly.net> <87fth4bj6y.fsf@gnu.org> <87bln9oupo.fsf@gnu.org> <87wo5vfuxi.fsf@gnu.org> <87o8qjekt7.fsf@gnu.org> Date: Mon, 01 Jun 2020 16:07:04 +0200 In-Reply-To: <87o8qjekt7.fsf@gnu.org> ("Ludovic \=\?utf-8\?Q\?Court\=C3\=A8s\=22'\?\= \=\?utf-8\?Q\?s\?\= message of "Tue, 19 May 2020 22:23:00 +0200") Message-ID: <87v9kanalz.fsf_-_@gnu.org> User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/26.3 (gnu/linux) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 22883 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) --=-=-= Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable Hi! Ludovic Court=C3=A8s skribis: > The good news with this model is that an adversary cannot trick users > into fetching an unrelated branch where the authorizations would be > different: they can always detect that it=E2=80=99s a disconnected branch= or > that it=E2=80=99s not a fast-forward pull. > > The bad news is that this also prevents =E2=80=9Cunauthorized forks=E2=80= =9D in general. > Unless Guix folks explicitly push a commit authorizing the key of the > person who forks, commits by that person will appear as unauthorized. > > So we need an extra mechanism to say: =E2=80=9Cthis fork starts here=E2= =80=9D. However, > modifications to that piece of information must be detectable so that > one cannot serve a malicious fork that pretends to forego history. We have two issues to address: (1) bootstraping trust in a channel the first time =E2=80=98guix pull=E2=80=99 obtains it, and (2) supporting =E2= =80=9Cunauthorized=E2=80=9D forks as described above. The two are very similar. I think we need a way to =E2=80=9Cintroduce=E2=80=9D a channel to its users= that goes beyond a mere URL. It should be possible to obtain a =E2=80=9Cchannel introduction=E2=80=9D ou= t-of-band (i.e., not in the channel=E2=80=99s repo). For example, the introduction t= o the official =E2=80=98guix=E2=80=99 channel would be in =E2=80=98%default-chann= els=E2=80=99, in (guix channels), which users obtained in the binary tarball or ISO. For other channels where there=E2=80=99s no practical out-of-band transmission of the introduction, it would very much be trust-on-first-use (TOFU). I think the introduction needs to include: 1. The commit used as a starting point for =E2=80=98.guix-authorizations= =E2=80=99 checks. 2. The OpenPGP fingerprint of the signer of this first commit. 3. A signature over this commit/fingerprint pair made by the signer of the commit. Rationale ~~~~~~~~~ 1. The =E2=80=98guix=E2=80=99 channel and others will have their authoriz= ation start long after their initial commit. Thus, authors need to state where to start the authentication process. (Currently the starting point is hard-coded in =E2=80=98Makefile.am=E2=80=99 and passed as an argume= nt to =E2=80=98build-aux/git-authenticate.scm=E2=80=99.) If that information were stored in =E2=80=98.guix-channel=E2=80=99, it= would be trivial for an attacker to fork the project (or push a new commit) and pretend the authentication process must not take previous commits into account. 2. The fingerprint of the signer of the initial commit makes it easy for =E2=80=98guix pull=E2=80=99 to verify that initial commit on its f= irst clone. 3. The signature over the commit/fingerprint pair makes sure that it was emitted by an authorized party. Without it, anyone could emit a channel introduction that skips over a range of commits. 4. When publishing a fork of a channel, one emits a new channel introduction. Users switching to the fork have to explicitly allow that new channel via its introduction; flipping the URL won=E2=80=99t = be enough because =E2=80=98guix pull=E2=80=99 would report unauthorized c= ommits. 5. The channel URL is not included in the introduction. However, the official URL is an important piece of information: it tells users this is where they=E2=80=99ll get the latest updates. It should be possible to create mirrors, but by default users should go to the official URL. They should be aware that mirrors can be outdated. I think the official URL can be stored in =E2=80=98.guix-channel=E2=80= =99 in the repo (which is subject to the authentication machinery). That way, =E2=80=98guix pull=E2=80=99 can let the user know if they=E2=80=99re t= alking to a mirror rather than to the official channel. Prototype ~~~~~~~~~ The attached code creates channel introductions. One implementation uses a =E2=80=9Ccompact=E2=80=9D binary encoding, which, one encoded in Rad= ix-64, looks like this: -----BEGIN GUIX CHANNEL INTRODUCTION----- R1hDSQAAAAAAFJdEzHtGNvr7dyyUrbjwWWG1s58WPORkVYqE/cadtAz7CQsRmT2a67UCvaMBA= bYC Sf2QDQMACgEJCxGZPZrrtQHLb2IAXtUHUCgoY29tbWl0ICI5NzQ0Y2M3YjQ2MzZmYWZiNzcyY= zk0 YWRiOGYwNTk2MWI1YjM5ZjE2IikgKHNpZ25lciAiM2NlNDY0NTU4YTg0ZmRjNjlkYjQwY2ZiM= Dkw YjExOTkzZDlhZWJiNSIpKYkCMwQAAQoAHRYhBDzkZFWKhP3GnbQM+wkLEZk9muu1BQJe1QdQA= AoJ EAkLEZk9muu1SlUQAIQVO6JeNmDywM5bAVIktX0VbPmPl0CNCt2oTN8jPZ30lwa3osckOEa2L= qQv 0xP59n9tPUue8rXoACcdZ4RS7fZeWjn9qBGCr7h/cQIS0p7II08jHccovvEfgQGxqQCe5hZ++= Ehv OyKV84bG3VOxLFe7B2wIqoIAtmFYCeNmqBfOixSWgzCwC/G5fh7frXR+O9BzvhfBVeFSgzgay= Nnu GxgO+Jx7ZhnxR8rRIxrOxCEUjbi8ilIhw++z4ea7g4yTFCkmpPY54HIhzgERanm0/iJUBj9nv= 9kQ TXnJ2dcFeZL7b+fWdh8kiKaTgYKV3BGQOKpdqSBQcV9Ys+FFJ+I5ZRCzmTxafPLmR8eGM3aT4= Fld drCjSHga4hbppQJPpNyKsrhwJ4d5tLuLHYUNlXEhcYi11Uo2i30T+TbOynD/uWFRY1+s0ffI1= LmX FhSaqk44jfQoXYeeS8FO471ze3087fsp2WVwNZ5kdPOFNa1Iv+uj/CDILsb4kooRrawQod2W7= eb6 jMCayWCUApah22BisQepkFVtKeEAG6aSkIa6Haq4ZrEtzz9gu4n6Pmgv60+n3t77rb7GbPafd= SKu LiJ8PeYSBR3II2YdjIn9PTQ75Xg90IeFxt5PF70q/yb/WoRTIokHFho1QEmbmAm1HBO8SVlmH= G+X 8WFn7ex7cx3iYH6H =3DxZUu -----END GUIX CHANNEL INTRODUCTION----- (guix channels) could provide a =E2=80=98channel-introduction->channel=E2= =80=99 procedure. In ~/.config/guix/channels.scm, one would write: (list (channel-introduction->channel "https://=E2=80=A6" "\ -----BEGIN GUIX CHANNEL INTRODUCTION----- =E2=80=A6")) The introduction needs to be decoded and checked only the first time =E2=80=98guix pull=E2=80=99 encounters a channel. This verbose interface creates an incentive to create a =E2=80=98guix chann= el=E2=80=99 command that could make it easier to add a new channel. Thoughts? Ludo=E2=80=99. --=-=-= Content-Type: text/plain Content-Disposition: inline; filename=channel-intro.scm Content-Description: the code (use-modules (guix) (gcrypt pk-crypto) (gcrypt base16) (gcrypt base64) (srfi srfi-1) (srfi srfi-71) (ice-9 popen) (ice-9 match) (guix utils) (guix build utils) ((guix openpgp) #:select (openpgp-format-fingerprint)) (rnrs bytevectors) (rnrs io ports)) (define url "https://git.savannah.gnu.org/git/guix.git") (define commit "9744cc7b4636fafb772c94adb8f05961b5b39f16") (define signer (base16-string->bytevector "3ce464558a84fdc69db40cfb090b11993d9aebb5")) (define (sign-introduction commit signer) (let ((pipe pids (filtered-port (list (which "gpg") "-s" "-u" (bytevector->base16-string signer)) (open-input-string (object->string `((commit ,commit) (signer ,(bytevector->base16-string signer)))))))) (let ((bv (get-bytevector-all pipe))) (and (every (compose zero? cdr waitpid) pids) bv)))) (define (channel-introduction commit signer) "Return an sexp representing a channel introduction." `(channel-introduction (version 0) (commit ,commit) (signer ,(openpgp-format-fingerprint signer)) (signature ,(base64-encode (sign-introduction commit signer))))) (define (radix-64-encode bv) (define (int24->bv int) (let ((bv (make-bytevector 3))) (bytevector-u8-set! bv 0 (ash (logand int #xff0000) -16)) (bytevector-u8-set! bv 1 (ash (logand int #x00ff00) -8)) (bytevector-u8-set! bv 2 (logand int #x0000ff)) bv)) (let ((str (base64-encode bv))) (string-append "-----BEGIN GUIX CHANNEL INTRODUCTION-----\n\n" (insert-newlines str) "=" (base64-encode (int24->bv ((@@ (guix openpgp) crc24) bv))) "\n\n" "-----END GUIX CHANNEL INTRODUCTION-----\n"))) (define* (insert-newlines str #:optional (line-length 76)) "Insert newlines in STR every LINE-LENGTH characters." (let loop ((result '()) (str str)) (if (string-null? str) (string-concatenate-reverse result) (let* ((length (min (string-length str) line-length)) (prefix (string-take str length)) (suffix (string-drop str length))) (loop (cons (string-append prefix "\n") result) suffix))))) (radix-64-encode (string->utf8 (object->string (channel-introduction commit signer)))) (define (channel-introduction/compact commit signer) "Return a channel introduction as a bytevector, in compact binary encoding." (let ((port get (open-bytevector-output-port))) (put-bytevector port (u8-list->bytevector (map char->integer (string->list "GXCI")))) (put-bytevector port #vu8(0 0 0 0)) ;version (let ((commit (base16-string->bytevector commit)) (len (make-bytevector 2))) (bytevector-u16-set! len 0 (bytevector-length commit) (endianness big)) (put-bytevector port len) (put-bytevector port commit)) (put-bytevector port signer) (let ((signature (sign-introduction commit signer)) (len (make-bytevector 2))) (bytevector-u16-set! len 0 (bytevector-length signature) (endianness big)) (put-bytevector port len) (put-bytevector port signature)) (force-output port) (get))) (radix-64-encode (channel-introduction/compact commit signer)) --=-=-=--