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>
508 lines
15 KiB
Go
508 lines
15 KiB
Go
// Copyright The gittuf Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package gitinterface
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
artifacts "github.com/gittuf/gittuf/internal/testartifacts"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetReferenceNotFoundZeroHash(t *testing.T) {
|
|
t.Parallel()
|
|
for _, objectFormat := range []ObjectFormat{ObjectFormatSHA1, ObjectFormatSHA256} {
|
|
t.Run(string(objectFormat), func(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false, WithObjectFormat(objectFormat))
|
|
|
|
tip, err := repo.GetReference("refs/heads/does-not-exist")
|
|
assert.ErrorIs(t, err, ErrReferenceNotFound)
|
|
assert.Equal(t, repo.ZeroHash(), tip)
|
|
assert.True(t, tip.IsZero())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetReference(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)
|
|
}
|
|
|
|
commitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
refTip, err := repo.GetReference(refName)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, commitID, refTip)
|
|
}
|
|
|
|
func TestSetReference(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)
|
|
}
|
|
|
|
firstCommitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
// Create second commit with tree
|
|
secondCommitID, err := repo.Commit(emptyTreeID, refName, "Add README\n", false)
|
|
require.Nil(t, err)
|
|
|
|
refTip, err := repo.GetReference(refName)
|
|
require.Nil(t, err)
|
|
require.Equal(t, secondCommitID, refTip)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
err = repo.SetReference(refName, firstCommitID)
|
|
assert.Nil(t, err)
|
|
|
|
refTip, err = repo.GetReference(refName)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, firstCommitID, refTip)
|
|
})
|
|
|
|
t.Run("invalid ref name", func(t *testing.T) {
|
|
err = repo.SetReference("invalid ref name", firstCommitID)
|
|
assert.ErrorContains(t, err, "unable to set Git reference")
|
|
})
|
|
}
|
|
|
|
func TestCheckAndSetReference(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)
|
|
}
|
|
|
|
firstCommitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
// Create second commit with tree
|
|
secondCommitID, err := repo.Commit(emptyTreeID, refName, "Add README\n", false)
|
|
require.Nil(t, err)
|
|
|
|
refTip, err := repo.GetReference(refName)
|
|
require.Nil(t, err)
|
|
require.Equal(t, secondCommitID, refTip)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
err = repo.CheckAndSetReference(refName, firstCommitID, secondCommitID)
|
|
assert.Nil(t, err)
|
|
|
|
refTip, err = repo.GetReference(refName)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, firstCommitID, refTip)
|
|
})
|
|
|
|
t.Run("error when old value mismatches", func(t *testing.T) {
|
|
err = repo.SetReference(refName, secondCommitID)
|
|
require.Nil(t, err)
|
|
|
|
err = repo.CheckAndSetReference(refName, firstCommitID, firstCommitID)
|
|
assert.ErrorContains(t, err, "unable to set Git reference")
|
|
})
|
|
}
|
|
|
|
func TestGetSymbolicReferenceTarget(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)
|
|
}
|
|
|
|
_, err = repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
// HEAD must be set to the main branch -> this is handled by git init
|
|
head, err := repo.GetSymbolicReferenceTarget("HEAD")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, refName, head)
|
|
}
|
|
|
|
func TestSetSymbolicReference(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
refName := "refs/heads/not-main" // we want to ensure it's set to something other than the default main
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
// Write empty tree
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
head, err := repo.GetSymbolicReferenceTarget("HEAD")
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "refs/heads/main", head)
|
|
|
|
err = repo.SetSymbolicReference("HEAD", refName)
|
|
assert.Nil(t, err)
|
|
|
|
head, err = repo.GetSymbolicReferenceTarget("HEAD")
|
|
require.Nil(t, err)
|
|
assert.Equal(t, refName, head) // not main anymore
|
|
}
|
|
|
|
func TestRepositoryRefSpec(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
shortRefName := "master"
|
|
qualifiedRefName := "refs/heads/master"
|
|
qualifiedRemoteRefName := "refs/remotes/origin/master"
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
emptyTreeHash, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
commitID, err := repo.Commit(emptyTreeHash, qualifiedRefName, "Test Commit", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
refHash, err := repo.GetReference(qualifiedRefName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, commitID, refHash, "unexpected value configuring test repo")
|
|
|
|
tests := map[string]struct {
|
|
repo *Repository
|
|
refName string
|
|
remoteName string
|
|
fastForwardOnly bool
|
|
expectedRefSpec string
|
|
expectedError error
|
|
}{
|
|
"standard branch, not fast forward only, no remote": {
|
|
refName: "refs/heads/main",
|
|
expectedRefSpec: "+refs/heads/main:refs/heads/main",
|
|
},
|
|
"standard branch, fast forward only, no remote": {
|
|
refName: "refs/heads/main",
|
|
fastForwardOnly: true,
|
|
expectedRefSpec: "refs/heads/main:refs/heads/main",
|
|
},
|
|
"standard branch, not fast forward only, remote": {
|
|
refName: "refs/heads/main",
|
|
remoteName: "origin",
|
|
expectedRefSpec: "+refs/heads/main:refs/remotes/origin/main",
|
|
},
|
|
"standard branch, fast forward only, remote": {
|
|
refName: "refs/heads/main",
|
|
remoteName: "origin",
|
|
fastForwardOnly: true,
|
|
expectedRefSpec: "refs/heads/main:refs/remotes/origin/main",
|
|
},
|
|
"non-standard branch, not fast forward only, no remote": {
|
|
refName: "refs/heads/foo/bar",
|
|
expectedRefSpec: "+refs/heads/foo/bar:refs/heads/foo/bar",
|
|
},
|
|
"non-standard branch, fast forward only, no remote": {
|
|
refName: "refs/heads/foo/bar",
|
|
fastForwardOnly: true,
|
|
expectedRefSpec: "refs/heads/foo/bar:refs/heads/foo/bar",
|
|
},
|
|
"non-standard branch, not fast forward only, remote": {
|
|
refName: "refs/heads/foo/bar",
|
|
remoteName: "origin",
|
|
expectedRefSpec: "+refs/heads/foo/bar:refs/remotes/origin/foo/bar",
|
|
},
|
|
"non-standard branch, fast forward only, remote": {
|
|
refName: "refs/heads/foo/bar",
|
|
remoteName: "origin",
|
|
fastForwardOnly: true,
|
|
expectedRefSpec: "refs/heads/foo/bar:refs/remotes/origin/foo/bar",
|
|
},
|
|
"short branch, not fast forward only, no remote": {
|
|
refName: shortRefName,
|
|
repo: repo,
|
|
expectedRefSpec: fmt.Sprintf("+%s:%s", qualifiedRefName, qualifiedRefName),
|
|
},
|
|
"short branch, fast forward only, no remote": {
|
|
refName: shortRefName,
|
|
repo: repo,
|
|
fastForwardOnly: true,
|
|
expectedRefSpec: fmt.Sprintf("%s:%s", qualifiedRefName, qualifiedRefName),
|
|
},
|
|
"short branch, not fast forward only, remote": {
|
|
refName: shortRefName,
|
|
repo: repo,
|
|
remoteName: "origin",
|
|
expectedRefSpec: fmt.Sprintf("+%s:%s", qualifiedRefName, qualifiedRemoteRefName),
|
|
},
|
|
"short branch, fast forward only, remote": {
|
|
refName: shortRefName,
|
|
repo: repo,
|
|
fastForwardOnly: true,
|
|
remoteName: "origin",
|
|
expectedRefSpec: fmt.Sprintf("%s:%s", qualifiedRefName, qualifiedRemoteRefName),
|
|
},
|
|
"custom namespace, not fast forward only, no remote": {
|
|
refName: "refs/foo/bar",
|
|
expectedRefSpec: "+refs/foo/bar:refs/foo/bar",
|
|
},
|
|
"custom namespace, fast forward only, no remote": {
|
|
refName: "refs/foo/bar",
|
|
fastForwardOnly: true,
|
|
expectedRefSpec: "refs/foo/bar:refs/foo/bar",
|
|
},
|
|
"custom namespace, not fast forward only, remote": {
|
|
refName: "refs/foo/bar",
|
|
remoteName: "origin",
|
|
expectedRefSpec: "+refs/foo/bar:refs/remotes/origin/foo/bar",
|
|
},
|
|
"custom namespace, fast forward only, remote": {
|
|
refName: "refs/foo/bar",
|
|
remoteName: "origin",
|
|
fastForwardOnly: true,
|
|
expectedRefSpec: "refs/foo/bar:refs/remotes/origin/foo/bar",
|
|
},
|
|
"tag, not fast forward only, no remote": {
|
|
refName: "refs/tags/v1.0.0",
|
|
fastForwardOnly: false,
|
|
expectedRefSpec: "refs/tags/v1.0.0:refs/tags/v1.0.0",
|
|
},
|
|
"tag, fast forward only, no remote": {
|
|
refName: "refs/tags/v1.0.0",
|
|
fastForwardOnly: true,
|
|
expectedRefSpec: "refs/tags/v1.0.0:refs/tags/v1.0.0",
|
|
},
|
|
"tag, not fast forward only, remote": {
|
|
refName: "refs/tags/v1.0.0",
|
|
remoteName: "origin",
|
|
fastForwardOnly: false,
|
|
expectedRefSpec: "refs/tags/v1.0.0:refs/tags/v1.0.0",
|
|
},
|
|
"tag, fast forward only, remote": {
|
|
refName: "refs/tags/v1.0.0",
|
|
remoteName: "origin",
|
|
fastForwardOnly: true,
|
|
expectedRefSpec: "refs/tags/v1.0.0:refs/tags/v1.0.0",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
refSpec, err := test.repo.RefSpec(test.refName, test.remoteName, test.fastForwardOnly)
|
|
assert.ErrorIs(t, err, test.expectedError, fmt.Sprintf("unexpected error in test '%s'", name))
|
|
assert.Equal(t, test.expectedRefSpec, refSpec, fmt.Sprintf("unexpected refspec returned in test '%s'", name))
|
|
}
|
|
|
|
t.Run("nonexistent ref", func(t *testing.T) {
|
|
_, err := repo.RefSpec("nonexistent", "", false)
|
|
assert.ErrorIs(t, err, ErrReferenceNotFound)
|
|
})
|
|
}
|
|
|
|
func TestBranchReferenceName(t *testing.T) {
|
|
tests := map[string]struct {
|
|
branchName string
|
|
expectedReferenceName string
|
|
}{
|
|
"short name": {
|
|
branchName: "main",
|
|
expectedReferenceName: "refs/heads/main",
|
|
},
|
|
"reference name": {
|
|
branchName: "refs/heads/main",
|
|
expectedReferenceName: "refs/heads/main",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
referenceName := BranchReferenceName(test.branchName)
|
|
assert.Equal(t, test.expectedReferenceName, referenceName, fmt.Sprintf("unexpected branch reference received in test '%s'", name))
|
|
}
|
|
}
|
|
|
|
func TestTagReferenceName(t *testing.T) {
|
|
tests := map[string]struct {
|
|
tagName string
|
|
expectedReferenceName string
|
|
}{
|
|
"short name": {
|
|
tagName: "v1",
|
|
expectedReferenceName: "refs/tags/v1",
|
|
},
|
|
"reference name": {
|
|
tagName: "refs/tags/v1",
|
|
expectedReferenceName: "refs/tags/v1",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
referenceName := TagReferenceName(test.tagName)
|
|
assert.Equal(t, test.expectedReferenceName, referenceName, fmt.Sprintf("unexpected tag reference received in test '%s'", name))
|
|
}
|
|
}
|
|
|
|
func TestDeleteReference(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)
|
|
}
|
|
|
|
commitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
refTip, err := repo.GetReference(refName)
|
|
require.Nil(t, err)
|
|
require.Equal(t, commitID, refTip)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
err = repo.DeleteReference(refName)
|
|
assert.Nil(t, err)
|
|
|
|
_, err = repo.GetReference(refName)
|
|
assert.ErrorIs(t, err, ErrReferenceNotFound)
|
|
})
|
|
|
|
t.Run("invalid ref name", func(t *testing.T) {
|
|
err = repo.DeleteReference("invalid ref name")
|
|
assert.ErrorContains(t, err, "unable to delete Git reference")
|
|
})
|
|
}
|
|
|
|
func TestRemoteReferenceName(t *testing.T) {
|
|
tests := map[string]struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
"adds prefix if missing": {
|
|
input: "origin/main",
|
|
expected: "refs/remotes/origin/main",
|
|
},
|
|
"keeps prefix if already present": {
|
|
input: "refs/remotes/origin/main",
|
|
expected: "refs/remotes/origin/main",
|
|
},
|
|
"empty input returns prefix only": {
|
|
input: "",
|
|
expected: "refs/remotes/",
|
|
},
|
|
"exact prefix is preserved": {
|
|
input: "refs/remotes/",
|
|
expected: "refs/remotes/",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
referenceName := RemoteReferenceName(test.input)
|
|
assert.Equal(t, test.expected, referenceName, fmt.Sprintf("unexpected remote reference for input %s", name))
|
|
}
|
|
}
|
|
|
|
func TestAbsoluteReference(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)
|
|
|
|
_, err = repo.TagUsingSpecificKey(commitID, "v1.0", "v1.0\n", artifacts.SSHED25519Private)
|
|
require.Nil(t, err)
|
|
|
|
err = repo.SetReference("refs/custom/myref", commitID)
|
|
require.Nil(t, err)
|
|
|
|
err = repo.SetReference("refs/remotes/origin/main", commitID)
|
|
require.Nil(t, err)
|
|
|
|
err = repo.SetSymbolicReference("refs/remotes/origin/HEAD", "refs/remotes/origin/main")
|
|
require.Nil(t, err)
|
|
|
|
t.Run("symbolic ref HEAD", func(t *testing.T) {
|
|
ref, err := repo.AbsoluteReference("HEAD")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "refs/heads/main", ref)
|
|
})
|
|
|
|
t.Run("fully qualified branch", func(t *testing.T) {
|
|
ref, err := repo.AbsoluteReference("refs/heads/main")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "refs/heads/main", ref)
|
|
})
|
|
|
|
t.Run("short branch name", func(t *testing.T) {
|
|
ref, err := repo.AbsoluteReference("main")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "refs/heads/main", ref)
|
|
})
|
|
|
|
t.Run("tag name", func(t *testing.T) {
|
|
ref, err := repo.AbsoluteReference("v1.0")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "refs/tags/v1.0", ref)
|
|
})
|
|
|
|
t.Run("custom ref", func(t *testing.T) {
|
|
ref, err := repo.AbsoluteReference("custom/myref")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "refs/custom/myref", ref)
|
|
})
|
|
|
|
t.Run("remote tracking ref", func(t *testing.T) {
|
|
ref, err := repo.AbsoluteReference("origin/main")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "refs/remotes/origin/main", ref)
|
|
})
|
|
|
|
t.Run("remote HEAD", func(t *testing.T) {
|
|
ref, err := repo.AbsoluteReference("origin")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "refs/remotes/origin/HEAD", ref)
|
|
})
|
|
|
|
t.Run("non-existent ref", func(t *testing.T) {
|
|
_, err := repo.AbsoluteReference("nonexistent")
|
|
assert.ErrorIs(t, err, ErrReferenceNotFound)
|
|
})
|
|
}
|