mirror of
https://github.com/vee1e/gittuf.git
synced 2026-09-01 18:27:06 +00:00
Restructure storage so gittuf verification can run over backends other
than the git binary (e.g. go-git):
- pkg/githash: concrete Git object hash, stdlib-only.
gitinterface.Hash aliases it.
- pkg/gitstore: the single Storer interface (24 methods) that all
storage consumers program against, plus the shared
ErrReferenceNotFound sentinel. *gitinterface.Repository satisfies it
structurally (compile-time asserted). Also defines ConfigKey, the
canonical type for the Git config settings gittuf reads.
- pkg/rsl (from internal/rsl): entry model, codec, and readers over
gitstore.Storer; zero gitinterface/sigstore dependencies. rsl.Hash
aliases githash.Hash; nil is the unset-Hash sentinel and IsZero
matches nil and empty as well as both format zeros (no
object-format-unaware ZeroHash). Entry commits (empty tree on the
RSL ref) are owned by the package; no storer adapter.
- internal/signerverifier/gitobject: verifies commit/tag signatures
over (payload, signature) bytes, Rekor URL as an option. The storage
half is Repository.GetObjectSignature. Removes sigstore, cosign, and
gitsign from gitinterface's dependency tree.
- internal/propagation: propagation workflow, moved off pkg/rsl's
public API (its tuf directive types are internal).
- internal/{attestations,cache,policy}: storage via gitstore.Storer;
tree writing via WriteTree(blobs, subtrees).
Breaking changes to pkg/gitinterface: Repository.VerifySignature and
the verification sentinels are removed (use gitobject.Verify);
ErrReferenceNotFound now aliases gitstore's. Repository.GetGitConfig
(which returned the whole config map) is replaced by
LookupConfig(gitstore.ConfigKey), returning a single setting's value.
Policy resolves the Rekor override from git config once per
verification and extracts signed payloads once per object instead of
per key attempt.
Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
306 lines
9 KiB
Go
306 lines
9 KiB
Go
// Copyright The gittuf Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package gitinterface
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/gittuf/gittuf/pkg/gitstore"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var _ gitstore.Storer = (*Repository)(nil)
|
|
|
|
func TestErrReferenceNotFoundIsGitstoreSentinel(t *testing.T) {
|
|
t.Parallel()
|
|
assert.ErrorIs(t, ErrReferenceNotFound, gitstore.ErrReferenceNotFound)
|
|
}
|
|
|
|
func TestRepository(t *testing.T) {
|
|
t.Run("repository.isBare", func(t *testing.T) {
|
|
t.Run("bare=true", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, true)
|
|
assert.True(t, repo.IsBare())
|
|
})
|
|
|
|
t.Run("bare=false", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
assert.False(t, repo.IsBare())
|
|
})
|
|
})
|
|
|
|
t.Run("with specified path, not bare", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
_ = CreateTestGitRepository(t, tmpDir, false)
|
|
repo, err := LoadRepository(tmpDir)
|
|
assert.Nil(t, err)
|
|
|
|
expectedPath, err := filepath.EvalSymlinks(filepath.Join(tmpDir, ".git"))
|
|
require.Nil(t, err)
|
|
actualPath, err := filepath.EvalSymlinks(repo.GetGitDir())
|
|
require.Nil(t, err)
|
|
assert.Equal(t, expectedPath, actualPath)
|
|
})
|
|
|
|
t.Run("with specified path, is bare", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
_ = CreateTestGitRepository(t, tmpDir, true)
|
|
repo, err := LoadRepository(tmpDir)
|
|
assert.Nil(t, err)
|
|
|
|
expectedPath, err := filepath.EvalSymlinks(tmpDir)
|
|
require.Nil(t, err)
|
|
actualPath, err := filepath.EvalSymlinks(repo.GetGitDir())
|
|
require.Nil(t, err)
|
|
assert.Equal(t, expectedPath, actualPath)
|
|
})
|
|
|
|
t.Run("empty path", func(t *testing.T) {
|
|
_, err := LoadRepository("")
|
|
assert.ErrorIs(t, err, ErrRepositoryPathNotSpecified)
|
|
})
|
|
|
|
t.Run("invalid path", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
if _, has, err := findGitDirPath(tmpDir); err == nil && has {
|
|
tmpDir = filepath.Join(tmpDir, "invalid-repository")
|
|
require.Nil(t, os.Mkdir(tmpDir, 0o700))
|
|
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, ".git"), []byte("not a gitdir file"), 0o600))
|
|
}
|
|
|
|
_, err := LoadRepository(tmpDir)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestEnsureNoCompatObjectFormat(t *testing.T) {
|
|
t.Run("no compat object format", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
|
|
|
|
assert.Nil(t, repo.ensureNoCompatObjectFormat())
|
|
})
|
|
|
|
t.Run("compat object format", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
|
|
|
|
require.Nil(t, repo.SetGitConfig("extensions.compatObjectFormat", "sha1"))
|
|
|
|
assert.ErrorIs(t, repo.ensureNoCompatObjectFormat(), ErrCompatObjectFormatUnsupported)
|
|
|
|
_, err := LoadRepository(tmpDir)
|
|
assert.ErrorIs(t, err, ErrCompatObjectFormatUnsupported)
|
|
})
|
|
|
|
t.Run("missing config", func(t *testing.T) {
|
|
repo := &Repository{gitDirPath: t.TempDir()}
|
|
|
|
err := repo.ensureNoCompatObjectFormat()
|
|
assert.ErrorContains(t, err, "unable to read repository config")
|
|
})
|
|
|
|
t.Run("invalid config", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, "config"), []byte("[extensions\n"), 0o600))
|
|
|
|
repo := &Repository{gitDirPath: tmpDir}
|
|
err := repo.ensureNoCompatObjectFormat()
|
|
assert.ErrorContains(t, err, "unable to parse repository config")
|
|
})
|
|
}
|
|
|
|
func TestFindGitDirPath(t *testing.T) {
|
|
t.Run("worktree git directory", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
_ = CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
nestedDir := filepath.Join(tmpDir, "nested", "dir")
|
|
require.Nil(t, os.MkdirAll(nestedDir, 0o700))
|
|
|
|
gitDirPath, has, err := findGitDirPath(nestedDir)
|
|
require.Nil(t, err)
|
|
assert.True(t, has)
|
|
assert.Equal(t, filepath.Join(tmpDir, ".git"), gitDirPath)
|
|
})
|
|
|
|
t.Run("bare git directory", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
_ = CreateTestGitRepository(t, tmpDir, true)
|
|
|
|
gitDirPath, has, err := findGitDirPath(tmpDir)
|
|
require.Nil(t, err)
|
|
assert.True(t, has)
|
|
assert.Equal(t, tmpDir, gitDirPath)
|
|
})
|
|
|
|
t.Run("gitdir file", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
worktreePath := filepath.Join(tmpDir, "worktree")
|
|
require.Nil(t, os.MkdirAll(worktreePath, 0o700))
|
|
|
|
gitDirPath := filepath.Join(tmpDir, "actual.git")
|
|
require.Nil(t, os.WriteFile(filepath.Join(worktreePath, ".git"), []byte("gitdir: ../actual.git\n"), 0o600))
|
|
|
|
gotGitDirPath, has, err := findGitDirPath(worktreePath)
|
|
require.Nil(t, err)
|
|
assert.True(t, has)
|
|
assert.Equal(t, gitDirPath, gotGitDirPath)
|
|
})
|
|
|
|
t.Run("invalid gitdir file", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, ".git"), []byte("not a gitdir file"), 0o600))
|
|
|
|
_, has, err := findGitDirPath(tmpDir)
|
|
assert.False(t, has)
|
|
assert.ErrorContains(t, err, "invalid gitdir file")
|
|
})
|
|
|
|
t.Run("no git directory", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
if _, has, err := findGitDirPath(tmpDir); err == nil && has {
|
|
tmpDir = "/dev"
|
|
if _, has, err := findGitDirPath(tmpDir); err != nil || has {
|
|
t.Skip("unable to find a filesystem path outside a Git repository")
|
|
}
|
|
}
|
|
|
|
_, has, err := findGitDirPath(tmpDir)
|
|
require.Nil(t, err)
|
|
assert.False(t, has)
|
|
})
|
|
}
|
|
|
|
func TestReadGitDirFile(t *testing.T) {
|
|
t.Run("absolute gitdir path", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
gitDirPath := filepath.Join(tmpDir, "actual.git")
|
|
gitDirFilePath := filepath.Join(tmpDir, ".git")
|
|
require.Nil(t, os.WriteFile(gitDirFilePath, []byte("gitdir: "+gitDirPath+"\n"), 0o600))
|
|
|
|
gotGitDirPath, err := readGitDirFile(gitDirFilePath, tmpDir)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, gitDirPath, gotGitDirPath)
|
|
})
|
|
|
|
t.Run("missing gitdir file", func(t *testing.T) {
|
|
_, err := readGitDirFile(filepath.Join(t.TempDir(), ".git"), t.TempDir())
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestIsBareGitDir(t *testing.T) {
|
|
t.Run("config without head", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, "config"), nil, 0o600))
|
|
|
|
assert.False(t, isBareGitDir(tmpDir))
|
|
})
|
|
|
|
t.Run("head as directory", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
require.Nil(t, os.WriteFile(filepath.Join(tmpDir, "config"), nil, 0o600))
|
|
require.Nil(t, os.Mkdir(filepath.Join(tmpDir, "HEAD"), 0o700))
|
|
|
|
assert.False(t, isBareGitDir(tmpDir))
|
|
})
|
|
}
|
|
|
|
func TestRepositoryObjectFormat(t *testing.T) {
|
|
t.Run("sha1", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false, WithObjectFormat(ObjectFormatSHA1))
|
|
assert.Equal(t, ObjectFormatSHA1, repo.GetObjectFormat())
|
|
assert.Equal(t, "0000000000000000000000000000000000000000", repo.ZeroHash().String())
|
|
|
|
loaded, err := LoadRepository(tmpDir)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, ObjectFormatSHA1, loaded.GetObjectFormat())
|
|
})
|
|
|
|
t.Run("sha256", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false, WithSHA256Format())
|
|
assert.Equal(t, ObjectFormatSHA256, repo.GetObjectFormat())
|
|
assert.Equal(t, "0000000000000000000000000000000000000000000000000000000000000000", repo.ZeroHash().String())
|
|
|
|
loaded, err := LoadRepository(tmpDir)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, ObjectFormatSHA256, loaded.GetObjectFormat())
|
|
})
|
|
|
|
t.Run("GetGoGitRepository", func(t *testing.T) {
|
|
for _, bare := range []bool{false, true} {
|
|
name := "worktree"
|
|
if bare {
|
|
name = "bare"
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, bare)
|
|
ggr, err := repo.GetGoGitRepository()
|
|
require.NoError(t, err, "GetGoGitRepository on %s repo", name)
|
|
_, err = ggr.Head()
|
|
// Empty repo: ErrReferenceNotFound is fine; what matters is the
|
|
// repo opened. Anything else (esp. ErrRepositoryNotExists) is a
|
|
// failure to open the storage.
|
|
if err != nil {
|
|
assert.ErrorContains(t, err, "reference not found")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("GetGoGitRepository on .git-suffixed bare repo", func(t *testing.T) {
|
|
// Forge-style bare repos are conventionally named <name>.git.
|
|
dir := filepath.Join(t.TempDir(), "demo.git")
|
|
repo := CreateTestGitRepository(t, dir, true)
|
|
_, err := repo.GetGoGitRepository()
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestLoadRepositoryConcurrent(t *testing.T) {
|
|
// LoadRepository must not change the process working directory; concurrent
|
|
// callers for different paths must each resolve their own gitDirPath.
|
|
dirA := t.TempDir()
|
|
dirB := t.TempDir()
|
|
_ = CreateTestGitRepository(t, dirA, true)
|
|
_ = CreateTestGitRepository(t, dirB, false)
|
|
|
|
wantA, err := filepath.EvalSymlinks(dirA)
|
|
require.NoError(t, err)
|
|
wantB, err := filepath.EvalSymlinks(filepath.Join(dirB, ".git"))
|
|
require.NoError(t, err)
|
|
|
|
const iterations = 20
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < iterations; i++ {
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
r, err := LoadRepository(dirA)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, wantA, r.GetGitDir())
|
|
}
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
r, err := LoadRepository(dirB)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, wantB, r.GetGitDir())
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|