{"thread":{"id":"63099","subject":"Making bit-by-bit reproducible Git Bundles?","startedAt":"2025-03-12T11:58:59Z","lastAt":"2025-03-14T22:26:34Z","messageCount":11,"participants":["Simon Josefsson","Junio C Hamano","Kyle Lippincott","Jeff King","rsbecker@nexbridge.com"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"514040","messageId":"871pv2jx4a.fsf@josefsson.org","threadId":"63099","inReplyTo":null,"subject":"Making bit-by-bit reproducible Git Bundles?","fromName":"Simon Josefsson","fromEmail":"simon@josefsson.org","sentAt":"2025-03-12T11:40:05Z","receivedAt":"2025-03-12T11:58:59Z","isPatch":false,"sender":{"key":"simon@josefsson.org","avatar":"https://gravatar.com/avatar/bbcf4eaca84bcb1c3d191a2d8868291127d072e3ed2cc92e8b835d8a009020d8?d=mp&s=160"},"body":"Hi.\n\nThank you for the \"git-archive\" and \"git-bundle\" features, making it\neasier to do source-based builds in a no-Internet environment.\n\nI have published a Git bundle of Gnulib:\n\nhttps://www.gnu.org/software/gnulib/manual/html_node/Gnulib-Git-Bundle.html\n\nAs you can see at the end, I struggle to come up with a recipe to allow\nothers to reproduce the git bundle that I created.\n\nIf I run the recipe above twice (including the clone), I get different\nchecksums.  This even if nothing was committed in the remote repository\nmeanwhile.\n\nIs it possible to create a bit-by-bit reproducible git bundle using some\nother set of commands?  If so, how?  I'm using git 2.48.1 from Guix.\n\nCan anyone explain what is causing the irreproducibility?  Running\ndiffoscope is not helpful, since the bundle is compressed and diffoscope\ndoesn't seem to know how to untangle it.\n\nIf this is not possible today, what do you think about changes to make\nthis work?\n\nThanks,\n/Simon\n"},{"id":"514088","messageId":"xmqqbju61bky.fsf@gitster.g","threadId":"63099","inReplyTo":"871pv2jx4a.fsf@josefsson.org","subject":"Re: Making bit-by-bit reproducible Git Bundles?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-12T16:02:37Z","receivedAt":"2025-03-12T16:02:40Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Simon Josefsson <simon@josefsson.org> writes:\n\n> Can anyone explain what is causing the irreproducibility?\n\nMultithreading?\n\n"},{"id":"514153","messageId":"CAO_smViryqTa1LfQSsPbBYcSvijs-UkYkHaot3CK1j=uiuEppQ@mail.gmail.com","threadId":"63099","inReplyTo":"871pv2jx4a.fsf@josefsson.org","subject":"Re: Making bit-by-bit reproducible Git Bundles?","fromName":"Kyle Lippincott","fromEmail":"spectral@google.com","sentAt":"2025-03-13T03:09:03Z","receivedAt":"2025-03-13T03:09:15Z","isPatch":false,"sender":{"key":"spectral@google.com","avatar":"https://avatars.githubusercontent.com/u/6371650?v=4"},"body":"On Wed, Mar 12, 2025 at 4:59 AM Simon Josefsson <simon@josefsson.org> wrote:\n>\n> Hi.\n>\n> Thank you for the \"git-archive\" and \"git-bundle\" features, making it\n> easier to do source-based builds in a no-Internet environment.\n>\n> I have published a Git bundle of Gnulib:\n>\n> https://www.gnu.org/software/gnulib/manual/html_node/Gnulib-Git-Bundle.html\n>\n> As you can see at the end, I struggle to come up with a recipe to allow\n> others to reproduce the git bundle that I created.\n>\n> If I run the recipe above twice (including the clone), I get different\n> checksums.  This even if nothing was committed in the remote repository\n> meanwhile.\n>\n> Is it possible to create a bit-by-bit reproducible git bundle using some\n> other set of commands?  If so, how?  I'm using git 2.48.1 from Guix.\n>\n> Can anyone explain what is causing the irreproducibility?  Running\n> diffoscope is not helpful, since the bundle is compressed and diffoscope\n> doesn't seem to know how to untangle it.\n\nSpent some time on this, and when I followed the instructions, the\ndiffs were in the pack file portion of the bundle file, different\n\"tree\" objects were produced at different points in the pack file. But\nit produces identical bundles if I run `git bundle create` multiple\ntimes in the same clone. My guess is that the non-determinism is\ncoming from the clone process being multi-threaded, meaning that the\norder things are created in the filesystem during the clone,\npresumably due to multithreading happening during the clone process,\nor maybe during gc? The contents of .git/objects/pack have different\nhashes across my two clones, and I haven't investigated why.\n\n>\n> If this is not possible today, what do you think about changes to make\n> this work?\n\nWhat is your end goal with being able to reproduce the bundles?\nBundles are just a list of refs and a pack file, I think. Reproducing\nthe bundle doesn't provide any more security than git provides when it\nwrites the pack file to disk - if you end up with commits with the\nsame hashes, the bundle has to be *effectively* the same as a git\nclone of the repository.\n\nProducing an identical bit-for-bit bundle might be doable by doing\nsome form of sorting of the objects in the pack file, but this would\nonly get us closer to bit-for-bit reproducibility *on the same machine\nand versions of everything*. There could be some changes to git, zlib,\nmachine architecture, etc. that causes deterministic but different\nvalues to be produced. As an example, maybe future versions of zlib\ncompress better, producing an equal result when decompressed, but a\ndifferent compressed result.\n\n>\n> Thanks,\n> /Simon\n"},{"id":"514154","messageId":"20250313051538.GA94015@coredump.intra.peff.net","threadId":"63099","inReplyTo":"871pv2jx4a.fsf@josefsson.org","subject":"Re: Making bit-by-bit reproducible Git Bundles?","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-13T05:15:38Z","receivedAt":"2025-03-13T05:15:47Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Mar 12, 2025 at 12:40:05PM +0100, Simon Josefsson wrote:\n\n> If I run the recipe above twice (including the clone), I get different\n> checksums.  This even if nothing was committed in the remote repository\n> meanwhile.\n> \n> Is it possible to create a bit-by-bit reproducible git bundle using some\n> other set of commands?  If so, how?  I'm using git 2.48.1 from Guix.\n\nAs Junio noted, multithreading is the first problem. E.g., here are some\ncommands on git.git, using my 8-core machine:\n\n  [try once...]\n  $ git bundle create --no-progress - HEAD | sha1sum\n  686da850200da487032c9d91bdc544b605a3e426  -\n\n  [and again; oops, it's different]\n  $ git bundle create --no-progress - HEAD | sha1sum\n  70b018c16d244f32b36e55deb931e29ae15506e3  -\n\n  [now without threading]\n  $ git -c pack.threads=1 bundle create --no-progress - HEAD | sha1sum\n  c897caf9c68d2c37d997d3973196886af3b0b46e  -\n\n  [and we can do it again. yay!]\n  $ git -c pack.threads=1 bundle create --no-progress - HEAD | sha1sum\n  c897caf9c68d2c37d997d3973196886af3b0b46e  -\n\nWhat's happening here is that the bundle mostly consists of a packfile,\nwhere many objects will be stored as deltas against others. The search\nfor deltas is multi-threaded, so it will find slightly different ones\neach time (there surely is an \"optimal\" answer, but finding it is much\ntoo expensive, so we bound the search with some heuristics).\n\nSo disabling threading gives you a deterministic answer. But that's not\nthe end of the story! We only search for deltas of objects that are not\nalready stored as deltas in on-disk packfiles. We try to reuse any\ndeltas we have already on disk (assuming that both the delta and its\nbase are going to be in the output).\n\nThere are options to ask pack-objects (the command which git-bundle uses\nunder the hood to generate the pack) not to reuse deltas. So\npack-objects running on a single thread without any delta reuse should\ngenerate a deterministic pack. But there are some gotchas:\n\n  1. It's stable only for a given Git version, and with a particular set\n     of delta window/depth options. I wouldn't expect behavior to change\n     much between versions, but it's not something that we try to\n     guarantee.\n\n  2. There is no way to pass pack-objects options down through\n     git-bundle. So you'd have to either assemble the bundle yourself,\n     or perhaps generate a stable on-disk pack state, and then generate\n     the bundle. Perhaps something like:\n\n       # make one single pack, with no reuse, using the default options\n       git -c pack.threads=1 repack -adf\n\n       # now we can make a bundle from that. We probably do not even\n       # need to disable threads here, since we'd just be picking the\n       # deltas from the on-disk file (assuming that you're including\n       # all objects in the bundle)\n       git bundle create - | sha1sum\n\n  3. It will be really slow. We're throwing out all of the deltas and\n     searching from scratch. And doing it single-threaded. I didn't time\n     it, but I'd guess from past experience we're talking about hours to\n     generate the bundle for something like linux.git.\n\nSo I think it's possible, but I doubt it's very ergonomic. You're\nprobably better off using some checksum over Git's logical model, rather\nthan the stored bytes. The obvious one is that a single Git commit hash\nunambiguously represents the whole tree and all of history leading up to\nit, because of the chains of hashes.\n\nBut that implies you trust Git's object hash algorithm. If you don't\ntrust sha1 (and don't want to try out the sha256 support), then you'd\nhave to design something else.  Perhaps something like:\n\n  # print all commits in topological order, with ties broken by\n  # committer date, which should be stable. And then follow up with the\n  # trees and blobs for each.\n  git rev-list --topo-order --objects HEAD >objects\n\n  # now print the contents of each object (preceded by its name, type,\n  # and length, so there's no chance of weird prepending or appending\n  # attacks). We cut off the path information from rev-list here, since\n  # the ordered set of objects is all we care about.\n  cut -d' ' -f1 objects |\n  git cat-file --batch >content\n\n  # and then take a hash over that content; this will be unambiguous.\n  sha256sum <content\n\n-Peff\n"},{"id":"514167","messageId":"87tt7xicnr.fsf@josefsson.org","threadId":"63099","inReplyTo":"CAO_smViryqTa1LfQSsPbBYcSvijs-UkYkHaot3CK1j=uiuEppQ@mail.gmail.com","subject":"Re: Making bit-by-bit reproducible Git Bundles?","fromName":"Simon Josefsson","fromEmail":"simon@josefsson.org","sentAt":"2025-03-13T07:59:36Z","receivedAt":"2025-03-13T08:00:01Z","isPatch":false,"sender":{"key":"simon@josefsson.org","avatar":"https://gravatar.com/avatar/bbcf4eaca84bcb1c3d191a2d8868291127d072e3ed2cc92e8b835d8a009020d8?d=mp&s=160"},"body":"Kyle Lippincott <spectral@google.com> writes:\n\n>> Can anyone explain what is causing the irreproducibility?  Running\n>> diffoscope is not helpful, since the bundle is compressed and diffoscope\n>> doesn't seem to know how to untangle it.\n>\n> Spent some time on this, and when I followed the instructions, the\n> diffs were in the pack file portion of the bundle file, different\n> \"tree\" objects were produced at different points in the pack file. But\n> it produces identical bundles if I run `git bundle create` multiple\n> times in the same clone. My guess is that the non-determinism is\n> coming from the clone process being multi-threaded, meaning that the\n> order things are created in the filesystem during the clone,\n> presumably due to multithreading happening during the clone process,\n> or maybe during gc? The contents of .git/objects/pack have different\n> hashes across my two clones, and I haven't investigated why.\n\nYes, my perception is also that the reproducibility problems happens\nduring 'git clone'.  Within the same git clone, it is no problem to\ncreate a bit-by-bit reproducible git bundle.  But if you work in two\ndifferent clones, I haven't been able to find any set of commands that\nleads to identical results.\n\nFWIW, some other ways to do the clone that I have tried but didn't get\nto work (of course I may have made some mistake in my attempts):\n\n# dumb protocol doesn't repack the objects\nGIT_SMART_HTTP=0 git clone https://git.savannah.gnu.org/git/gnulib.git\n\n# using rsync fetches .git identical as upstream\nrsync -av git.savannah.gnu.org::git/gnulib.git/ gnulib\n\n>> If this is not possible today, what do you think about changes to make\n>> this work?\n>\n> What is your end goal with being able to reproduce the bundles?\n\nGood question - I should have made that clear.\n\nThe end goal is for someone other than me as uploader of the gnulib git\nbundle to be able re-create it bit-by-bit identical.  This pursuit is in\nthe name of improved software security supply-chain security.  Compare\nefforts to make gzip and tarball files reproducible by others:\n\nhttps://www.gnu.org/software/tar/manual/html_node/Reproducibility.html\nhttps://www.gnu.org/software/gzip/manual/html_node/Environment.html\n\n> Producing an identical bit-for-bit bundle might be doable by doing\n> some form of sorting of the objects in the pack file, but this would\n> only get us closer to bit-for-bit reproducibility *on the same machine\n> and versions of everything*. There could be some changes to git, zlib,\n> machine architecture, etc. that causes deterministic but different\n> values to be produced. As an example, maybe future versions of zlib\n> compress better, producing an equal result when decompressed, but a\n> different compressed result.\n\nThat is an improvement compared to todays situation where nobody can\nreproduce the git bundle at all.  Being able to reproduce it using the\nsame environment (toolchain) is better.  This is similar for\nreproducible builds of binaries: typically you need to reproduce a\nsimilar environment to get reproducible results.\n\n/Simon\n"},{"id":"514182","messageId":"xmqqzfhpqcgo.fsf@gitster.g","threadId":"63099","inReplyTo":"20250313051538.GA94015@coredump.intra.peff.net","subject":"Re: Making bit-by-bit reproducible Git Bundles?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-13T13:36:39Z","receivedAt":"2025-03-13T13:36:42Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> .... But there are some gotchas:\n>\n>   1. It's stable only for a given Git version, and with a particular set\n> ...\n>   2. There is no way to pass pack-objects options down through\n> ...\n>   3. It will be really slow. We're throwing out all of the deltas and\n> ...\nThere also is 4.\n\n    4. We do not control zlib, so even with the same Git binary, the\n       zlib implementation that is dynamically linked to us is free\n       to produce better compressed base object (or compressed\n       delta).\n\n3. is not a downside if the priority of the requestor is about\nbit-for-bit reproducibility (iow, \"no matter what the cost\").\n\n>   # print all commits in topological order, with ties broken by\n>   # committer date, which should be stable. And then follow up with the\n>   # trees and blobs for each.\n>   git rev-list --topo-order --objects HEAD >objects\n>\n>   # now print the contents of each object (preceded by its name, type,\n>   # and length, so there's no chance of weird prepending or appending\n>   # attacks). We cut off the path information from rev-list here, since\n>   # the ordered set of objects is all we care about.\n>   cut -d' ' -f1 objects |\n>   git cat-file --batch >content\n>\n>   # and then take a hash over that content; this will be unambiguous.\n>   sha256sum <content\n\nGross but probably stable ;-)\n"},{"id":"514228","messageId":"87msdo1yal.fsf@josefsson.org","threadId":"63099","inReplyTo":"20250313051538.GA94015@coredump.intra.peff.net","subject":"Re: Making bit-by-bit reproducible Git Bundles?","fromName":"Simon Josefsson","fromEmail":"simon@josefsson.org","sentAt":"2025-03-13T20:16:34Z","receivedAt":"2025-03-13T20:17:27Z","isPatch":false,"sender":{"key":"simon@josefsson.org","avatar":"https://gravatar.com/avatar/bbcf4eaca84bcb1c3d191a2d8868291127d072e3ed2cc92e8b835d8a009020d8?d=mp&s=160"},"body":"Jeff King <peff@peff.net> writes:\n\n>   [now without threading]\n>   $ git -c pack.threads=1 bundle create --no-progress - HEAD | sha1sum\n>   c897caf9c68d2c37d997d3973196886af3b0b46e  -\n>\n>   [and we can do it again. yay!]\n>   $ git -c pack.threads=1 bundle create --no-progress - HEAD | sha1sum\n>   c897caf9c68d2c37d997d3973196886af3b0b46e  -\n\nThat's the commands I use -- it doesn't lead to the same hash in two\ndifferent 'git clone's.  I tried running 'git clone' with the same '-c\npack.threads=1' but it made no difference.\n\n>   2. There is no way to pass pack-objects options down through\n>      git-bundle. So you'd have to either assemble the bundle yourself,\n>      or perhaps generate a stable on-disk pack state, and then generate\n>      the bundle. Perhaps something like:\n>\n>        # make one single pack, with no reuse, using the default options\n>        git -c pack.threads=1 repack -adf\n\nYay!  You may have solved this for me.  I have to verify this a bit\nmore, but this looks promising (these are two different git clones):\n\njas@kaka:~/t/gnulib-1$ git -c pack.threads=1 repack -adf\njas@kaka:~/t/gnulib-1$ git -c 'pack.threads=1' bundle create gnulib.bundle --all\njas@kaka:~/t/gnulib-1$ sha256sum gnulib.bundle \nc780bb07501cf016e702fbe3f52704b4f64edd6882c13c9be0f3f114c894e890  gnulib.bundle\njas@kaka:~/t/gnulib-1$ cd ../gnulib-2\njas@kaka:~/t/gnulib-2$ git -c pack.threads=1 repack -adf\njas@kaka:~/t/gnulib-2$ git -c 'pack.threads=1' bundle create gnulib.bundle --all\njas@kaka:~/t/gnulib-2$ sha256sum gnulib.bundle \nc780bb07501cf016e702fbe3f52704b4f64edd6882c13c9be0f3f114c894e890  gnulib.bundle\njas@kaka:~/t/gnulib-2$ \n\n> So I think it's possible, but I doubt it's very ergonomic. You're\n> probably better off using some checksum over Git's logical model, rather\n> than the stored bytes. The obvious one is that a single Git commit hash\n> unambiguously represents the whole tree and all of history leading up to\n> it, because of the chains of hashes.\n>\n> But that implies you trust Git's object hash algorithm.\n\nRight -- I think anything but bit-by-bit identical files is going to be\ntoo complex to verify.\n\n>   # print all commits in topological order, with ties broken by\n>   # committer date, which should be stable. And then follow up with the\n>   # trees and blobs for each.\n>   git rev-list --topo-order --objects HEAD >objects\n>\n>   # now print the contents of each object (preceded by its name, type,\n>   # and length, so there's no chance of weird prepending or appending\n>   # attacks). We cut off the path information from rev-list here, since\n>   # the ordered set of objects is all we care about.\n>   cut -d' ' -f1 objects |\n>   git cat-file --batch >content\n>\n>   # and then take a hash over that content; this will be unambiguous.\n>   sha256sum <content\n\nHow to read this output?  Could this be made git bundle compatible?\n\nBut if the above is solves it, this part isn't necessary.\n\n/Simon\n"},{"id":"514231","messageId":"CAO_smVgSRaYqNrA4c1yeu-cpj+P36MY+bsQT=8K0SXpWHkaWCQ@mail.gmail.com","threadId":"63099","inReplyTo":"87msdo1yal.fsf@josefsson.org","subject":"Re: Making bit-by-bit reproducible Git Bundles?","fromName":"Kyle Lippincott","fromEmail":"spectral@google.com","sentAt":"2025-03-13T21:07:16Z","receivedAt":"2025-03-13T21:07:28Z","isPatch":false,"sender":{"key":"spectral@google.com","avatar":"https://avatars.githubusercontent.com/u/6371650?v=4"},"body":"On Thu, Mar 13, 2025 at 1:18 PM Simon Josefsson <simon@josefsson.org> wrote:\n>\n> Jeff King <peff@peff.net> writes:\n>\n> >   [now without threading]\n> >   $ git -c pack.threads=1 bundle create --no-progress - HEAD | sha1sum\n> >   c897caf9c68d2c37d997d3973196886af3b0b46e  -\n> >\n> >   [and we can do it again. yay!]\n> >   $ git -c pack.threads=1 bundle create --no-progress - HEAD | sha1sum\n> >   c897caf9c68d2c37d997d3973196886af3b0b46e  -\n>\n> That's the commands I use -- it doesn't lead to the same hash in two\n> different 'git clone's.  I tried running 'git clone' with the same '-c\n> pack.threads=1' but it made no difference.\n>\n> >   2. There is no way to pass pack-objects options down through\n> >      git-bundle. So you'd have to either assemble the bundle yourself,\n> >      or perhaps generate a stable on-disk pack state, and then generate\n> >      the bundle. Perhaps something like:\n> >\n> >        # make one single pack, with no reuse, using the default options\n> >        git -c pack.threads=1 repack -adf\n>\n> Yay!  You may have solved this for me.  I have to verify this a bit\n> more, but this looks promising (these are two different git clones):\n>\n> jas@kaka:~/t/gnulib-1$ git -c pack.threads=1 repack -adf\n> jas@kaka:~/t/gnulib-1$ git -c 'pack.threads=1' bundle create gnulib.bundle --all\n> jas@kaka:~/t/gnulib-1$ sha256sum gnulib.bundle\n> c780bb07501cf016e702fbe3f52704b4f64edd6882c13c9be0f3f114c894e890  gnulib.bundle\n> jas@kaka:~/t/gnulib-1$ cd ../gnulib-2\n> jas@kaka:~/t/gnulib-2$ git -c pack.threads=1 repack -adf\n> jas@kaka:~/t/gnulib-2$ git -c 'pack.threads=1' bundle create gnulib.bundle --all\n> jas@kaka:~/t/gnulib-2$ sha256sum gnulib.bundle\n> c780bb07501cf016e702fbe3f52704b4f64edd6882c13c9be0f3f114c894e890  gnulib.bundle\n> jas@kaka:~/t/gnulib-2$\n>\n> > So I think it's possible, but I doubt it's very ergonomic. You're\n> > probably better off using some checksum over Git's logical model, rather\n> > than the stored bytes. The obvious one is that a single Git commit hash\n> > unambiguously represents the whole tree and all of history leading up to\n> > it, because of the chains of hashes.\n> >\n> > But that implies you trust Git's object hash algorithm.\n>\n> Right -- I think anything but bit-by-bit identical files is going to be\n> too complex to verify.\n\nI'm curious what specific attacks you're trying to catch here. Because\nto get into a situation where you unbundle the bundle and have the\nsame commit hash but different contents, you would need to have a\ncollision in the SHA-1 hash for some object (or SHA-256 hash if the\nrepo is using that). If you're also providing the instructions (or\neven just the commit hash and server to clone from, and linking to\ninstructions maintained elsewhere) to validate the bundle is\nlegitimate, it seems MUCH easier to just replace those validation\ninstructions to point to a commit/server that has already been\nbackdoored than it would be to generate a SHA-1 collision that would\ngo undetected.\n\n>\n> >   # print all commits in topological order, with ties broken by\n> >   # committer date, which should be stable. And then follow up with the\n> >   # trees and blobs for each.\n> >   git rev-list --topo-order --objects HEAD >objects\n> >\n> >   # now print the contents of each object (preceded by its name, type,\n> >   # and length, so there's no chance of weird prepending or appending\n> >   # attacks). We cut off the path information from rev-list here, since\n> >   # the ordered set of objects is all we care about.\n> >   cut -d' ' -f1 objects |\n> >   git cat-file --batch >content\n> >\n> >   # and then take a hash over that content; this will be unambiguous.\n> >   sha256sum <content\n>\n> How to read this output?  Could this be made git bundle compatible?\n>\n> But if the above is solves it, this part isn't necessary.\n>\n> /Simon\n"},{"id":"514233","messageId":"xmqq5xkcmvle.fsf@gitster.g","threadId":"63099","inReplyTo":"CAO_smVgSRaYqNrA4c1yeu-cpj+P36MY+bsQT=8K0SXpWHkaWCQ@mail.gmail.com","subject":"Re: Making bit-by-bit reproducible Git Bundles?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-03-13T22:09:17Z","receivedAt":"2025-03-13T22:09:21Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Kyle Lippincott <spectral@google.com> writes:\n\n>> > But that implies you trust Git's object hash algorithm.\n>>\n>> Right -- I think anything but bit-by-bit identical files is going to be\n>> too complex to verify.\n>\n> I'm curious what specific attacks you're trying to catch here. Because\n> to get into a situation where you unbundle the bundle and have the\n> same commit hash but different contents, you would need to have a\n> collision in the SHA-1 hash for some object (or SHA-256 hash if the\n> repo is using that). If you're also providing the instructions (or\n> even just the commit hash and server to clone from, and linking to\n> instructions maintained elsewhere) to validate the bundle is\n> legitimate, it seems MUCH easier to just replace those validation\n> instructions to point to a commit/server that has already been\n> backdoored than it would be to generate a SHA-1 collision that would\n> go undetected.\n\nI think there are two levels of \"verify\" involved in this discussion.\n\nThere are those who want to trust bundles and and place enough trust\non whoever created that bundle.  They are happy as long as the\nbitstream they received the first time does not change when they ask\nfor it for the second time, because they at least know that the same\ninput would result in the same output.  To them, \"attack\" is what\nchanges the bitstream while they are looking the other way.  They do\nnot like the fact that there can be more than one representations of\nthe same thing for this reason.\n\nThen there are those who know Git enough to know that they do not\nneed to trust the middleman who create bundle files, and they do not\nneed to trust exact bitstream that is contained within these bundle\nfiles.  They can extract the bundle to verify the tip commits of the\nhistory (by comparing their object names with published hashes, by\nverifying the embedded signatures, etc.), which is what ensures\nintegrity in Merkle tree based systems like history stored in Git.\n\nThe latter folks may worry about the \"attacks\" you mention here, but\nthe former may not necessarily do so.\n"},{"id":"514252","messageId":"20250314024218.GA114103@coredump.intra.peff.net","threadId":"63099","inReplyTo":"87msdo1yal.fsf@josefsson.org","subject":"Re: Making bit-by-bit reproducible Git Bundles?","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-03-14T02:42:18Z","receivedAt":"2025-03-14T02:42:19Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Mar 13, 2025 at 09:16:34PM +0100, Simon Josefsson wrote:\n\n> >   2. There is no way to pass pack-objects options down through\n> >      git-bundle. So you'd have to either assemble the bundle yourself,\n> >      or perhaps generate a stable on-disk pack state, and then generate\n> >      the bundle. Perhaps something like:\n> >\n> >        # make one single pack, with no reuse, using the default options\n> >        git -c pack.threads=1 repack -adf\n> \n> Yay!  You may have solved this for me.  I have to verify this a bit\n> more, but this looks promising (these are two different git clones):\n> \n> jas@kaka:~/t/gnulib-1$ git -c pack.threads=1 repack -adf\n> jas@kaka:~/t/gnulib-1$ git -c 'pack.threads=1' bundle create gnulib.bundle --all\n> jas@kaka:~/t/gnulib-1$ sha256sum gnulib.bundle \n> c780bb07501cf016e702fbe3f52704b4f64edd6882c13c9be0f3f114c894e890  gnulib.bundle\n> jas@kaka:~/t/gnulib-1$ cd ../gnulib-2\n> jas@kaka:~/t/gnulib-2$ git -c pack.threads=1 repack -adf\n> jas@kaka:~/t/gnulib-2$ git -c 'pack.threads=1' bundle create gnulib.bundle --all\n> jas@kaka:~/t/gnulib-2$ sha256sum gnulib.bundle \n> c780bb07501cf016e702fbe3f52704b4f64edd6882c13c9be0f3f114c894e890  gnulib.bundle\n> jas@kaka:~/t/gnulib-2$ \n\nOne thing to watch out for here: that repack is going to look at _all_\nobjects in the repository. So you will get different output if you make\na bundle of a tag \"v1.0\" today than you would get later, when \"v1.1\"\nalso exists. Ditto for any other activity in the repository, like writes\nto unrelated branches, or even reflog entries.\n\nSo you'd probably want to make an absolute minimal repository with the\nreachable objects, perhaps like:\n\n  git clone --bare --no-local --single-branch -b v1.0 . just-v1.0.git\n  cd just-v1.0.git\n  git -c pack.threads=1 repack -adf\n\nIt doesn't have to be just one ref, of course; you might want to\nsnapshot the whole set of refs at the time you make the bundle. E.g., by\nfetching into the empty repo using a refspec.\n\nThis would all be a non-issue if you could ask git-bundle to directly\npass the equivalent of \"-f\" to pack-objects (at that layer it is called\n\"--no-reuse-delta\"). Since then it would be computing the full set of\nobjects itself. But without a patch to Git, I don't think there's a way\nto do that.\n\nThe bundle format is pretty simple, so you _could_ hack around it\nyourself, like:\n\n  # list refs we care about; you can pick whatever subset you want\n  # here.\n  git for-each-ref --format='%(objectname) %(refname)' refs/heads/ >refs\n\n  {\n\t# bundle header plus list of refs, plus blank line terminator\n\techo \"# v2 git bundle\"\n\tcat refs\n\techo\n\n\t# and now the pack. We just need to feed it the object ids for\n\t# all of the refs. It will handle sorting and de-duping for us.\n\tcut -d' ' -f1 <refs |\n\tgit -c pack.threads=1 pack-objects \\\n\t\t--stdout --revs --delta-base-offset --no-reuse-delta\n  } >foo.bundle\n\nI dunno if that is more or less gross than teaching git-bundle to pass\n--no-reuse-delta itself. It's certainly more intimate with the details,\nbut OTOH it is less likely to change in other versions of Git (e.g., if\nwe started making \"v3\" bundles by default).\n\n> >   # print all commits in topological order, with ties broken by\n> >   # committer date, which should be stable. And then follow up with the\n> >   # trees and blobs for each.\n> >   git rev-list --topo-order --objects HEAD >objects\n> >\n> >   # now print the contents of each object (preceded by its name, type,\n> >   # and length, so there's no chance of weird prepending or appending\n> >   # attacks). We cut off the path information from rev-list here, since\n> >   # the ordered set of objects is all we care about.\n> >   cut -d' ' -f1 objects |\n> >   git cat-file --batch >content\n> >\n> >   # and then take a hash over that content; this will be unambiguous.\n> >   sha256sum <content\n> \n> How to read this output?  Could this be made git bundle compatible?\n\nYou'd have to compare the result of doing that after fetching from the\nbundle into an empty repo. I don't think there's a great way to operate\ndirectly on the bundle packfile (it has to be indexed first to see\nwhat's in it).\n\nThe closest I could get is:\n\n  input=foo.bundle\n\n  # split the bundle into header and packfile sections on the first\n  # blank line\n  sed '/^$/q' <$input >header\n  size=$(stat --format=%s header)\n  tail -c +$((size+1)) <$input >bundle.pack\n\n  # we can first do a byte-level comparison of the header; if this isn't\n  # the same, the bundles do not match.\n  sha256sum <header\n\n  # now index the pack, so we know what's in it; this makes bundle.idx\n  git index-pack -v bundle.pack\n\n  # and now we want to dump the full logical contents (not the\n  # delta-compressed versions) of each object. First we need a list of\n  # the objects. This will come out in lexical order of object id, which\n  # is good for us since it will be stable.\n  git show-index <bundle.idx  | awk '{print $2}' >objects\n\n  # unfortunately here things break down. There is no command to read\n  # the data directly out of the pack/idx pair without a repository\n  # (even though it could be done technically). So we hack around it\n  # with a temp repo.\n  git init --bare tmp.git\n  mv bundle.idx bundle.pack tmp.git/objects/pack/\n  git -C tmp.git cat-file --batch <objects | sha256sum\n\nSo...also kind of gross. And not really all that different than what:\n\n  git init --bare tmp.git\n  cd tmp.git\n  git fetch ../foo.bundle refs/*:refs/*\n\nwould do (you end up with the same pack/idx pair). So I dunno. I guess\nit depends how many and which Git commands you're willing to trust. ;)\n\n-Peff\n"},{"id":"514325","messageId":"011101db952f$ebcffe80$c36ffb80$@nexbridge.com","threadId":"63099","inReplyTo":"20250314024218.GA114103@coredump.intra.peff.net","subject":"RE: Making bit-by-bit reproducible Git Bundles?","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2025-03-14T22:24:53Z","receivedAt":"2025-03-14T22:26:34Z","isPatch":false,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On March 13, 2025 10:42 PM, Jeff King wrote:\n>On Thu, Mar 13, 2025 at 09:16:34PM +0100, Simon Josefsson wrote:\n>\n>> >   2. There is no way to pass pack-objects options down through\n>> >      git-bundle. So you'd have to either assemble the bundle yourself,\n>> >      or perhaps generate a stable on-disk pack state, and then generate\n>> >      the bundle. Perhaps something like:\n>> >\n>> >        # make one single pack, with no reuse, using the default options\n>> >        git -c pack.threads=1 repack -adf\n>>\n>> Yay!  You may have solved this for me.  I have to verify this a bit\n>> more, but this looks promising (these are two different git clones):\n>>\n>> jas@kaka:~/t/gnulib-1$ git -c pack.threads=1 repack -adf\n>> jas@kaka:~/t/gnulib-1$ git -c 'pack.threads=1' bundle create\n>> gnulib.bundle --all jas@kaka:~/t/gnulib-1$ sha256sum gnulib.bundle\n>> c780bb07501cf016e702fbe3f52704b4f64edd6882c13c9be0f3f114c894e890\n>> gnulib.bundle jas@kaka:~/t/gnulib-1$ cd ../gnulib-2\n>> jas@kaka:~/t/gnulib-2$ git -c pack.threads=1 repack -adf\n>> jas@kaka:~/t/gnulib-2$ git -c 'pack.threads=1' bundle create\n>> gnulib.bundle --all jas@kaka:~/t/gnulib-2$ sha256sum gnulib.bundle\n>> c780bb07501cf016e702fbe3f52704b4f64edd6882c13c9be0f3f114c894e890\n>> gnulib.bundle jas@kaka:~/t/gnulib-2$\n>\n>One thing to watch out for here: that repack is going to look at _all_ objects in the\n>repository. So you will get different output if you make a bundle of a tag \"v1.0\"\n>today than you would get later, when \"v1.1\"\n>also exists. Ditto for any other activity in the repository, like writes to unrelated\n>branches, or even reflog entries.\n>\n>So you'd probably want to make an absolute minimal repository with the reachable\n>objects, perhaps like:\n>\n>  git clone --bare --no-local --single-branch -b v1.0 . just-v1.0.git\n>  cd just-v1.0.git\n>  git -c pack.threads=1 repack -adf\n>\n>It doesn't have to be just one ref, of course; you might want to snapshot the whole\n>set of refs at the time you make the bundle. E.g., by fetching into the empty repo\n>using a refspec.\n>\n>This would all be a non-issue if you could ask git-bundle to directly pass the\n>equivalent of \"-f\" to pack-objects (at that layer it is called \"--no-reuse-delta\"). Since\n>then it would be computing the full set of objects itself. But without a patch to Git, I\n>don't think there's a way to do that.\n>\n>The bundle format is pretty simple, so you _could_ hack around it yourself, like:\n>\n>  # list refs we care about; you can pick whatever subset you want\n>  # here.\n>  git for-each-ref --format='%(objectname) %(refname)' refs/heads/ >refs\n>\n>  {\n>\t# bundle header plus list of refs, plus blank line terminator\n>\techo \"# v2 git bundle\"\n>\tcat refs\n>\techo\n>\n>\t# and now the pack. We just need to feed it the object ids for\n>\t# all of the refs. It will handle sorting and de-duping for us.\n>\tcut -d' ' -f1 <refs |\n>\tgit -c pack.threads=1 pack-objects \\\n>\t\t--stdout --revs --delta-base-offset --no-reuse-delta\n>  } >foo.bundle\n>\n>I dunno if that is more or less gross than teaching git-bundle to pass --no-reuse-\n>delta itself. It's certainly more intimate with the details, but OTOH it is less likely to\n>change in other versions of Git (e.g., if we started making \"v3\" bundles by default).\n>\n>> >   # print all commits in topological order, with ties broken by\n>> >   # committer date, which should be stable. And then follow up with the\n>> >   # trees and blobs for each.\n>> >   git rev-list --topo-order --objects HEAD >objects\n>> >\n>> >   # now print the contents of each object (preceded by its name, type,\n>> >   # and length, so there's no chance of weird prepending or appending\n>> >   # attacks). We cut off the path information from rev-list here, since\n>> >   # the ordered set of objects is all we care about.\n>> >   cut -d' ' -f1 objects |\n>> >   git cat-file --batch >content\n>> >\n>> >   # and then take a hash over that content; this will be unambiguous.\n>> >   sha256sum <content\n>>\n>> How to read this output?  Could this be made git bundle compatible?\n>\n>You'd have to compare the result of doing that after fetching from the bundle into\n>an empty repo. I don't think there's a great way to operate directly on the bundle\n>packfile (it has to be indexed first to see what's in it).\n>\n>The closest I could get is:\n>\n>  input=foo.bundle\n>\n>  # split the bundle into header and packfile sections on the first\n>  # blank line\n>  sed '/^$/q' <$input >header\n>  size=$(stat --format=%s header)\n>  tail -c +$((size+1)) <$input >bundle.pack\n>\n>  # we can first do a byte-level comparison of the header; if this isn't\n>  # the same, the bundles do not match.\n>  sha256sum <header\n>\n>  # now index the pack, so we know what's in it; this makes bundle.idx\n>  git index-pack -v bundle.pack\n>\n>  # and now we want to dump the full logical contents (not the\n>  # delta-compressed versions) of each object. First we need a list of\n>  # the objects. This will come out in lexical order of object id, which\n>  # is good for us since it will be stable.\n>  git show-index <bundle.idx  | awk '{print $2}' >objects\n>\n>  # unfortunately here things break down. There is no command to read\n>  # the data directly out of the pack/idx pair without a repository\n>  # (even though it could be done technically). So we hack around it\n>  # with a temp repo.\n>  git init --bare tmp.git\n>  mv bundle.idx bundle.pack tmp.git/objects/pack/\n>  git -C tmp.git cat-file --batch <objects | sha256sum\n>\n>So...also kind of gross. And not really all that different than what:\n>\n>  git init --bare tmp.git\n>  cd tmp.git\n>  git fetch ../foo.bundle refs/*:refs/*\n>\n>would do (you end up with the same pack/idx pair). So I dunno. I guess it depends\n>how many and which Git commands you're willing to trust. ;)\n\nI would go one step further on this. Using --depth=1 and potentially a --sparse checkout\nwith only what you specifically need to verify.\n\nHowever, Junio's point on checking end-point commit and tags is useful and significant\non verifying that the Merkel Tree itself is intact and not modified using signing is usually\nsufficient verification and more reliable than a bit-for bit comparison, which may have\ndependencies on the underlying  operating system, particularly if the originating\ndirectory inode contents differ from the destination - an example is using a Windows\nserver for the upstream and a NonStop server for the clone (not so much with Linux vs.\nNonStop). It is pretty much guaranteed that the inodes will be different.\n\n--Randall\n\n"}]}