Subject: bug#75552: Non-committers can't keep authenticated forks updated

From: Liliana Marie Prikler <[email-masked]>
Date: Wed, 15 Jan 2025 10:41:29 +0100

Am Dienstag, dem 14.01.2025 um 04:21 +0000 schrieb 45mg:
> --8<---------------cut here---------------start------------->8---
> When authenticating merge commits, intersection of authorized keys
> from all parents is used. That is fine in Guix proper, since all
> involved commits are under the control of the Guix committers,
> however it does not work that well for authenticating merge commits
> in forks.
> 
> When Guix fork is created (starting from Guix-proper commit A), new
> commit (authorizing the fork creator's signing key K) is created (I).
> Later, when update from Guix proper (U) is merged, new merge commit
> is created (M):
> 
>      M
>     / \
>    I   U
>     \ /
>      A
> 
> The M is signed with the K. However since the K is allowed by only
> one parent (I), it will not be in the set of authorized keys
> (intersection of keys from I and U). So, commit M cannot be
> authenticated.
> 
> Thus, an authenticated fork cannot be kept updated.
> --8<---------------cut here---------------end--------------->8---
For most use cases, this is a non-issue.  Assuming you are a single
committer to your fork, you can always rebase your changes on top of
Guix (if you're willing to bump the introductory commit) or sign the
changes to Guix with your own key (if you are willing to accept that
this changes the history).  With multiple committers, you will need to
do the latter.  Of course, you can also keep your own fork
unauthenticated, which might be preferable if you only do local work
anyway, but that's besides the issue here.

> […]
> 
> For commits that have multiple parents - ie. merge commits - we
> weaken the authorization invariant [6] as follows:
> 
> 1. If all parents have the primary introduction as their most recent
>    ancestor, then the invariant holds as usual.
>    
> 2. If one or more parents has the primary introduction as its most
>    recent ancestor (call these the 'primary parents'), and the rest 
>    have any of the additional introductions, then the merge commit is
>    authenticated if and only if:
>    a) it's signed by a key authorized in all of the primary parents,
>    AND
>    b) the /first parent/ [^] of the merge commit is a primary parent.
This does not state how the additional introductions are used, if at
all.  It may mean that the additional introductions are pointless other
than for blocking case 4.
   
> 3. If all parents have the same additional introduction as their most
>    recent ancestor, then the invariant holds as usual.
> 
> 4. If none of the parents have the primary introduction as their most
>    recent ancestor, nor do they have the same additional
>    introduction, then the merge commit cannot be authenticated.

> The idea is - the primary introduction is for the part of the tree
> under YOUR control. When you fork Guix and create your own branch,
> you use the initial commit on your branch as the primary channel
> introduction.  You add upstream Guix's primary channel introduction
> as an additional channel introduction. If you add anyone else's fork
> as a remote and pull one of their branches, you add their primary
> introduction as one of your additional introductions.
> 
> Thus, any merge into one of YOUR branches (ie. any branch with the
> primary introduction as the most recent ancestor) only needs to be
> signed by a key that's authorized on that branch.
> 
> But you can't merge into a branch from upstream Guix or someone
> else's authenticated fork (unless you're authorized to commit to
> those), because the first parent of the merge commit would not be a
> primary parent (see 2b) - it would be a commit on someone else's
> branch. And people not authorized by you can't merge into your branch
> either, because of 2a. And finally, you can't merge someone else's
> fork and upstream, or anything like that. The merge commit would not
> be authenticated in any of these cases.

> So What Do You Want From Me Anyway, 45mg?
> ========================================
> 
> I've tried to think of ways in which this modification to the
> behaviour of `guix git authenticate` could compromise security, but
> so far I haven't been able to think of any attacks it might enable.
> 
> Of course, this only means that /I/ haven't been able to think of
> anything wrong. You, dear reader, have the advantage of a unique
> perspective and a fresh view on this idea. So, I'm hoping that you'll
> be able to sniff out any fundamental issues with the design here.
I think this might still hide a serious flaw.  With the way *upstream*
authentication works.  Let's flip the example in [6] around a little
bit and construct the following:

-A---B---C---D
  \       \
   \       \-E---F---💀
    \               /
     \----G--H--I*-/
  
Both A and I* are introductory commits on their various branches.  In
💀, any committer who has valid keys in both F and I* can merge a
branch with unsigned commits, effectively voiding the invariant of
BCEF, e.g. by undoing any changes that happened there.  Of course, they
can do so with signed commits as well, given that they have commit
access to the main repository, but the point still holds that they may
introduce unsigned commits to any fork where their key is valid in.

Cheers