package commit_test
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"lindenii.org/go/furgit/internal/testgit"
"lindenii.org/go/furgit/object/id"
"lindenii.org/go/furgit/object/signed/commit"
"lindenii.org/go/furgit/object/typ"
)
const signerPrincipal = "signer@example.org"
func setupSSHSignedCommit(
t *testing.T,
objectFormat id.ObjectFormat,
) (payload []byte, allowedSignersPath string, signaturePath string) {
t.Helper()
repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat})
if err != nil {
t.Fatalf("NewRepo: %v", err)
}
signDir := t.TempDir()
signRoot, err := os.OpenRoot(signDir)
if err != nil {
t.Fatalf("os.OpenRoot(%q): %v", signDir, err)
}
t.Cleanup(func() { _ = signRoot.Close() })
privateKeyPath := filepath.Join(signDir, "signing_key")
allowedSignersPath = filepath.Join(signDir, "allowed_signers")
signaturePath = filepath.Join(signDir, "commit.sig")
cmd := exec.CommandContext(
t.Context(),
"ssh-keygen",
"-q",
"-t", "ed25519",
"-N", "",
"-C", signerPrincipal,
"-f", privateKeyPath,
) //#nosec G204
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("ssh-keygen: %v\n%s", err, out)
}
publicKey, err := signRoot.ReadFile("signing_key.pub")
if err != nil {
t.Fatalf("ReadFile(signing_key.pub): %v", err)
}
err = signRoot.WriteFile(
"allowed_signers",
append([]byte(signerPrincipal+" "), publicKey...),
0o600,
)
if err != nil {
t.Fatalf("WriteFile(allowed_signers): %v", err)
}
err = repo.ConfigSet(t, "gpg.format", "ssh")
if err != nil {
t.Fatalf("ConfigSet(gpg.format): %v", err)
}
err = repo.ConfigSet(t, "user.signingkey", privateKeyPath)
if err != nil {
t.Fatalf("ConfigSet(user.signingkey): %v", err)
}
blobID, err := repo.HashObject(t, typ.Blob, strings.NewReader("signed\n"))
if err != nil {
t.Fatalf("HashObject(blob): %v", err)
}
treeID, err := repo.MkTree(t, []testgit.TreeEntry{
{Mode: "100644", Type: typ.Blob, OID: blobID, Name: "file.txt"},
})
if err != nil {
t.Fatalf("MkTree: %v", err)
}
commitID, err := repo.CommitTree(t, treeID, testgit.CommitTreeOptions{
Message: "signed commit",
Sign: true,
})
if err != nil {
t.Fatalf("CommitTree: %v", err)
}
body, err := repo.CatFile(t, typ.Commit, commitID)
if err != nil {
t.Fatalf("CatFile: %v", err)
}
parsed, err := commit.Parse(body)
if err != nil {
t.Fatalf("Parse: %v", err)
}
signature, ok := parsed.AppendSignature(nil, objectFormat)
if !ok {
t.Fatalf("missing %s signature", objectFormat)
}
err = signRoot.WriteFile("commit.sig", signature, 0o600)
if err != nil {
t.Fatalf("WriteFile(commit.sig): %v", err)
}
return parsed.AppendPayload(nil), allowedSignersPath, signaturePath
}
func sshVerify(
t *testing.T,
payload []byte,
allowedSignersPath string,
signaturePath string,
) ([]byte, error) {
t.Helper()
cmd := exec.CommandContext(
t.Context(),
"ssh-keygen",
"-Y", "verify",
"-n", "git",
"-f", allowedSignersPath,
"-I", signerPrincipal,
"-s", signaturePath,
) //#nosec G204
cmd.Stdin = bytes.NewReader(payload)
return cmd.CombinedOutput() //nolint:wrapcheck
}
func TestSSHSignedCommitVerifies(t *testing.T) {
t.Parallel()
for _, objectFormat := range id.SupportedObjectFormats() {
t.Run(objectFormat.String(), func(t *testing.T) {
t.Parallel()
payload, allowedSignersPath, signaturePath := setupSSHSignedCommit(t, objectFormat)
out, err := sshVerify(t, payload, allowedSignersPath, signaturePath)
if err != nil {
t.Fatalf("ssh-keygen verify failed: %v\n%s", err, out)
}
})
}
}
func TestSSHSignedCommitRejectsTamperedPayload(t *testing.T) {
t.Parallel()
for _, objectFormat := range id.SupportedObjectFormats() {
t.Run(objectFormat.String(), func(t *testing.T) {
t.Parallel()
payload, allowedSignersPath, signaturePath := setupSSHSignedCommit(t, objectFormat)
payload = append([]byte(nil), payload...)
payload[len(payload)-2] ^= 1
out, err := sshVerify(t, payload, allowedSignersPath, signaturePath)
if err == nil {
t.Fatalf("ssh-keygen verify unexpectedly succeeded:\n%s", out)
}
})
}
}