mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-04-25 04:57:31 +00:00
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>
This commit is contained in:
parent
eb85681b41
commit
b55c72828e
17 changed files with 687 additions and 306 deletions
|
@ -1163,9 +1163,13 @@ LEVEL = Info
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
|
;; Signing format that Forgejo should use, openpgp uses GPG and ssh uses OpenSSH.
|
||||||
|
;FORMAT = openpgp
|
||||||
|
;;
|
||||||
;; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey
|
;; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey
|
||||||
;; run in the context of the RUN_USER
|
;; run in the context of the RUN_USER
|
||||||
;; Switch to none to stop signing completely
|
;; Switch to none to stop signing completely.
|
||||||
|
;; If `FORMAT` is set to **ssh** this should be set to an absolute path to an public OpenSSH key.
|
||||||
;SIGNING_KEY = default
|
;SIGNING_KEY = default
|
||||||
;;
|
;;
|
||||||
;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer.
|
;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer.
|
||||||
|
|
|
@ -201,7 +201,7 @@ func ParseObjectWithSignature(ctx context.Context, c *GitObject) *ObjectVerifica
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
|
if setting.Repository.Signing.Format == "openpgp" && setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
|
||||||
// OK we should try the default key
|
// OK we should try the default key
|
||||||
gpgSettings := git.GPGSettings{
|
gpgSettings := git.GPGSettings{
|
||||||
Sign: true,
|
Sign: true,
|
||||||
|
|
|
@ -12,8 +12,10 @@ import (
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
|
||||||
"github.com/42wim/sshsig"
|
"github.com/42wim/sshsig"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseObjectWithSSHSignature check if signature is good against keystore.
|
// ParseObjectWithSSHSignature check if signature is good against keystore.
|
||||||
|
@ -62,6 +64,22 @@ func ParseObjectWithSSHSignature(ctx context.Context, c *GitObject, committer *u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the SSH instance key is set, try to verify it with that key.
|
||||||
|
if setting.SSHInstanceKey != nil {
|
||||||
|
instanceSSHKey := &PublicKey{
|
||||||
|
Content: string(ssh.MarshalAuthorizedKey(setting.SSHInstanceKey)),
|
||||||
|
Fingerprint: ssh.FingerprintSHA256(setting.SSHInstanceKey),
|
||||||
|
}
|
||||||
|
instanceUser := &user_model.User{
|
||||||
|
Name: setting.Repository.Signing.SigningName,
|
||||||
|
Email: setting.Repository.Signing.SigningEmail,
|
||||||
|
}
|
||||||
|
commitVerification := verifySSHObjectVerification(c.Signature.Signature, c.Signature.Payload, instanceSSHKey, committer, instanceUser, setting.Repository.Signing.SigningEmail)
|
||||||
|
if commitVerification != nil {
|
||||||
|
return commitVerification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &ObjectVerification{
|
return &ObjectVerification{
|
||||||
CommittingUser: committer,
|
CommittingUser: committer,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package asymkey
|
package asymkey
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
|
@ -15,6 +16,7 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseCommitWithSSHSignature(t *testing.T) {
|
func TestParseCommitWithSSHSignature(t *testing.T) {
|
||||||
|
@ -150,4 +152,43 @@ muPLbvEduU+Ze/1Ol1pgk=
|
||||||
assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
|
assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
|
||||||
assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
|
assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Instance key", func(t *testing.T) {
|
||||||
|
pubKeyContent, err := os.ReadFile("../../tests/integration/ssh-signing-key.pub")
|
||||||
|
require.NoError(t, err)
|
||||||
|
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyContent)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "UwU")()
|
||||||
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "fox@example.com")()
|
||||||
|
defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)()
|
||||||
|
|
||||||
|
gitCommit := &git.Commit{
|
||||||
|
Committer: &git.Signature{
|
||||||
|
Email: "fox@example.com",
|
||||||
|
},
|
||||||
|
Signature: &git.ObjectSignature{
|
||||||
|
Payload: `tree f96f1a4f1a51dc42e2983592f503980b60b8849c
|
||||||
|
parent 93f84db542dd8c6e952c8130bc2fcbe2e299b8b4
|
||||||
|
author OwO <instance@example.com> 1738961379 +0100
|
||||||
|
committer UwU <fox@example.com> 1738961379 +0100
|
||||||
|
|
||||||
|
Fox
|
||||||
|
`,
|
||||||
|
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||||
|
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgV5ELwZ8XJe2LLR/UTuEu/vsFdb
|
||||||
|
t7ry0W8hyzz/b1iocAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||||
|
AAAAQCnyMRkWVVNoZxZkvi/ZoknUhs4LNBmEwZs9e9214WIt+mhKfc6BiHoE2qeluR2McD
|
||||||
|
Y5RzHnA8Ke9wXddEePCQE=
|
||||||
|
-----END SSH SIGNATURE-----
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := commitToGitObject(gitCommit)
|
||||||
|
commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2)
|
||||||
|
assert.True(t, commitVerification.Verified)
|
||||||
|
assert.Equal(t, "UwU / SHA256:QttK41r/zMUeAW71b5UgVSb8xGFF/DlZJ6TyADW+uoI", commitVerification.Reason)
|
||||||
|
assert.Equal(t, "SHA256:QttK41r/zMUeAW71b5UgVSb8xGFF/DlZJ6TyADW+uoI", commitVerification.SigningSSHKey.Fingerprint)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,6 +278,49 @@ func syncGitConfig() (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch setting.Repository.Signing.Format {
|
||||||
|
case "ssh":
|
||||||
|
// First do a git version check.
|
||||||
|
if CheckGitVersionAtLeast("2.34.0") != nil {
|
||||||
|
return errors.New("ssh signing requires Git >= 2.34.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the ssh-keygen binary that Git will use.
|
||||||
|
// This can be overriden in app.ini in [git.config] section, so we must
|
||||||
|
// query this information.
|
||||||
|
sshKeygenPath, err := configGet("gpg.ssh.program")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// git is very stubborn and does not give a default value, so we must do
|
||||||
|
// this ourselves.
|
||||||
|
if len(sshKeygenPath) == 0 {
|
||||||
|
// Default value of git, very unlikely to change.
|
||||||
|
// https://github.com/git/git/blob/5b97a56fa0e7d580dc8865b73107407c9b3f0eff/gpg-interface.c#L116
|
||||||
|
sshKeygenPath = "ssh-keygen"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Although there's a version requirement of 8.2p1, there's no cross-version
|
||||||
|
// method to get the version of ssh-keygen. Therefore we do a simple binary
|
||||||
|
// presence check and hope for the best.
|
||||||
|
if _, err := exec.LookPath(sshKeygenPath); err != nil {
|
||||||
|
if errors.Is(err, exec.ErrNotFound) {
|
||||||
|
return errors.New("git signing requires a ssh-keygen binary")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := configSet("gpg.format", "ssh"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// openpgp is already the default value, so in the case of a non SSH format
|
||||||
|
// set the value to openpgp.
|
||||||
|
default:
|
||||||
|
if err := configSet("gpg.format", "openpgp"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// By default partial clones are disabled, enable them from git v2.22
|
// By default partial clones are disabled, enable them from git v2.22
|
||||||
if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil {
|
if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil {
|
||||||
if err = configSet("uploadpack.allowfilter", "true"); err != nil {
|
if err = configSet("uploadpack.allowfilter", "true"); err != nil {
|
||||||
|
@ -324,6 +367,15 @@ func CheckGitVersionEqual(equal string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configGet(key string) (string, error) {
|
||||||
|
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||||
|
if err != nil && !IsErrorExitCode(err, 1) {
|
||||||
|
return "", fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(stdout), nil
|
||||||
|
}
|
||||||
|
|
||||||
func configSet(key, value string) error {
|
func configSet(key, value string) error {
|
||||||
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||||
if err != nil && !IsErrorExitCode(err, 1) {
|
if err != nil && !IsErrorExitCode(err, 1) {
|
||||||
|
|
|
@ -11,8 +11,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
"forgejo.org/modules/util"
|
"forgejo.org/modules/util"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-version"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -94,3 +96,57 @@ func TestSyncConfig(t *testing.T) {
|
||||||
assert.True(t, gitConfigContains("[sync-test]"))
|
assert.True(t, gitConfigContains("[sync-test]"))
|
||||||
assert.True(t, gitConfigContains("cfg-key-a = CfgValA"))
|
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"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -4,12 +4,15 @@
|
||||||
package setting
|
package setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// enumerates all the policy repository creating
|
// enumerates all the policy repository creating
|
||||||
|
@ -26,6 +29,8 @@ var MaxUserCardsPerPage = 36
|
||||||
// MaxForksPerPage sets maximum amount of forks shown per page
|
// MaxForksPerPage sets maximum amount of forks shown per page
|
||||||
var MaxForksPerPage = 40
|
var MaxForksPerPage = 40
|
||||||
|
|
||||||
|
var SSHInstanceKey ssh.PublicKey
|
||||||
|
|
||||||
// Repository settings
|
// Repository settings
|
||||||
var (
|
var (
|
||||||
Repository = struct {
|
Repository = struct {
|
||||||
|
@ -109,6 +114,7 @@ var (
|
||||||
SigningKey string
|
SigningKey string
|
||||||
SigningName string
|
SigningName string
|
||||||
SigningEmail string
|
SigningEmail string
|
||||||
|
Format string
|
||||||
InitialCommit []string
|
InitialCommit []string
|
||||||
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
||||||
Merges []string
|
Merges []string
|
||||||
|
@ -262,6 +268,7 @@ var (
|
||||||
SigningKey string
|
SigningKey string
|
||||||
SigningName string
|
SigningName string
|
||||||
SigningEmail string
|
SigningEmail string
|
||||||
|
Format string
|
||||||
InitialCommit []string
|
InitialCommit []string
|
||||||
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
||||||
Merges []string
|
Merges []string
|
||||||
|
@ -271,6 +278,7 @@ var (
|
||||||
SigningKey: "default",
|
SigningKey: "default",
|
||||||
SigningName: "",
|
SigningName: "",
|
||||||
SigningEmail: "",
|
SigningEmail: "",
|
||||||
|
Format: "openpgp",
|
||||||
InitialCommit: []string{"always"},
|
InitialCommit: []string{"always"},
|
||||||
CRUDActions: []string{"pubkey", "twofa", "parentsigned"},
|
CRUDActions: []string{"pubkey", "twofa", "parentsigned"},
|
||||||
Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
|
Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
|
||||||
|
@ -376,4 +384,15 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
||||||
log.Fatal("loadRepoArchiveFrom: %v", err)
|
log.Fatal("loadRepoArchiveFrom: %v", err)
|
||||||
}
|
}
|
||||||
Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool()
|
Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool()
|
||||||
|
|
||||||
|
if Repository.Signing.Format == "ssh" && Repository.Signing.SigningKey != "none" && Repository.Signing.SigningKey != "" {
|
||||||
|
sshPublicKey, err := os.ReadFile(Repository.Signing.SigningKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Could not read repository signing key in %q: %v", Repository.Signing.SigningKey, err)
|
||||||
|
}
|
||||||
|
SSHInstanceKey, _, _, _, err = ssh.ParseAuthorizedKey(sshPublicKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Could not parse the SSH signing key %q: %v", sshPublicKey, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
59
modules/setting/repository_test.go
Normal file
59
modules/setting/repository_test.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSSHInstanceKey(t *testing.T) {
|
||||||
|
sshSigningKeyPath, err := filepath.Abs("../../tests/integration/ssh-signing-key.pub")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("None value", func(t *testing.T) {
|
||||||
|
cfg, err := NewConfigProviderFromData(`
|
||||||
|
[repository.signing]
|
||||||
|
FORMAT = ssh
|
||||||
|
SIGNING_KEY = none
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
loadRepositoryFrom(cfg)
|
||||||
|
|
||||||
|
assert.Nil(t, SSHInstanceKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("No value", func(t *testing.T) {
|
||||||
|
cfg, err := NewConfigProviderFromData(`
|
||||||
|
[repository.signing]
|
||||||
|
FORMAT = ssh
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
loadRepositoryFrom(cfg)
|
||||||
|
|
||||||
|
assert.Nil(t, SSHInstanceKey)
|
||||||
|
})
|
||||||
|
t.Run("Normal", func(t *testing.T) {
|
||||||
|
iniStr := fmt.Sprintf(`
|
||||||
|
[repository.signing]
|
||||||
|
FORMAT = ssh
|
||||||
|
SIGNING_KEY = %s
|
||||||
|
`, sshSigningKeyPath)
|
||||||
|
cfg, err := NewConfigProviderFromData(iniStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
loadRepositoryFrom(cfg)
|
||||||
|
|
||||||
|
assert.NotNil(t, SSHInstanceKey)
|
||||||
|
assert.Equal(t, "ssh-ed25519", SSHInstanceKey.Type())
|
||||||
|
assert.EqualValues(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH\n", ssh.MarshalAuthorizedKey(SSHInstanceKey))
|
||||||
|
})
|
||||||
|
}
|
|
@ -865,6 +865,7 @@ func Routes() *web.Route {
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Get("/version", misc.Version)
|
m.Get("/version", misc.Version)
|
||||||
m.Get("/signing-key.gpg", misc.SigningKey)
|
m.Get("/signing-key.gpg", misc.SigningKey)
|
||||||
|
m.Get("/signing-key.ssh", misc.SSHSigningKey)
|
||||||
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
|
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
|
||||||
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
|
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
|
||||||
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
|
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
|
||||||
|
|
|
@ -7,8 +7,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
asymkey_service "forgejo.org/services/asymkey"
|
asymkey_service "forgejo.org/services/asymkey"
|
||||||
"forgejo.org/services/context"
|
"forgejo.org/services/context"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SigningKey returns the public key of the default signing key if it exists
|
// SigningKey returns the public key of the default signing key if it exists
|
||||||
|
@ -61,3 +64,29 @@ func SigningKey(ctx *context.APIContext) {
|
||||||
ctx.Error(http.StatusInternalServerError, "gpg export", fmt.Errorf("Error writing key content %w", err))
|
ctx.Error(http.StatusInternalServerError, "gpg export", fmt.Errorf("Error writing key content %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSHSigningKey returns the public SSH key of the default signing key if it exists
|
||||||
|
func SSHSigningKey(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /signing-key.ssh miscellaneous getSSHSigningKey
|
||||||
|
// ---
|
||||||
|
// summary: Get default signing-key.ssh
|
||||||
|
// produces:
|
||||||
|
// - text/plain
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// description: "SSH public key in OpenSSH authorized key format"
|
||||||
|
// schema:
|
||||||
|
// type: string
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
if setting.SSHInstanceKey == nil {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ctx.Write(ssh.MarshalAuthorizedKey(setting.SSHInstanceKey))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "ssh export", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -90,6 +90,13 @@ func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if setting.Repository.Signing.Format == "ssh" {
|
||||||
|
return setting.Repository.Signing.SigningKey, &git.Signature{
|
||||||
|
Name: setting.Repository.Signing.SigningName,
|
||||||
|
Email: setting.Repository.Signing.SigningEmail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
|
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
|
||||||
// Can ignore the error here as it means that commit.gpgsign is not set
|
// Can ignore the error here as it means that commit.gpgsign is not set
|
||||||
value, _, _ := git.NewCommand(ctx, "config", "--get", "commit.gpgsign").RunStdString(&git.RunOpts{Dir: repoPath})
|
value, _, _ := git.NewCommand(ctx, "config", "--get", "commit.gpgsign").RunStdString(&git.RunOpts{Dir: repoPath})
|
||||||
|
|
23
templates/swagger/v1_json.tmpl
generated
23
templates/swagger/v1_json.tmpl
generated
|
@ -17315,6 +17315,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/signing-key.ssh": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"text/plain"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"miscellaneous"
|
||||||
|
],
|
||||||
|
"summary": "Get default signing-key.ssh",
|
||||||
|
"operationId": "getSSHSigningKey",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "SSH public key in OpenSSH authorized key format",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/teams/{id}": {
|
"/teams/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|
38
tests/integration/api_misc_test.go
Normal file
38
tests/integration/api_misc_test.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
|
"forgejo.org/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPISSHSigningKey(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
t.Run("No signing key", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.SSHInstanceKey, nil)()
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", "/api/v1/signing-key.ssh"), http.StatusNotFound)
|
||||||
|
})
|
||||||
|
t.Run("With signing key", func(t *testing.T) {
|
||||||
|
publicKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH\n"
|
||||||
|
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKey))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)()
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/signing-key.ssh"), http.StatusOK)
|
||||||
|
assert.Equal(t, publicKey, resp.Body.String())
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,304 +0,0 @@
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
auth_model "forgejo.org/models/auth"
|
|
||||||
"forgejo.org/models/unittest"
|
|
||||||
user_model "forgejo.org/models/user"
|
|
||||||
"forgejo.org/modules/git"
|
|
||||||
"forgejo.org/modules/process"
|
|
||||||
"forgejo.org/modules/setting"
|
|
||||||
api "forgejo.org/modules/structs"
|
|
||||||
"forgejo.org/modules/test"
|
|
||||||
"forgejo.org/tests"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGPGGit(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring
|
|
||||||
err := os.Chmod(tmpDir, 0o700)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Setenv("GNUPGHOME", tmpDir)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Need to create a root key
|
|
||||||
rootKeyPair, err := importTestingKey()
|
|
||||||
require.NoError(t, err, "importTestingKey")
|
|
||||||
|
|
||||||
defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, rootKeyPair.PrimaryKey.KeyIdShortString())()
|
|
||||||
defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")()
|
|
||||||
defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")()
|
|
||||||
defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})()
|
|
||||||
defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})()
|
|
||||||
|
|
||||||
username := "user2"
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
|
||||||
baseAPITestContext := NewAPITestContext(t, username, "repo1")
|
|
||||||
|
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
|
||||||
u.Path = baseAPITestContext.GitPath()
|
|
||||||
|
|
||||||
t.Run("Unsigned-Initial", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat
|
|
||||||
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
||||||
assert.NotNil(t, branch.Commit)
|
|
||||||
assert.NotNil(t, branch.Commit.Verification)
|
|
||||||
assert.False(t, branch.Commit.Verification.Verified)
|
|
||||||
assert.Empty(t, branch.Commit.Verification.Signature)
|
|
||||||
}))
|
|
||||||
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.False(t, response.Verification.Verified)
|
|
||||||
}))
|
|
||||||
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.False(t, response.Verification.Verified)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
|
|
||||||
t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.False(t, response.Verification.Verified)
|
|
||||||
}))
|
|
||||||
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.False(t, response.Verification.Verified)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.CRUDActions = []string{"never"}
|
|
||||||
t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.False(t, response.Verification.Verified)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.CRUDActions = []string{"always"}
|
|
||||||
t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreateCRUDFile-Always", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.NotNil(t, response.Verification)
|
|
||||||
if response.Verification == nil {
|
|
||||||
assert.FailNow(t, "no verification provided with response", "response: %v", response)
|
|
||||||
}
|
|
||||||
assert.True(t, response.Verification.Verified)
|
|
||||||
if !response.Verification.Verified {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
|
|
||||||
}))
|
|
||||||
t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.NotNil(t, response.Verification)
|
|
||||||
if response.Verification == nil {
|
|
||||||
assert.FailNow(t, "no verification provided with response", "response: %v", response)
|
|
||||||
}
|
|
||||||
assert.True(t, response.Verification.Verified)
|
|
||||||
if !response.Verification.Verified {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
|
|
||||||
t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.NotNil(t, response.Verification)
|
|
||||||
if response.Verification == nil {
|
|
||||||
assert.FailNow(t, "no verification provided with response", "response: %v", response)
|
|
||||||
}
|
|
||||||
assert.True(t, response.Verification.Verified)
|
|
||||||
if !response.Verification.Verified {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.InitialCommit = []string{"always"}
|
|
||||||
t.Run("AlwaysSign-Initial", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat
|
|
||||||
t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
||||||
assert.NotNil(t, branch.Commit)
|
|
||||||
if branch.Commit == nil {
|
|
||||||
assert.FailNow(t, "no commit provided with branch", "branch: %v", branch)
|
|
||||||
}
|
|
||||||
assert.NotNil(t, branch.Commit.Verification)
|
|
||||||
if branch.Commit.Verification == nil {
|
|
||||||
assert.FailNow(t, "no verification provided with branch commit", "commit: %v", branch.Commit)
|
|
||||||
}
|
|
||||||
assert.True(t, branch.Commit.Verification.Verified)
|
|
||||||
if !branch.Commit.Verification.Verified {
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
assert.Equal(t, "gitea@fake.local", branch.Commit.Verification.Signer.Email)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.CRUDActions = []string{"never"}
|
|
||||||
t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-always-never", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat
|
|
||||||
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.False(t, response.Verification.Verified)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
|
|
||||||
t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-always-parent", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat
|
|
||||||
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.True(t, response.Verification.Verified)
|
|
||||||
if !response.Verification.Verified {
|
|
||||||
t.FailNow()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.CRUDActions = []string{"always"}
|
|
||||||
t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-always-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat
|
|
||||||
t.Run("CreateCRUDFile-Always", crudActionCreateFile(
|
|
||||||
t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
|
|
||||||
assert.True(t, response.Verification.Verified)
|
|
||||||
if !response.Verification.Verified {
|
|
||||||
t.FailNow()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.Merges = []string{"commitssigned"}
|
|
||||||
t.Run("UnsignedMerging", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreatePullRequest", func(t *testing.T) {
|
|
||||||
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
|
|
||||||
})
|
|
||||||
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
||||||
assert.NotNil(t, branch.Commit)
|
|
||||||
assert.NotNil(t, branch.Commit.Verification)
|
|
||||||
assert.False(t, branch.Commit.Verification.Verified)
|
|
||||||
assert.Empty(t, branch.Commit.Verification.Signature)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.Merges = []string{"basesigned"}
|
|
||||||
t.Run("BaseSignedMerging", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreatePullRequest", func(t *testing.T) {
|
|
||||||
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
|
|
||||||
})
|
|
||||||
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
||||||
assert.NotNil(t, branch.Commit)
|
|
||||||
assert.NotNil(t, branch.Commit.Verification)
|
|
||||||
assert.False(t, branch.Commit.Verification.Verified)
|
|
||||||
assert.Empty(t, branch.Commit.Verification.Signature)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
setting.Repository.Signing.Merges = []string{"commitssigned"}
|
|
||||||
t.Run("CommitsSignedMerging", func(t *testing.T) {
|
|
||||||
defer tests.PrintCurrentTest(t)()
|
|
||||||
testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
||||||
t.Run("CreatePullRequest", func(t *testing.T) {
|
|
||||||
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
|
|
||||||
})
|
|
||||||
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
|
||||||
assert.NotNil(t, branch.Commit)
|
|
||||||
assert.NotNil(t, branch.Commit.Verification)
|
|
||||||
assert.True(t, branch.Commit.Verification.Verified)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
|
|
||||||
return doAPICreateFile(ctx, path, &api.CreateFileOptions{
|
|
||||||
FileOptions: api.FileOptions{
|
|
||||||
BranchName: from,
|
|
||||||
NewBranchName: to,
|
|
||||||
Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path),
|
|
||||||
Author: api.Identity{
|
|
||||||
Name: user.FullName,
|
|
||||||
Email: user.Email,
|
|
||||||
},
|
|
||||||
Committer: api.Identity{
|
|
||||||
Name: user.FullName,
|
|
||||||
Email: user.Email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("This is new text for %s", path))),
|
|
||||||
}, callback...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func importTestingKey() (*openpgp.Entity, error) {
|
|
||||||
if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keyringFile, err := os.Open("tests/integration/private-testing.key")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer keyringFile.Close()
|
|
||||||
|
|
||||||
block, err := armor.Decode(keyringFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
keyring, err := openpgp.ReadKeyRing(block.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Keyring access failed: '%w'", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// There should only be one entity in this file.
|
|
||||||
return keyring[0], nil
|
|
||||||
}
|
|
330
tests/integration/signing_git_test.go
Normal file
330
tests/integration/signing_git_test.go
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
auth_model "forgejo.org/models/auth"
|
||||||
|
"forgejo.org/models/unittest"
|
||||||
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/git"
|
||||||
|
"forgejo.org/modules/process"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
api "forgejo.org/modules/structs"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
|
"forgejo.org/tests"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInstanceSigning(t *testing.T) {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
// Cannot use t.Context(), it is in the done state.
|
||||||
|
require.NoError(t, git.InitFull(context.Background())) //nolint:usetesting
|
||||||
|
})
|
||||||
|
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "UwU")()
|
||||||
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "fox@example.com")()
|
||||||
|
defer test.MockProtect(&setting.Repository.Signing.InitialCommit)()
|
||||||
|
defer test.MockProtect(&setting.Repository.Signing.CRUDActions)()
|
||||||
|
|
||||||
|
t.Run("SSH", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
pubKeyContent, err := os.ReadFile("tests/integration/ssh-signing-key.pub")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyContent)
|
||||||
|
require.NoError(t, err)
|
||||||
|
signingKeyPath, err := filepath.Abs("tests/integration/ssh-signing-key")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.Chmod(signingKeyPath, 0o600))
|
||||||
|
defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)()
|
||||||
|
defer test.MockVariableValue(&setting.Repository.Signing.Format, "ssh")()
|
||||||
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, signingKeyPath)()
|
||||||
|
|
||||||
|
// Ensure the git config is updated with the new signing format.
|
||||||
|
require.NoError(t, git.InitFull(t.Context()))
|
||||||
|
|
||||||
|
forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
|
||||||
|
u2 := *u
|
||||||
|
testCRUD(t, &u2, "ssh", objectFormat)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PGP", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
// Use a new GNUPGPHOME to avoid messing with the existing GPG keyring.
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
require.NoError(t, os.Chmod(tmpDir, 0o700))
|
||||||
|
t.Setenv("GNUPGHOME", tmpDir)
|
||||||
|
|
||||||
|
rootKeyPair, err := importTestingKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, rootKeyPair.PrimaryKey.KeyIdShortString())()
|
||||||
|
defer test.MockVariableValue(&setting.Repository.Signing.Format, "openpgp")()
|
||||||
|
|
||||||
|
// Ensure the git config is updated with the new signing format.
|
||||||
|
require.NoError(t, git.InitFull(t.Context()))
|
||||||
|
|
||||||
|
forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
|
||||||
|
u2 := *u
|
||||||
|
testCRUD(t, &u2, "pgp", objectFormat)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCRUD(t *testing.T, u *url.URL, signingFormat string, objectFormat git.ObjectFormat) {
|
||||||
|
t.Helper()
|
||||||
|
setting.Repository.Signing.CRUDActions = []string{"never"}
|
||||||
|
setting.Repository.Signing.InitialCommit = []string{"never"}
|
||||||
|
|
||||||
|
username := "user2"
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
||||||
|
baseAPITestContext := NewAPITestContext(t, username, "repo1")
|
||||||
|
u.Path = baseAPITestContext.GitPath()
|
||||||
|
|
||||||
|
suffix := "-" + signingFormat + "-" + objectFormat.Name()
|
||||||
|
|
||||||
|
t.Run("Unsigned-Initial", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat))
|
||||||
|
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
||||||
|
assert.NotNil(t, branch.Commit)
|
||||||
|
assert.NotNil(t, branch.Commit.Verification)
|
||||||
|
assert.False(t, branch.Commit.Verification.Verified)
|
||||||
|
assert.Empty(t, branch.Commit.Verification.Signature)
|
||||||
|
}))
|
||||||
|
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
assert.False(t, response.Verification.Verified)
|
||||||
|
}))
|
||||||
|
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
assert.False(t, response.Verification.Verified)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
assert.False(t, response.Verification.Verified)
|
||||||
|
}))
|
||||||
|
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
assert.False(t, response.Verification.Verified)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.InitialCommit = []string{"never"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
assert.False(t, response.Verification.Verified)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.CRUDActions = []string{"always"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreateCRUDFile-Always", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
require.NotNil(t, response.Verification)
|
||||||
|
assert.True(t, response.Verification.Verified)
|
||||||
|
assert.Equal(t, "fox@example.com", response.Verification.Signer.Email)
|
||||||
|
}))
|
||||||
|
t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
require.NotNil(t, response.Verification)
|
||||||
|
assert.True(t, response.Verification.Verified)
|
||||||
|
assert.Equal(t, "fox@example.com", response.Verification.Signer.Email)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
require.NotNil(t, response.Verification)
|
||||||
|
assert.True(t, response.Verification.Verified)
|
||||||
|
assert.Equal(t, "fox@example.com", response.Verification.Signer.Email)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AlwaysSign-Initial", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.InitialCommit = []string{"always"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-always"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat))
|
||||||
|
t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
||||||
|
require.NotNil(t, branch.Commit)
|
||||||
|
require.NotNil(t, branch.Commit.Verification)
|
||||||
|
assert.True(t, branch.Commit.Verification.Verified)
|
||||||
|
assert.Equal(t, "fox@example.com", branch.Commit.Verification.Signer.Email)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.CRUDActions = []string{"never"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-always-never"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat))
|
||||||
|
t.Run("CreateCRUDFile-Never", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
assert.False(t, response.Verification.Verified)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-always-parent"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat))
|
||||||
|
t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
assert.True(t, response.Verification.Verified)
|
||||||
|
assert.Equal(t, "fox@example.com", response.Verification.Signer.Email)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.CRUDActions = []string{"always"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-always-always"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat))
|
||||||
|
t.Run("CreateCRUDFile-Always", crudActionCreateFile(
|
||||||
|
t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
|
||||||
|
assert.True(t, response.Verification.Verified)
|
||||||
|
assert.Equal(t, "fox@example.com", response.Verification.Signer.Email)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UnsignedMerging", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.Merges = []string{"commitssigned"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreatePullRequest", func(t *testing.T) {
|
||||||
|
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
|
||||||
|
})
|
||||||
|
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
||||||
|
require.NotNil(t, branch.Commit)
|
||||||
|
require.NotNil(t, branch.Commit.Verification)
|
||||||
|
assert.False(t, branch.Commit.Verification.Verified)
|
||||||
|
assert.Empty(t, branch.Commit.Verification.Signature)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("BaseSignedMerging", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.Merges = []string{"basesigned"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreatePullRequest", func(t *testing.T) {
|
||||||
|
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
|
||||||
|
})
|
||||||
|
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
||||||
|
require.NotNil(t, branch.Commit)
|
||||||
|
require.NotNil(t, branch.Commit.Verification)
|
||||||
|
assert.False(t, branch.Commit.Verification.Verified)
|
||||||
|
assert.Empty(t, branch.Commit.Verification.Signature)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CommitsSignedMerging", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
setting.Repository.Signing.Merges = []string{"commitssigned"}
|
||||||
|
|
||||||
|
testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
|
t.Run("CreatePullRequest", func(t *testing.T) {
|
||||||
|
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
|
||||||
|
})
|
||||||
|
t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
|
||||||
|
require.NotNil(t, branch.Commit)
|
||||||
|
require.NotNil(t, branch.Commit.Verification)
|
||||||
|
assert.True(t, branch.Commit.Verification.Verified)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
|
||||||
|
return doAPICreateFile(ctx, path, &api.CreateFileOptions{
|
||||||
|
FileOptions: api.FileOptions{
|
||||||
|
BranchName: from,
|
||||||
|
NewBranchName: to,
|
||||||
|
Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path),
|
||||||
|
Author: api.Identity{
|
||||||
|
Name: user.FullName,
|
||||||
|
Email: user.Email,
|
||||||
|
},
|
||||||
|
Committer: api.Identity{
|
||||||
|
Name: user.FullName,
|
||||||
|
Email: user.Email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ContentBase64: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("This is new text for %s", path))),
|
||||||
|
}, callback...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func importTestingKey() (*openpgp.Entity, error) {
|
||||||
|
if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keyringFile, err := os.Open("tests/integration/private-testing.key")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer keyringFile.Close()
|
||||||
|
|
||||||
|
block, err := armor.Decode(keyringFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyring, err := openpgp.ReadKeyRing(block.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Keyring access failed: '%w'", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// There should only be one entity in this file.
|
||||||
|
return keyring[0], nil
|
||||||
|
}
|
7
tests/integration/ssh-signing-key
Normal file
7
tests/integration/ssh-signing-key
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACBXkQvBnxcl7YstH9RO4S7++wV1u3uvLRbyHLPP9vWKhwAAAJhlmhmkZZoZ
|
||||||
|
pAAAAAtzc2gtZWQyNTUxOQAAACBXkQvBnxcl7YstH9RO4S7++wV1u3uvLRbyHLPP9vWKhw
|
||||||
|
AAAEDnOTuE2rDECN+2OsuUbQgGrMSY22tn+IF5JG5nuyJinVeRC8GfFyXtiy0f1E7hLv77
|
||||||
|
BXW7e68tFvIcs8/29YqHAAAAE2d1c3RlZEBndXN0ZWQtYmVhc3QBAg==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
1
tests/integration/ssh-signing-key.pub
Normal file
1
tests/integration/ssh-signing-key.pub
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH
|
Loading…
Add table
Reference in a new issue