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

From: Saturanya Rahjane de Lasca <[email-masked]>
Date: Thu, 16 Jan 2025 22:13:32 +0100

Am Donnerstag, dem 16.01.2025 um 17:29 +0000 schrieb 45mg:
> Yeah, I know I can. My point is that people who use remote forks
> shouldn't have to rely on a trusted third party. We've figured out a
> way for upstream Guix not to have to, now let's try to extend that to
> forks.
Well, the same way already works for channels and hard forks.  It
really is soft forks that experience this issue – and even then there's
ways around it, as discussed.  

> > > 
> Since most future committers will take years to attain that status,
> and many (most?) Guix contributors can't commit (heh) to being
> committers, I think it would be a good thing for them to be able to
> make use of our security mechanisms.
They already can, see above.

I don't think asking would-be committers to soft-fork Guix is
worthwhile idea even with the proposed change.  Authoring your own
channel and contributing bits back to Guix suffices – and it really is
the things you contribute to Guix proper rather than your channels and
soft forks that make the cut.

> > W.r.t. keeping history intact, we had the following exchange on IRC
> > yesterday.
> > 
> > […]
> > So yeah, even for (branch-)local work at least some committers
> > prefer rebasing.
> 
> That seems to be a discussion about a merge commit in upstream Guix,
> not about the kind of merge commits that I'm trying to allow.
It is.  I just wanted to give some context.

> Again, not disputing that things work fine for people with commit
> access. Perhaps that is part of why this issue hasn't been addressed
> before :P
You may call us privileged – and yes, we are – but that doesn't change
the fact that weakening security weakens security.  

> > > 
> > > Let's imagine that the first example given there represents our
> > > fork of Guix, where the 'experiment' branch marks the beginning
> > > of our fork (and its channel introduction) and the 'master'
> > > branch tracks upstream Guix.  After `git rebase master`, the
> > > commit that used to be C4 is gone, and now C4' takes its place.
> > > It may contain the same changes, but it's a different commit - so
> > > it (and any commits that it's the parent of) has a different
> > > hash. So the channel introduction has changed, and so has the
> > > entire history of the `experimental` branch. So we need to force-
> > > pull.
> > Yes, that's one variant – the one where you need to keep bumping
> > your channel introductions.  The other direction would be to rebase
> > Guix changes on top of your local branch.  This keeps your channel
> > introduction as-is.
> 
> Ah, I see. Actually, I think that might work... if I create a
> 'upstream-backup' branch before rebasing, and reset the 'upstream'
> branch to that branch after, I can keep the full history of upstream,
> and authenticate it separately. And thanks to Ricardo's suggestion
> [12] to compare by Change-ID, it should even be possible to find
> corresponding commits between my fork and upstream. Looks like this
> solution meets all four requirements that I stated earlier, and it
> wouldn't be TOO annoying or complicated to script either.
Yes, it's the workflow I alluded to earlier: pull, authenticate, rebase
on your branch.

> One limitation I can think of is that you can't really verify whether
> a commit ostensibly rebased from master is actually the same commit
> (imagine 100 commits in a rebase, are you going to check the diff for
> each? Versus with a merge you only have to check that the merge
> commit looks sane). Then again, this is really only a problem if
> you're using someone ELSE's fork and need to ensure they've not gone
> mad or evil, which is not my personal use-case, and perhaps not that
> common.
Yeah, it really isn't too hard to think of 9000 commits bumping rust-
whatever.  We're not yet calling that Tuesday, but only because we lag
behind the Rust ecosystem as a whole.

> > > > 
> > > […]
> > > This led my to think of an attack that's possible with my design
> > > - if I want to screw with anyone `guix pull`ing from my fork, I
> > > can merge upstream into my fork branch, add a bunch of malicious
> > > commits, and then make the upstream branch ref point to the
> > > latest such commit.  Now anyone pulling from my fork will recieve
> > > the malicious commits as part of upstream's history - since no
> > > commit hashes needed to change, the pull is a regular fast-
> > > forward one, with no indication that anything is amiss.
> > > Authentication will succeed since the malicious merge commit has
> > > our fork as its (first) parent, and that parent has the primary
> > > introduction as its most recent ancestor.
> > > 
> > > The takeaway here is that anyone authorized via the primary
> > > introduction can fake new upstream commits.
> > Care to state how designating one introduction as "primary" adds to
> > security here?  
> 
> The problem I'm trying to solve is that you can't merge upstream into
> your fork unless you sign the merge commit with a key authorized in
> upstream, because of the intersections-of-parent-authorizations
> design.
> Tomas's solution was to do away with the 'intersections' requirement,
> and allow a key that's authorized for any parent to sign the merge
> commit; this is vulnerable to attacks like [4]. My solution falls
> somewhere in between - we keep the 'intersections' requirement,
> /except/ when one of the parent commits is a descendant of an
> introduction designated as the 'primary introduction'.
> 
> If you drop the 'primary introduction' designation (make all of them
> 'primary'), then you're basically dropping the intersections
> requirement. IOW, we've returned to the situation in Tomas's
> solution, where [4] is possible - anyone with even a single signed
> commit in Guix can now create a merge commit between Guix and your
> fork, even if their key was later removed from Guix.
Yeah, the point I'm trying to make here is that you still end up with
Tomas' solution as you can simply designate whatever primary you want.
The primary is not special in that sense.

> > 
> > > So what does this all of this mean for the statement of my
> > > design? Well, it means that we need to stop thinking in terms of
> > > 'which branch can be merged into which?' and more in terms of
> > > 'which merge commits can be authenticated?'. And the answer to
> > > that question, with my design, would be:
> > > 
> > > 1. Any merge commit signed with a key in the intersection of its
> > >    parents' .guix_authorizations. (Standard authorization
> > >    invariant.)
> > > 
> > > 2. Any merge commit that doesn't meet the above conditions, but
> > >    has a parent whose most recent ancestor is the primary 
> > >    introduction, and is signed by a key in the
> > >    .guix_authorizations of that parent. (My
> > >    weakened authorization invariant.)
> > That's a pretty long way of saying "Any merge commit signed with a
> > key in one of its parents' .guix_authorizations."
> 
> Not quite. Merge commits between upstream Guix and your fork, whose
> signing key is in the .guix_authorizations of its parent in upstream
> but not in the .guix_authorizations of its parent in your fork, would
> not be authenticated. (This is the kind of merge commit that would be
> used for [4].)

