forgejo/modules/git/git_test.go
Gusted b55c72828e feat(sec): Add SSH signing support for instances (#6897)
- Add support to set `gpg.format` in the Git config, via the new `[repository.signing].FORMAT` option. This is to tell Git that the instance would like to use SSH instead of OpenPGP to sign its commits. This is guarded behind a Git version check for v2.34.0 and a check that a `ssh-keygen` binary is present.
- Add support to recognize the public SSH key that is given to `[repository.signing].SIGNING_KEY` as the signing key by the instance.
- Thus this allows the instance to use SSH commit signing for commits that the instance creates (e.g. initial and squash commits) instead of using PGP.
- Technically (although I have no clue how as this is not documented) you can have a different PGP signing key for different repositories; this is not implemented for SSH signing.
- Add unit and integration testing.
  - `TestInstanceSigning` was reworked from `TestGPGGit`, now also includes testing for SHA256 repositories. Is the main integration test that actually signs commits and checks that they are marked as verified by Forgejo.
  - `TestParseCommitWithSSHSignature` is a unit test that makes sure that if a SSH instnace signing key is set, that it is used to possibly verify instance SSH signed commits.
  - `TestSyncConfigGPGFormat` is a unit test that makes sure the correct git config is set according to the signing format setting. Also checks that the guarded git version check and ssh-keygen binary presence check is done correctly.
  - `TestSSHInstanceKey` is a unit test that makes sure the parsing of a SSH signing key is done correctly.
  - `TestAPISSHSigningKey` is a integration test that makes sure the newly added API route `/api/v1/signing-key.ssh` responds correctly.

Documentation PR: forgejo/docs#1122

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6897
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
2025-04-11 13:25:35 +00:00

152 lines
4.6 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"fmt"
"os"
"strings"
"testing"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"forgejo.org/modules/util"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testRun(m *testing.M) error {
gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home")
if err != nil {
return fmt.Errorf("unable to create temp dir: %w", err)
}
defer util.RemoveAll(gitHomePath)
setting.Git.HomePath = gitHomePath
if err = InitFull(context.Background()); err != nil {
return fmt.Errorf("failed to call Init: %w", err)
}
exitCode := m.Run()
if exitCode != 0 {
return fmt.Errorf("run test failed, ExitCode=%d", exitCode)
}
return nil
}
func TestMain(m *testing.M) {
if err := testRun(m); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err)
os.Exit(1)
}
}
func gitConfigContains(sub string) bool {
if b, err := os.ReadFile(HomeDir() + "/.gitconfig"); err == nil {
return strings.Contains(string(b), sub)
}
return false
}
func TestGitConfig(t *testing.T) {
assert.False(t, gitConfigContains("key-a"))
require.NoError(t, configSetNonExist("test.key-a", "val-a"))
assert.True(t, gitConfigContains("key-a = val-a"))
require.NoError(t, configSetNonExist("test.key-a", "val-a-changed"))
assert.False(t, gitConfigContains("key-a = val-a-changed"))
require.NoError(t, configSet("test.key-a", "val-a-changed"))
assert.True(t, gitConfigContains("key-a = val-a-changed"))
require.NoError(t, configAddNonExist("test.key-b", "val-b"))
assert.True(t, gitConfigContains("key-b = val-b"))
require.NoError(t, configAddNonExist("test.key-b", "val-2b"))
assert.True(t, gitConfigContains("key-b = val-b"))
assert.True(t, gitConfigContains("key-b = val-2b"))
require.NoError(t, configUnsetAll("test.key-b", "val-b"))
assert.False(t, gitConfigContains("key-b = val-b"))
assert.True(t, gitConfigContains("key-b = val-2b"))
require.NoError(t, configUnsetAll("test.key-b", "val-2b"))
assert.False(t, gitConfigContains("key-b = val-2b"))
require.NoError(t, configSet("test.key-x", "*"))
assert.True(t, gitConfigContains("key-x = *"))
require.NoError(t, configSetNonExist("test.key-x", "*"))
require.NoError(t, configUnsetAll("test.key-x", "*"))
assert.False(t, gitConfigContains("key-x = *"))
}
func TestSyncConfig(t *testing.T) {
oldGitConfig := setting.GitConfig
defer func() {
setting.GitConfig = oldGitConfig
}()
setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA"
require.NoError(t, syncGitConfig())
assert.True(t, gitConfigContains("[sync-test]"))
assert.True(t, gitConfigContains("cfg-key-a = CfgValA"))
}
func TestSyncConfigGPGFormat(t *testing.T) {
defer test.MockProtect(&setting.GitConfig)()
t.Run("No format", func(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.Signing.Format, "")()
require.NoError(t, syncGitConfig())
assert.True(t, gitConfigContains("[gpg]"))
assert.True(t, gitConfigContains("format = openpgp"))
})
t.Run("SSH format", func(t *testing.T) {
r, err := os.OpenRoot(t.TempDir())
require.NoError(t, err)
f, err := r.OpenFile("ssh-keygen", os.O_CREATE|os.O_TRUNC, 0o700)
require.NoError(t, f.Close())
require.NoError(t, err)
t.Setenv("PATH", r.Name())
defer test.MockVariableValue(&setting.Repository.Signing.Format, "ssh")()
require.NoError(t, syncGitConfig())
assert.True(t, gitConfigContains("[gpg]"))
assert.True(t, gitConfigContains("format = ssh"))
t.Run("Old version", func(t *testing.T) {
oldVersion, err := version.NewVersion("2.33.0")
require.NoError(t, err)
defer test.MockVariableValue(&gitVersion, oldVersion)()
require.ErrorContains(t, syncGitConfig(), "ssh signing requires Git >= 2.34.0")
})
t.Run("No ssh-keygen binary", func(t *testing.T) {
require.NoError(t, r.Remove("ssh-keygen"))
require.ErrorContains(t, syncGitConfig(), "git signing requires a ssh-keygen binary")
})
t.Run("Dynamic ssh-keygen binary location", func(t *testing.T) {
f, err := r.OpenFile("ssh-keygen-2", os.O_CREATE|os.O_TRUNC, 0o700)
require.NoError(t, f.Close())
require.NoError(t, err)
defer test.MockVariableValue(&setting.GitConfig.Options, map[string]string{
"gpg.ssh.program": "ssh-keygen-2",
})()
require.NoError(t, syncGitConfig())
})
})
t.Run("OpenPGP format", func(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.Signing.Format, "openpgp")()
require.NoError(t, syncGitConfig())
assert.True(t, gitConfigContains("[gpg]"))
assert.True(t, gitConfigContains("format = openpgp"))
})
}