mirror of
https://github.com/vee1e/gittuf.git
synced 2026-09-01 18:27:06 +00:00
Make gittuf hash-algorithm aware so it operates on SHA-256 repositories: - gitinterface.Repository detects its object format (via `git rev-parse --show-object-format`) and exposes GetObjectFormat and a per-repo ZeroHash. GetReference returns the format-appropriate zero hash so ref updates to new refs succeed in SHA-256 repositories. - Commit and tag signing/verification use the `gpgsig-sha256` header for SHA-256 objects (go-git's SignatureSHA256), matching Git's behavior. The verification field is chosen by the object's OID length. Fixes verify-ref failing on Git-signed SHA-256 RSL entries. - git-remote-gittuf negotiates object-format from the repository instead of hardcoding sha1, and uses the repo-appropriate zero hash for ref tips. - Test helpers can create SHA-256 repositories; gitinterface, rsl, policy, and experimental/gittuf suites now run under both formats, plus an end-to-end Git-signed record+verify test. Assisted-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
897 lines
26 KiB
Go
897 lines
26 KiB
Go
// Copyright The gittuf Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package gitinterface
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
"github.com/gittuf/gittuf/internal/signerverifier/gpg"
|
|
"github.com/gittuf/gittuf/internal/signerverifier/ssh"
|
|
artifacts "github.com/gittuf/gittuf/internal/testartifacts"
|
|
"github.com/go-git/go-git/v6/plumbing"
|
|
"github.com/go-git/go-git/v6/plumbing/object"
|
|
"github.com/secure-systems-lab/go-securesystemslib/signerverifier"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestRepositoryCommitSHA256(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false, WithSHA256Format())
|
|
|
|
refName := "refs/heads/main"
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Commit to a ref that does not yet exist: the "current" tip is the zero
|
|
// hash, which must match the repository's SHA-256 object format.
|
|
commitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
assert.Nil(t, err)
|
|
assert.Len(t, commitID.String(), 64)
|
|
|
|
refHead, err := repo.GetReference(refName)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, commitID, refHead)
|
|
}
|
|
|
|
func TestRepositoryCommit(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
refName := "refs/heads/main"
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write second tree
|
|
blobID, err := repo.WriteBlob([]byte("Hello, world!\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
treeWithContentsID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("README.md", blobID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create initial commit with no tree
|
|
expectedInitialCommitID := "648c569f3958b899e832f04750de52cf5d0db2fa"
|
|
commitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedInitialCommitID, commitID.String())
|
|
|
|
refHead, err := repo.GetReference(refName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, expectedInitialCommitID, refHead.String())
|
|
|
|
// Create second commit with tree
|
|
expectedSecondCommitID := "3d7200c158ccfedf35a68a7d24842d60cac4ec0d"
|
|
commitID, err = repo.Commit(treeWithContentsID, refName, "Add README\n", false)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedSecondCommitID, commitID.String())
|
|
|
|
refHead, err = repo.GetReference(refName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, expectedSecondCommitID, refHead.String())
|
|
|
|
// Create third commit with same tree but sign this time
|
|
expectedThirdCommitID := "eed43c23f781ddc10359ce25e0fc486a000a8c9f"
|
|
commitID, err = repo.Commit(treeWithContentsID, refName, "Signing this commit\n", true)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedThirdCommitID, commitID.String())
|
|
|
|
refHead, err = repo.GetReference(refName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, expectedThirdCommitID, refHead.String())
|
|
}
|
|
|
|
func TestRepositoryCommitUsingSpecificKey(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
refName := "refs/heads/main"
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write second tree
|
|
blobID, err := repo.WriteBlob([]byte("Hello, world!\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
treeWithContentsID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("README.md", blobID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create initial commit with no tree
|
|
expectedInitialCommitID := "b218890d607cdcea53ebf6c640748b4b1c8015ca"
|
|
commitID, err := repo.CommitUsingSpecificKey(emptyTreeID, refName, "Initial commit\n", artifacts.SSHED25519Private)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedInitialCommitID, commitID.String())
|
|
|
|
refHead, err := repo.GetReference(refName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, expectedInitialCommitID, refHead.String())
|
|
|
|
// Create second commit with tree
|
|
expectedSecondCommitID := "2b3f8b1f6af0d0d3c37130ba4d054ff4c2e95a3a"
|
|
commitID, err = repo.CommitUsingSpecificKey(treeWithContentsID, refName, "Add README\n", artifacts.SSHED25519Private)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedSecondCommitID, commitID.String())
|
|
|
|
refHead, err = repo.GetReference(refName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, expectedSecondCommitID, refHead.String())
|
|
})
|
|
|
|
t.Run("invalid key", func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
refName := "refs/heads/main"
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
unknownKey := []byte("-----BEGIN UNKNOWN KEY-----\nYWJj\n-----END UNKNOWN KEY-----\n")
|
|
_, err = repo.CommitUsingSpecificKey(emptyTreeID, refName, "Initial commit\n", unknownKey)
|
|
assert.ErrorIs(t, err, ErrUnknownSigningMethod)
|
|
})
|
|
}
|
|
|
|
func TestCommitUsingSpecificKey(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
refName := "refs/heads/main"
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write second tree
|
|
blobID, err := repo.WriteBlob([]byte("Hello, world!\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
treeWithContentsID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("README.md", blobID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create initial commit with no tree
|
|
expectedInitialCommitID := "648c569f3958b899e832f04750de52cf5d0db2fa"
|
|
commitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedInitialCommitID, commitID.String())
|
|
|
|
refHead, err := repo.GetReference(refName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, expectedInitialCommitID, refHead.String())
|
|
|
|
privateKey := artifacts.SSHRSAPrivate
|
|
|
|
// Create publicKey
|
|
keyPath := filepath.Join(tempDir, "ssh-key")
|
|
if err := os.WriteFile(keyPath, artifacts.SSHRSAPublicSSH, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
publicKey, err := ssh.NewKeyFromFile(keyPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Create second commit with tree
|
|
expectedSecondCommitID := "11020a7c78c4f903d0592ec2e8f73d00a17ec47e"
|
|
commitID, err = repo.CommitUsingSpecificKey(treeWithContentsID, refName, "Add README\n", privateKey)
|
|
assert.Nil(t, err)
|
|
|
|
// Verify commit signature using publicKey
|
|
err = repo.verifyCommitSignature(context.Background(), commitID, publicKey)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedSecondCommitID, commitID.String())
|
|
}
|
|
|
|
func TestRepositoryVerifyCommit(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sshSignedCommitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
gpgSignedCommitID := createTestGPGSignedCommit(t, repo)
|
|
|
|
// FIXME: fix gitsign testing
|
|
gitsignSignedCommitID := createTestSigstoreSignedCommit(t, repo)
|
|
|
|
keyDir := t.TempDir()
|
|
keyPath := filepath.Join(keyDir, "ssh-key")
|
|
if err := os.WriteFile(keyPath, artifacts.SSHRSAPublicSSH, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sshKey, err := ssh.NewKeyFromFile(keyPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
gpgKey, err := gpg.LoadGPGKeyFromBytes(artifacts.GPGKey1Public)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("ssh signed commit, verify with ssh key", func(t *testing.T) {
|
|
err = repo.verifyCommitSignature(context.Background(), sshSignedCommitID, sshKey)
|
|
assert.Nil(t, err)
|
|
})
|
|
|
|
t.Run("ssh signed commit, verify with gpg key", func(t *testing.T) {
|
|
err = repo.verifyCommitSignature(context.Background(), sshSignedCommitID, gpgKey)
|
|
assert.ErrorIs(t, err, ErrIncorrectVerificationKey)
|
|
})
|
|
|
|
t.Run("gpg signed commit, verify with gpg key", func(t *testing.T) {
|
|
err = repo.verifyCommitSignature(context.Background(), gpgSignedCommitID, gpgKey)
|
|
assert.Nil(t, err)
|
|
})
|
|
|
|
t.Run("gpg signed commit, verify with ssh key", func(t *testing.T) {
|
|
err = repo.verifyCommitSignature(context.Background(), gpgSignedCommitID, sshKey)
|
|
assert.ErrorIs(t, err, ErrIncorrectVerificationKey)
|
|
})
|
|
|
|
t.Run("gitsign signed commit, verify with ssh key", func(t *testing.T) {
|
|
err = repo.verifyCommitSignature(context.Background(), gitsignSignedCommitID, sshKey)
|
|
assert.ErrorIs(t, err, ErrIncorrectVerificationKey)
|
|
})
|
|
|
|
t.Run("unknown signing method", func(t *testing.T) {
|
|
unknownKey := &signerverifier.SSLibKey{KeyType: "unknown"}
|
|
err = repo.verifyCommitSignature(t.Context(), sshSignedCommitID, unknownKey)
|
|
assert.ErrorIs(t, err, ErrUnknownSigningMethod)
|
|
})
|
|
}
|
|
|
|
func TestRepositoryVerifyCommitSHA256(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false, WithSHA256Format())
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sshSignedCommitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
keyDir := t.TempDir()
|
|
keyPath := filepath.Join(keyDir, "ssh-key")
|
|
if err := os.WriteFile(keyPath, artifacts.SSHRSAPublicSSH, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sshKey, err := ssh.NewKeyFromFile(keyPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
gpgKey, err := gpg.LoadGPGKeyFromBytes(artifacts.GPGKey1Public)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// gitsign/sigstore signed commits are not exercised here: that test relies
|
|
// on a precomputed signature over a fixed SHA-1 commit object, which cannot
|
|
// be reproduced for a SHA-256 repository.
|
|
|
|
t.Run("ssh signed commit, verify with ssh key", func(t *testing.T) {
|
|
err = repo.verifyCommitSignature(context.Background(), sshSignedCommitID, sshKey)
|
|
assert.Nil(t, err)
|
|
})
|
|
|
|
t.Run("ssh signed commit, verify with gpg key", func(t *testing.T) {
|
|
err = repo.verifyCommitSignature(context.Background(), sshSignedCommitID, gpgKey)
|
|
assert.ErrorIs(t, err, ErrIncorrectVerificationKey)
|
|
})
|
|
|
|
t.Run("unknown signing method", func(t *testing.T) {
|
|
unknownKey := &signerverifier.SSLibKey{KeyType: "unknown"}
|
|
err = repo.verifyCommitSignature(t.Context(), sshSignedCommitID, unknownKey)
|
|
assert.ErrorIs(t, err, ErrUnknownSigningMethod)
|
|
})
|
|
}
|
|
|
|
func TestCommitUsingSpecificKeySignatureHeader(t *testing.T) {
|
|
for _, objectFormat := range []ObjectFormat{ObjectFormatSHA1, ObjectFormatSHA256} {
|
|
t.Run(string(objectFormat), func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false, WithObjectFormat(objectFormat))
|
|
|
|
emptyTreeID, err := NewTreeBuilder(repo).WriteTreeFromEntries(nil)
|
|
require.Nil(t, err)
|
|
|
|
commitID, err := repo.CommitUsingSpecificKey(emptyTreeID, "refs/heads/main", "Initial commit\n", artifacts.SSHRSAPrivate)
|
|
require.Nil(t, err)
|
|
|
|
raw, err := repo.executor("cat-file", "commit", commitID.String()).executeString()
|
|
require.Nil(t, err)
|
|
|
|
// The signature must be stored under the header matching the
|
|
// object's hash algorithm.
|
|
if objectFormat == ObjectFormatSHA256 {
|
|
assert.Contains(t, raw, "\ngpgsig-sha256 ")
|
|
assert.NotContains(t, raw, "\ngpgsig -")
|
|
} else {
|
|
assert.Contains(t, raw, "\ngpgsig -")
|
|
assert.NotContains(t, raw, "gpgsig-sha256")
|
|
}
|
|
|
|
keyDir := t.TempDir()
|
|
keyPath := filepath.Join(keyDir, "ssh-key")
|
|
require.Nil(t, os.WriteFile(keyPath, artifacts.SSHRSAPublicSSH, 0o600))
|
|
sshKey, err := ssh.NewKeyFromFile(keyPath)
|
|
require.Nil(t, err)
|
|
|
|
assert.Nil(t, repo.verifyCommitSignature(context.Background(), commitID, sshKey))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSignatureBlockCount(t *testing.T) {
|
|
tests := map[string]struct {
|
|
signature string
|
|
expected int
|
|
}{
|
|
"empty": {"", 0},
|
|
"single pgp": {"-----BEGIN PGP SIGNATURE-----\nabc\n-----END PGP SIGNATURE-----\n", 1},
|
|
"single ssh": {"-----BEGIN SSH SIGNATURE-----\nabc\n-----END SSH SIGNATURE-----\n", 1},
|
|
"two pgp blocks": {"-----BEGIN PGP SIGNATURE-----\na\n-----END PGP SIGNATURE-----\n-----BEGIN PGP SIGNATURE-----\nb\n-----END PGP SIGNATURE-----\n", 2},
|
|
"nested ssh blocks": {"-----BEGIN SSH SIGNATURE-----\n-----BEGIN SSH SIGNATURE-----\nabc\n-----END SSH SIGNATURE-----\n-----END SSH SIGNATURE-----\n", 2},
|
|
"no signature": {"not a signature", 0},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
assert.Equal(t, test.expected, signatureBlockCount(test.signature), name)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCommitSignatureRejectsMultipleSignatures(t *testing.T) {
|
|
tests := map[string]struct {
|
|
signingKey []byte
|
|
verificationKey func(t *testing.T) *signerverifier.SSLibKey
|
|
}{
|
|
"gpg": {
|
|
signingKey: artifacts.GPGKey1Private,
|
|
verificationKey: func(t *testing.T) *signerverifier.SSLibKey {
|
|
t.Helper()
|
|
key, err := gpg.LoadGPGKeyFromBytes(artifacts.GPGKey1Public)
|
|
require.Nil(t, err)
|
|
return key
|
|
},
|
|
},
|
|
"ssh": {
|
|
signingKey: artifacts.SSHED25519Private,
|
|
verificationKey: func(t *testing.T) *signerverifier.SSLibKey {
|
|
t.Helper()
|
|
keyPath := filepath.Join(t.TempDir(), "ssh-key.pub")
|
|
require.Nil(t, os.WriteFile(keyPath, artifacts.SSHED25519PublicSSH, 0o600))
|
|
key, err := ssh.NewKeyFromFile(keyPath)
|
|
require.Nil(t, err)
|
|
return key
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
goGitRepo, err := repo.GetGoGitRepository()
|
|
require.Nil(t, err)
|
|
|
|
testCommit := &object.Commit{
|
|
Author: object.Signature{Name: testName, Email: testEmail, When: testClock.Now()},
|
|
Committer: object.Signature{Name: testName, Email: testEmail, When: testClock.Now()},
|
|
Message: "Test commit\n",
|
|
TreeHash: plumbing.ZeroHash,
|
|
}
|
|
|
|
commitEncoded := goGitRepo.Storer.NewEncodedObject()
|
|
require.Nil(t, testCommit.EncodeWithoutSignature(commitEncoded))
|
|
reader, err := commitEncoded.Reader()
|
|
require.Nil(t, err)
|
|
contents, err := io.ReadAll(reader)
|
|
require.Nil(t, err)
|
|
|
|
sig, err := signGitObjectUsingKey(contents, test.signingKey)
|
|
require.Nil(t, err)
|
|
|
|
// Two (individually valid) signature blocks, each on their own
|
|
// lines, must be rejected as ambiguous rather than verified
|
|
// against the first.
|
|
block := strings.TrimRight(sig, "\n") + "\n"
|
|
testCommit.Signature = block + block
|
|
|
|
commitEncoded = goGitRepo.Storer.NewEncodedObject()
|
|
require.Nil(t, testCommit.Encode(commitEncoded))
|
|
commitID, err := goGitRepo.Storer.SetEncodedObject(commitEncoded)
|
|
require.Nil(t, err)
|
|
commitHash, err := NewHash(commitID.String())
|
|
require.Nil(t, err)
|
|
|
|
err = repo.verifyCommitSignature(context.Background(), commitHash, test.verificationKey(t))
|
|
assert.ErrorIs(t, err, ErrMultipleSignatures)
|
|
assert.ErrorIs(t, err, ErrIncorrectVerificationKey)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestKnowsCommit(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
refName := "refs/heads/main"
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
firstCommitID, err := repo.Commit(emptyTreeID, refName, "First commit", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
secondCommitID, err := repo.Commit(emptyTreeID, refName, "Second commit", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
unknownCommitID, err := repo.Commit(emptyTreeID, "refs/heads/unknown", "Unknown commit", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("check if second commit knows first", func(t *testing.T) {
|
|
knows, err := repo.KnowsCommit(secondCommitID, firstCommitID)
|
|
assert.Nil(t, err)
|
|
assert.True(t, knows)
|
|
})
|
|
|
|
t.Run("check that first commit does not know second", func(t *testing.T) {
|
|
knows, err := repo.KnowsCommit(firstCommitID, secondCommitID)
|
|
assert.Nil(t, err)
|
|
assert.False(t, knows)
|
|
})
|
|
|
|
t.Run("check that both commits know themselves", func(t *testing.T) {
|
|
knows, err := repo.KnowsCommit(firstCommitID, firstCommitID)
|
|
assert.Nil(t, err)
|
|
assert.True(t, knows)
|
|
|
|
knows, err = repo.KnowsCommit(secondCommitID, secondCommitID)
|
|
assert.Nil(t, err)
|
|
assert.True(t, knows)
|
|
})
|
|
|
|
t.Run("check that an unknown commit can't know a known commit", func(t *testing.T) {
|
|
knows, _ := repo.KnowsCommit(unknownCommitID, firstCommitID)
|
|
assert.False(t, knows)
|
|
})
|
|
|
|
t.Run("non-commit object as first arg", func(t *testing.T) {
|
|
blobID, err := repo.WriteBlob([]byte("test"))
|
|
require.Nil(t, err)
|
|
|
|
_, err = repo.KnowsCommit(blobID, firstCommitID)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
})
|
|
|
|
t.Run("non-commit object as second arg", func(t *testing.T) {
|
|
blobID, err := repo.WriteBlob([]byte("test"))
|
|
require.Nil(t, err)
|
|
|
|
_, err = repo.KnowsCommit(firstCommitID, blobID)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
})
|
|
}
|
|
|
|
func createTestGPGSignedCommit(t *testing.T, repo *Repository) Hash {
|
|
t.Helper()
|
|
|
|
goGitRepo, err := repo.GetGoGitRepository()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testCommit := &object.Commit{
|
|
Author: object.Signature{
|
|
Name: testName,
|
|
Email: testEmail,
|
|
When: testClock.Now(),
|
|
},
|
|
Committer: object.Signature{
|
|
Name: testName,
|
|
Email: testEmail,
|
|
When: testClock.Now(),
|
|
},
|
|
Message: "Test commit\n",
|
|
TreeHash: plumbing.ZeroHash,
|
|
}
|
|
|
|
commitEncoded := goGitRepo.Storer.NewEncodedObject()
|
|
if err := testCommit.EncodeWithoutSignature(commitEncoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r, err := commitEncoded.Reader()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(artifacts.GPGKey1Private))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sig := new(strings.Builder)
|
|
if err := openpgp.ArmoredDetachSign(sig, keyring[0], r, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testCommit.Signature = sig.String()
|
|
|
|
// Re-encode with the signature
|
|
commitEncoded = goGitRepo.Storer.NewEncodedObject()
|
|
if err := testCommit.Encode(commitEncoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
commitID, err := goGitRepo.Storer.SetEncodedObject(commitEncoded)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
commitHash, err := NewHash(commitID.String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return commitHash
|
|
}
|
|
|
|
func createTestSigstoreSignedCommit(t *testing.T, repo *Repository) Hash {
|
|
t.Helper()
|
|
|
|
goGitRepo, err := repo.GetGoGitRepository()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testCommit := &object.Commit{
|
|
Hash: plumbing.NewHash("d6b230478965e25477263aa65f1ca6d23d0c0d97"),
|
|
Author: object.Signature{
|
|
Name: "Aditya Sirish",
|
|
Email: "aditya@saky.in",
|
|
When: time.Date(2023, time.August, 1, 15, 44, 23, 0, time.FixedZone("", -4*3600)),
|
|
},
|
|
Committer: object.Signature{
|
|
Name: "Aditya Sirish",
|
|
Email: "aditya@saky.in",
|
|
When: time.Date(2023, time.August, 1, 15, 44, 23, 0, time.FixedZone("", -4*3600)),
|
|
},
|
|
Signature: `-----BEGIN SIGNED MESSAGE-----
|
|
MIIEMAYJKoZIhvcNAQcCoIIEITCCBB0CAQExDTALBglghkgBZQMEAgEwCwYJKoZI
|
|
hvcNAQcBoIIC0DCCAswwggJToAMCAQICFHIJCrBVHxoHlGos++k1xJxcElGaMAoG
|
|
CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln
|
|
c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIzMDgwMTE5NDQzMVoXDTIzMDgwMTE5NTQz
|
|
MVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD8d752TJfGtANVYoiJJn+o6
|
|
JPKj5NwEZs1AcVRT2qElikVun5t+bQ07iDFa/Xiun5ytZrEK2YJVgqdntLd6hSOj
|
|
ggFyMIIBbjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD
|
|
VR0OBBYEFAuYzgyBA01YSSN1v0fYenGo7+PcMB8GA1UdIwQYMBaAFN/T6c9WJBGW
|
|
+ajY6ShVosYuGGQ/MBwGA1UdEQEB/wQSMBCBDmFkaXR5YUBzYWt5LmluMCwGCisG
|
|
AQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEE
|
|
AYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYB
|
|
BAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6O
|
|
AAABibKhcJgAAAQDAEcwRQIgcWuz6NhFgdL0fNni6j0SOQnAgFpPEaN8jDH70mbD
|
|
uPMCIQCX8koEnIX4c9crMT1hfoBBf1Z/CHJ6HLLHpQwWfEUMIzAKBggqhkjOPQQD
|
|
AwNnADBkAjBozIBaBtEu7JUyYLH7Ly698E0o8DdIOmqcUMUYWNC6zyJVdrL5gAla
|
|
mQSxfObSQasCMHQuw8youTjmFJXT7pNOYX4DW25knt+6P+W/m6zwcRRe3dMjmUAB
|
|
gdBJb32+XXJMRDGCASYwggEiAgEBME8wNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2
|
|
MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUCFHIJCrBVHxoHlGos++k1
|
|
xJxcElGaMAsGCWCGSAFlAwQCAaBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw
|
|
HAYJKoZIhvcNAQkFMQ8XDTIzMDgwMTE5NDQzMlowLwYJKoZIhvcNAQkEMSIEIBe6
|
|
VHcVlkO8jRm/fbUipwxwxNaI7UFDAL38Jl8eUj/5MAoGCCqGSM49BAMCBEgwRgIh
|
|
AIYiRbnVeWjjgX2XwljDryzQN5RhUQaVH/AcUj+tbvWxAiEAhm9l3BU58tQsgyJW
|
|
oYBpMWLgg6AUzpxx9mITZ2EKr4c=
|
|
-----END SIGNED MESSAGE-----
|
|
`,
|
|
Message: "Test commit\n",
|
|
TreeHash: plumbing.NewHash("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),
|
|
}
|
|
|
|
commitEncoded := goGitRepo.Storer.NewEncodedObject()
|
|
if err := testCommit.EncodeWithoutSignature(commitEncoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
commitID, err := goGitRepo.Storer.SetEncodedObject(commitEncoded)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
commitHash, err := NewHash(commitID.String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return commitHash
|
|
}
|
|
|
|
func TestRepositoryGetCommitMessage(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
refName := "refs/heads/main"
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
message := "Initial commit"
|
|
commit, err := repo.Commit(emptyTreeID, refName, message, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
commitMessage, err := repo.GetCommitMessage(commit)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, message, commitMessage)
|
|
|
|
t.Run("non-commit object", func(t *testing.T) {
|
|
blobID, err := repo.WriteBlob([]byte("test"))
|
|
require.Nil(t, err)
|
|
|
|
_, err = repo.GetCommitMessage(blobID)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
})
|
|
}
|
|
|
|
func TestGetCommitTreeID(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
refName := "refs/heads/main"
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write second tree
|
|
blobID, err := repo.WriteBlob([]byte("Hello, world!\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
treeWithContentsID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("README.md", blobID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create initial commit with no tree
|
|
initialCommitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
initialCommitTreeID, err := repo.GetCommitTreeID(initialCommitID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, emptyTreeID, initialCommitTreeID)
|
|
|
|
// Create second commit with tree
|
|
secondCommitID, err := repo.Commit(treeWithContentsID, refName, "Add README\n", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
secondCommitTreeID, err := repo.GetCommitTreeID(secondCommitID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, treeWithContentsID, secondCommitTreeID)
|
|
|
|
t.Run("non-commit object", func(t *testing.T) {
|
|
blobID, err := repo.WriteBlob([]byte("test"))
|
|
require.Nil(t, err)
|
|
|
|
_, err = repo.GetCommitTreeID(blobID)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
})
|
|
}
|
|
|
|
func TestGetCommitParentIDs(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
refName := "refs/heads/main"
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create initial commit
|
|
initialCommitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
initialCommitParentIDs, err := repo.GetCommitParentIDs(initialCommitID)
|
|
assert.Nil(t, err)
|
|
assert.Empty(t, initialCommitParentIDs)
|
|
|
|
// Create second commit
|
|
secondCommitID, err := repo.Commit(emptyTreeID, refName, "Add README\n", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
secondCommitParentIDs, err := repo.GetCommitParentIDs(secondCommitID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []Hash{initialCommitID}, secondCommitParentIDs)
|
|
|
|
// Create merge commit
|
|
mergeCommitID := repo.commitWithParents(t, emptyTreeID, []Hash{initialCommitID, secondCommitID}, "Merge commit\n", false)
|
|
|
|
mergeCommitParentIDs, err := repo.GetCommitParentIDs(mergeCommitID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, []Hash{initialCommitID, secondCommitID}, mergeCommitParentIDs)
|
|
|
|
blobID, err := repo.WriteBlob([]byte("test"))
|
|
require.Nil(t, err)
|
|
_, err = repo.GetCommitParentIDs(blobID)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
}
|
|
|
|
func TestGetCommonAncestor(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
refName := "refs/heads/main"
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
initialCommitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add child commit A
|
|
commitA, err := repo.Commit(emptyTreeID, refName, "Second commit A\n", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add child commit B
|
|
commitB := repo.commitWithParents(t, emptyTreeID, []Hash{initialCommitID}, "Second commit B\n", false)
|
|
|
|
// Test commits, ensure we get back initial commit
|
|
commonAncestor, err := repo.GetCommonAncestor(commitA, commitB)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, initialCommitID, commonAncestor)
|
|
|
|
// Test with disjoint commit histories
|
|
commitDisconnected := repo.commitWithParents(t, emptyTreeID, nil, "Disconnected initial commit\n", false)
|
|
|
|
_, err = repo.GetCommonAncestor(commitDisconnected, commitA)
|
|
assert.NotNil(t, err)
|
|
|
|
t.Run("non-commit object as first arg", func(t *testing.T) {
|
|
blobID, err := repo.WriteBlob([]byte("test"))
|
|
require.Nil(t, err)
|
|
|
|
_, err = repo.GetCommonAncestor(blobID, commitA)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
})
|
|
|
|
t.Run("non-commit object as second arg", func(t *testing.T) {
|
|
blobID, err := repo.WriteBlob([]byte("test"))
|
|
require.Nil(t, err)
|
|
|
|
_, err = repo.GetCommonAncestor(commitA, blobID)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
})
|
|
}
|
|
|
|
func TestEnsureIsCommit(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
require.Nil(t, err)
|
|
|
|
commitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
blobID, err := repo.WriteBlob([]byte("test"))
|
|
require.Nil(t, err)
|
|
|
|
t.Run("valid commit", func(t *testing.T) {
|
|
err := repo.ensureIsCommit(commitID)
|
|
assert.Nil(t, err)
|
|
})
|
|
|
|
t.Run("non-commit object", func(t *testing.T) {
|
|
err := repo.ensureIsCommit(blobID)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
})
|
|
|
|
t.Run("non-existent object", func(t *testing.T) {
|
|
err := repo.ensureIsCommit(ZeroHash)
|
|
assert.ErrorContains(t, err, "unable to inspect if object is commit")
|
|
})
|
|
}
|