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>
158 lines
4.7 KiB
Go
158 lines
4.7 KiB
Go
// Copyright The gittuf Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package gitinterface
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
|
|
artifacts "github.com/gittuf/gittuf/internal/testartifacts"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCanSign(t *testing.T) {
|
|
// Isolate from the developer's global and system git config so a host
|
|
// with user.signingkey or gpg.format set in ~/.gitconfig does not leak
|
|
// into the temp repos through scoped config lookups. os.DevNull resolves
|
|
// to the platform-appropriate null device (NUL on Windows).
|
|
t.Setenv("GIT_CONFIG_GLOBAL", os.DevNull)
|
|
t.Setenv("GIT_CONFIG_SYSTEM", os.DevNull)
|
|
|
|
tests := map[string]struct {
|
|
config map[string]string
|
|
expectedError error
|
|
}{
|
|
"explicit gpg, no key": {
|
|
config: map[string]string{
|
|
"gpg.format": "gpg",
|
|
},
|
|
},
|
|
"explicit gpg, explicit key": {
|
|
config: map[string]string{
|
|
"gpg.format": "gpg",
|
|
"user.signingkey": "gpg-fingerprint",
|
|
},
|
|
},
|
|
"no signing method, explicit key": {
|
|
config: map[string]string{
|
|
"user.signingkey": "gpg-fingerprint",
|
|
},
|
|
},
|
|
"explicit ssh, explicit key": {
|
|
config: map[string]string{
|
|
"gpg.format": "ssh",
|
|
"user.signingkey": "ssh/key/path",
|
|
},
|
|
},
|
|
"explicit ssh, no key": {
|
|
config: map[string]string{
|
|
"gpg.format": "ssh",
|
|
},
|
|
expectedError: ErrSigningKeyNotSpecified,
|
|
},
|
|
"explicit x509, no key": {
|
|
config: map[string]string{
|
|
"gpg.format": "x509",
|
|
},
|
|
},
|
|
"explicit x509, explicit key": {
|
|
config: map[string]string{
|
|
"gpg.format": "x509",
|
|
"user.signingkey": "x509-signing-info",
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := setupRepository(t, tmpDir, false, ObjectFormatSHA1) // explicitly not using CreateTestGitRepository as that includes signing configurations
|
|
|
|
for key, value := range test.config {
|
|
if err := repo.SetGitConfig(key, value); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
err := repo.CanSign()
|
|
assert.ErrorIs(t, err, test.expectedError, fmt.Sprintf("unexpected result in test '%s'", name))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetObjectSignature(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
emptyTreeHash, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
require.Nil(t, err)
|
|
|
|
t.Run("signed commit", func(t *testing.T) {
|
|
t.Parallel()
|
|
commitID, err := repo.CommitUsingSpecificKey(emptyTreeHash, "refs/heads/signed", "Signed commit\n", artifacts.SSHED25519Private)
|
|
require.Nil(t, err)
|
|
|
|
payload, signature, err := repo.GetObjectSignature(commitID)
|
|
assert.Nil(t, err)
|
|
assert.Contains(t, string(payload), "tree "+emptyTreeHash.String())
|
|
assert.NotContains(t, string(payload), "gpgsig")
|
|
assert.Contains(t, string(signature), "-----BEGIN SSH SIGNATURE-----")
|
|
})
|
|
|
|
t.Run("unsigned commit", func(t *testing.T) {
|
|
t.Parallel()
|
|
commitID, err := repo.Commit(emptyTreeHash, "refs/heads/unsigned", "Unsigned commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
payload, signature, err := repo.GetObjectSignature(commitID)
|
|
assert.Nil(t, err)
|
|
assert.NotEmpty(t, payload)
|
|
assert.Empty(t, signature)
|
|
})
|
|
|
|
t.Run("signed tag", func(t *testing.T) {
|
|
t.Parallel()
|
|
commitID, err := repo.Commit(emptyTreeHash, "refs/heads/tagged", "Commit to tag\n", false)
|
|
require.Nil(t, err)
|
|
tagID, err := repo.TagUsingSpecificKey(commitID, "v1-signed", "Signed tag\n", artifacts.SSHED25519Private)
|
|
require.Nil(t, err)
|
|
|
|
payload, signature, err := repo.GetObjectSignature(tagID)
|
|
assert.Nil(t, err)
|
|
assert.Contains(t, string(payload), "tag v1-signed")
|
|
assert.NotContains(t, string(payload), "SSH SIGNATURE")
|
|
assert.Contains(t, string(signature), "-----BEGIN SSH SIGNATURE-----")
|
|
})
|
|
|
|
t.Run("not a commit or tag", func(t *testing.T) {
|
|
t.Parallel()
|
|
blobID, err := repo.WriteBlob([]byte("test"))
|
|
require.Nil(t, err)
|
|
|
|
_, _, err = repo.GetObjectSignature(blobID)
|
|
assert.ErrorIs(t, err, ErrNotCommitOrTag)
|
|
})
|
|
|
|
t.Run("signed commit, SHA-256 repository", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false, WithObjectFormat(ObjectFormatSHA256))
|
|
|
|
emptyTreeHash, err := NewTreeBuilder(repo).WriteTreeFromEntries(nil)
|
|
require.Nil(t, err)
|
|
|
|
commitID, err := repo.CommitUsingSpecificKey(emptyTreeHash, "refs/heads/signed", "Signed commit\n", artifacts.SSHED25519Private)
|
|
require.Nil(t, err)
|
|
|
|
payload, signature, err := repo.GetObjectSignature(commitID)
|
|
assert.Nil(t, err)
|
|
assert.Contains(t, string(payload), "tree "+emptyTreeHash.String())
|
|
assert.NotContains(t, string(payload), "gpgsig")
|
|
assert.Contains(t, string(signature), "-----BEGIN SSH SIGNATURE-----")
|
|
})
|
|
}
|