bug-guix
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

bug#22883: Channel introductions


From: Ludovic Courtès
Subject: bug#22883: Channel introductions
Date: Mon, 01 Jun 2020 16:07:04 +0200
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/26.3 (gnu/linux)

Hi!

Ludovic Courtès <ludo@gnu.org> 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’s a disconnected branch or
> that it’s not a fast-forward pull.
>
> The bad news is that this also prevents “unauthorized forks” 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: “this fork starts here”.  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 ‘guix pull’ obtains it, and (2) supporting “unauthorized”
forks as described above.  The two are very similar.

I think we need a way to “introduce” a channel to its users that goes
beyond a mere URL.

It should be possible to obtain a “channel introduction” out-of-band
(i.e., not in the channel’s repo).  For example, the introduction to the
official ‘guix’ channel would be in ‘%default-channels’, in (guix
channels), which users obtained in the binary tarball or ISO.  For other
channels where there’s 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 ‘.guix-authorizations’
     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 ‘guix’ channel and others will have their authorization 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 ‘Makefile.am’ and passed as an argument to
     ‘build-aux/git-authenticate.scm’.)

     If that information were stored in ‘.guix-channel’, 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 ‘guix pull’ to verify that initial commit on its first 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’t be
     enough because ‘guix pull’ would report unauthorized commits.

  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’ll 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 ‘.guix-channel’ in the
     repo (which is subject to the authentication machinery).  That way,
     ‘guix pull’ can let the user know if they’re talking to a mirror
     rather than to the official channel.

Prototype
~~~~~~~~~

The attached code creates channel introductions.  One implementation
uses a “compact” binary encoding, which, one encoded in Radix-64, looks
like this:

  -----BEGIN GUIX CHANNEL INTRODUCTION-----

  R1hDSQAAAAAAFJdEzHtGNvr7dyyUrbjwWWG1s58WPORkVYqE/cadtAz7CQsRmT2a67UCvaMBAbYC
  Sf2QDQMACgEJCxGZPZrrtQHLb2IAXtUHUCgoY29tbWl0ICI5NzQ0Y2M3YjQ2MzZmYWZiNzcyYzk0
  YWRiOGYwNTk2MWI1YjM5ZjE2IikgKHNpZ25lciAiM2NlNDY0NTU4YTg0ZmRjNjlkYjQwY2ZiMDkw
  YjExOTkzZDlhZWJiNSIpKYkCMwQAAQoAHRYhBDzkZFWKhP3GnbQM+wkLEZk9muu1BQJe1QdQAAoJ
  EAkLEZk9muu1SlUQAIQVO6JeNmDywM5bAVIktX0VbPmPl0CNCt2oTN8jPZ30lwa3osckOEa2LqQv
  0xP59n9tPUue8rXoACcdZ4RS7fZeWjn9qBGCr7h/cQIS0p7II08jHccovvEfgQGxqQCe5hZ++Ehv
  OyKV84bG3VOxLFe7B2wIqoIAtmFYCeNmqBfOixSWgzCwC/G5fh7frXR+O9BzvhfBVeFSgzgayNnu
  GxgO+Jx7ZhnxR8rRIxrOxCEUjbi8ilIhw++z4ea7g4yTFCkmpPY54HIhzgERanm0/iJUBj9nv9kQ
  TXnJ2dcFeZL7b+fWdh8kiKaTgYKV3BGQOKpdqSBQcV9Ys+FFJ+I5ZRCzmTxafPLmR8eGM3aT4Fld
  drCjSHga4hbppQJPpNyKsrhwJ4d5tLuLHYUNlXEhcYi11Uo2i30T+TbOynD/uWFRY1+s0ffI1LmX
  FhSaqk44jfQoXYeeS8FO471ze3087fsp2WVwNZ5kdPOFNa1Iv+uj/CDILsb4kooRrawQod2W7eb6
  jMCayWCUApah22BisQepkFVtKeEAG6aSkIa6Haq4ZrEtzz9gu4n6Pmgv60+n3t77rb7GbPafdSKu
  LiJ8PeYSBR3II2YdjIn9PTQ75Xg90IeFxt5PF70q/yb/WoRTIokHFho1QEmbmAm1HBO8SVlmHG+X
  8WFn7ex7cx3iYH6H
  =xZUu

  -----END GUIX CHANNEL INTRODUCTION-----

(guix channels) could provide a ‘channel-introduction->channel’
procedure.  In ~/.config/guix/channels.scm, one would write:

  (list (channel-introduction->channel "https://…";
                                       "\
-----BEGIN GUIX CHANNEL INTRODUCTION-----
…"))

The introduction needs to be decoded and checked only the first time
‘guix pull’ encounters a channel.

This verbose interface creates an incentive to create a ‘guix channel’
command that could make it easier to add a new channel.

Thoughts?

Ludo’.

(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))


reply via email to

[Prev in Thread] Current Thread [Next in Thread]