So merge commits for upstream will look like 

---X---Y---Z-\
              (M)
---A---B---C-/

whereas merge commits for your fork will look like 

---X---Y---Z-\
              (M)
---A---B---C-/

Uhm… how does `guix git authenticate' differentiate between the two?
🤔️

> 
> > 
> > > What does "assert that this set is a subset of what we declare as
> > > introductions" mean?
> > Let's say that you work on branches B, C, and D with "primary"
> > introductions I, J , and K.  If you want to merge C into B, you
> > need to remember that B has I as its primary introduction, C has J,
> > and so on.
> 
> Ah, got it. Yes, that's correct. (Except that there's only one
> primary introduction, at least as I intended it.)
When those branches tail different upstreams, they won't necessarily
have the same primary introduction – at least as you intended it.

> > > 
> > > my design does not require us to distribute any introductions
> > > besides Guix's existing one, nor will it provide any mechanism to
> > > automatically 'install' someone else's introduction.
> > Yes it will, per `%default-guix-channel'.
> 
> Ok, technically true. And someone with the ability to make trusted
> commits upstream could modify that so that a fresh `guix pull` skips
> whatever malicious stuff they've done. But my solution doesn't make
> this situation any worse AFAICT.
As far as you can tell.  I think the scheme would be gnarly enough to
implement that an exploit à la [4] would be possible, even if by
accident rather than design.

This is actually a stronger argument against soft forks of Guix whether
or not your idea is implemented, but anyone using a "trusted" fork
could have their %default-guix-channel changed by someone who is not an
upstream committer.

And no, malicious trusted commits cannot be undone once done; unless
you allow downgrades, which come with their own caveats.  Thus, for a
fresh checkout, a committer who has had access to upstream and your
fork at some point in time – not necessarily the same time – can make
it so that they are the only committer whose patches get accepted on
both.  (A weaker attack is still possible if you only have access to
one repo.)

> > 
> By that logic, we would never have gotten the ability to specify
> multiple channels, since that isn't used in upstream Guix and doesn't
> need to be.
Except that there are legitimate free software channels like Guix
Science, Guix Past or rde's Guix Home channel, which, if memory serves,
use Guix authorizations without issue.  "I want to fork Guix and I need
to worsen its security to do so" is not a good selling point, just
sayin.

Cheers