loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Gusted
Gusted
and committed by
Gusted
b55c7282 eb85681b

+687 -306
+5 -1
custom/conf/app.example.ini
··· 1163 1163 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 1164 1164 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 1165 1165 ;; 1166 + ;; Signing format that Forgejo should use, openpgp uses GPG and ssh uses OpenSSH. 1167 + ;FORMAT = openpgp 1168 + ;; 1166 1169 ;; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey 1167 1170 ;; run in the context of the RUN_USER 1168 - ;; Switch to none to stop signing completely 1171 + ;; Switch to none to stop signing completely. 1172 + ;; If `FORMAT` is set to **ssh** this should be set to an absolute path to an public OpenSSH key. 1169 1173 ;SIGNING_KEY = default 1170 1174 ;; 1171 1175 ;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer.
+1 -1
models/asymkey/gpg_key_object_verification.go
··· 201 201 } 202 202 } 203 203 204 - if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { 204 + if setting.Repository.Signing.Format == "openpgp" && setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { 205 205 // OK we should try the default key 206 206 gpgSettings := git.GPGSettings{ 207 207 Sign: true,
+18
models/asymkey/ssh_key_object_verification.go
··· 12 12 "forgejo.org/models/db" 13 13 user_model "forgejo.org/models/user" 14 14 "forgejo.org/modules/log" 15 + "forgejo.org/modules/setting" 15 16 16 17 "github.com/42wim/sshsig" 18 + "golang.org/x/crypto/ssh" 17 19 ) 18 20 19 21 // ParseObjectWithSSHSignature check if signature is good against keystore. ··· 59 61 return commitVerification 60 62 } 61 63 } 64 + } 65 + } 66 + 67 + // If the SSH instance key is set, try to verify it with that key. 68 + if setting.SSHInstanceKey != nil { 69 + instanceSSHKey := &PublicKey{ 70 + Content: string(ssh.MarshalAuthorizedKey(setting.SSHInstanceKey)), 71 + Fingerprint: ssh.FingerprintSHA256(setting.SSHInstanceKey), 72 + } 73 + instanceUser := &user_model.User{ 74 + Name: setting.Repository.Signing.SigningName, 75 + Email: setting.Repository.Signing.SigningEmail, 76 + } 77 + commitVerification := verifySSHObjectVerification(c.Signature.Signature, c.Signature.Payload, instanceSSHKey, committer, instanceUser, setting.Repository.Signing.SigningEmail) 78 + if commitVerification != nil { 79 + return commitVerification 62 80 } 63 81 } 64 82
+41
models/asymkey/ssh_key_object_verification_test.go
··· 4 4 package asymkey 5 5 6 6 import ( 7 + "os" 7 8 "testing" 8 9 9 10 "forgejo.org/models/db" ··· 15 16 16 17 "github.com/stretchr/testify/assert" 17 18 "github.com/stretchr/testify/require" 19 + "golang.org/x/crypto/ssh" 18 20 ) 19 21 20 22 func TestParseCommitWithSSHSignature(t *testing.T) { ··· 149 151 assert.True(t, commitVerification.Verified) 150 152 assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason) 151 153 assert.Equal(t, sshKey, commitVerification.SigningSSHKey) 154 + }) 155 + 156 + t.Run("Instance key", func(t *testing.T) { 157 + pubKeyContent, err := os.ReadFile("../../tests/integration/ssh-signing-key.pub") 158 + require.NoError(t, err) 159 + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyContent) 160 + require.NoError(t, err) 161 + 162 + defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "UwU")() 163 + defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "fox@example.com")() 164 + defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)() 165 + 166 + gitCommit := &git.Commit{ 167 + Committer: &git.Signature{ 168 + Email: "fox@example.com", 169 + }, 170 + Signature: &git.ObjectSignature{ 171 + Payload: `tree f96f1a4f1a51dc42e2983592f503980b60b8849c 172 + parent 93f84db542dd8c6e952c8130bc2fcbe2e299b8b4 173 + author OwO <instance@example.com> 1738961379 +0100 174 + committer UwU <fox@example.com> 1738961379 +0100 175 + 176 + Fox 177 + `, 178 + Signature: `-----BEGIN SSH SIGNATURE----- 179 + U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgV5ELwZ8XJe2LLR/UTuEu/vsFdb 180 + t7ry0W8hyzz/b1iocAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 181 + AAAAQCnyMRkWVVNoZxZkvi/ZoknUhs4LNBmEwZs9e9214WIt+mhKfc6BiHoE2qeluR2McD 182 + Y5RzHnA8Ke9wXddEePCQE= 183 + -----END SSH SIGNATURE----- 184 + `, 185 + }, 186 + } 187 + 188 + o := commitToGitObject(gitCommit) 189 + commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2) 190 + assert.True(t, commitVerification.Verified) 191 + assert.Equal(t, "UwU / SHA256:QttK41r/zMUeAW71b5UgVSb8xGFF/DlZJ6TyADW+uoI", commitVerification.Reason) 192 + assert.Equal(t, "SHA256:QttK41r/zMUeAW71b5UgVSb8xGFF/DlZJ6TyADW+uoI", commitVerification.SigningSSHKey.Fingerprint) 152 193 }) 153 194 }
+52
modules/git/git.go
··· 278 278 return err 279 279 } 280 280 281 + switch setting.Repository.Signing.Format { 282 + case "ssh": 283 + // First do a git version check. 284 + if CheckGitVersionAtLeast("2.34.0") != nil { 285 + return errors.New("ssh signing requires Git >= 2.34.0") 286 + } 287 + 288 + // Get the ssh-keygen binary that Git will use. 289 + // This can be overriden in app.ini in [git.config] section, so we must 290 + // query this information. 291 + sshKeygenPath, err := configGet("gpg.ssh.program") 292 + if err != nil { 293 + return err 294 + } 295 + // git is very stubborn and does not give a default value, so we must do 296 + // this ourselves. 297 + if len(sshKeygenPath) == 0 { 298 + // Default value of git, very unlikely to change. 299 + // https://github.com/git/git/blob/5b97a56fa0e7d580dc8865b73107407c9b3f0eff/gpg-interface.c#L116 300 + sshKeygenPath = "ssh-keygen" 301 + } 302 + 303 + // Although there's a version requirement of 8.2p1, there's no cross-version 304 + // method to get the version of ssh-keygen. Therefore we do a simple binary 305 + // presence check and hope for the best. 306 + if _, err := exec.LookPath(sshKeygenPath); err != nil { 307 + if errors.Is(err, exec.ErrNotFound) { 308 + return errors.New("git signing requires a ssh-keygen binary") 309 + } 310 + return err 311 + } 312 + 313 + if err := configSet("gpg.format", "ssh"); err != nil { 314 + return err 315 + } 316 + // openpgp is already the default value, so in the case of a non SSH format 317 + // set the value to openpgp. 318 + default: 319 + if err := configSet("gpg.format", "openpgp"); err != nil { 320 + return err 321 + } 322 + } 323 + 281 324 // By default partial clones are disabled, enable them from git v2.22 282 325 if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil { 283 326 if err = configSet("uploadpack.allowfilter", "true"); err != nil { ··· 322 365 return fmt.Errorf("installed git binary version %s is not equal to %s", gitVersion.Original(), equal) 323 366 } 324 367 return nil 368 + } 369 + 370 + func configGet(key string) (string, error) { 371 + stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) 372 + if err != nil && !IsErrorExitCode(err, 1) { 373 + return "", fmt.Errorf("failed to get git config %s, err: %w", key, err) 374 + } 375 + 376 + return strings.TrimSpace(stdout), nil 325 377 } 326 378 327 379 func configSet(key, value string) error {
+56
modules/git/git_test.go
··· 11 11 "testing" 12 12 13 13 "forgejo.org/modules/setting" 14 + "forgejo.org/modules/test" 14 15 "forgejo.org/modules/util" 15 16 17 + "github.com/hashicorp/go-version" 16 18 "github.com/stretchr/testify/assert" 17 19 "github.com/stretchr/testify/require" 18 20 ) ··· 94 96 assert.True(t, gitConfigContains("[sync-test]")) 95 97 assert.True(t, gitConfigContains("cfg-key-a = CfgValA")) 96 98 } 99 + 100 + func TestSyncConfigGPGFormat(t *testing.T) { 101 + defer test.MockProtect(&setting.GitConfig)() 102 + 103 + t.Run("No format", func(t *testing.T) { 104 + defer test.MockVariableValue(&setting.Repository.Signing.Format, "")() 105 + require.NoError(t, syncGitConfig()) 106 + assert.True(t, gitConfigContains("[gpg]")) 107 + assert.True(t, gitConfigContains("format = openpgp")) 108 + }) 109 + 110 + t.Run("SSH format", func(t *testing.T) { 111 + r, err := os.OpenRoot(t.TempDir()) 112 + require.NoError(t, err) 113 + f, err := r.OpenFile("ssh-keygen", os.O_CREATE|os.O_TRUNC, 0o700) 114 + require.NoError(t, f.Close()) 115 + require.NoError(t, err) 116 + t.Setenv("PATH", r.Name()) 117 + defer test.MockVariableValue(&setting.Repository.Signing.Format, "ssh")() 118 + 119 + require.NoError(t, syncGitConfig()) 120 + assert.True(t, gitConfigContains("[gpg]")) 121 + assert.True(t, gitConfigContains("format = ssh")) 122 + 123 + t.Run("Old version", func(t *testing.T) { 124 + oldVersion, err := version.NewVersion("2.33.0") 125 + require.NoError(t, err) 126 + defer test.MockVariableValue(&gitVersion, oldVersion)() 127 + require.ErrorContains(t, syncGitConfig(), "ssh signing requires Git >= 2.34.0") 128 + }) 129 + 130 + t.Run("No ssh-keygen binary", func(t *testing.T) { 131 + require.NoError(t, r.Remove("ssh-keygen")) 132 + require.ErrorContains(t, syncGitConfig(), "git signing requires a ssh-keygen binary") 133 + }) 134 + 135 + t.Run("Dynamic ssh-keygen binary location", func(t *testing.T) { 136 + f, err := r.OpenFile("ssh-keygen-2", os.O_CREATE|os.O_TRUNC, 0o700) 137 + require.NoError(t, f.Close()) 138 + require.NoError(t, err) 139 + defer test.MockVariableValue(&setting.GitConfig.Options, map[string]string{ 140 + "gpg.ssh.program": "ssh-keygen-2", 141 + })() 142 + require.NoError(t, syncGitConfig()) 143 + }) 144 + }) 145 + 146 + t.Run("OpenPGP format", func(t *testing.T) { 147 + defer test.MockVariableValue(&setting.Repository.Signing.Format, "openpgp")() 148 + require.NoError(t, syncGitConfig()) 149 + assert.True(t, gitConfigContains("[gpg]")) 150 + assert.True(t, gitConfigContains("format = openpgp")) 151 + }) 152 + }
+19
modules/setting/repository.go
··· 4 4 package setting 5 5 6 6 import ( 7 + "os" 7 8 "os/exec" 8 9 "path" 9 10 "path/filepath" 10 11 "strings" 11 12 12 13 "forgejo.org/modules/log" 14 + 15 + "golang.org/x/crypto/ssh" 13 16 ) 14 17 15 18 // enumerates all the policy repository creating ··· 25 28 26 29 // MaxForksPerPage sets maximum amount of forks shown per page 27 30 var MaxForksPerPage = 40 31 + 32 + var SSHInstanceKey ssh.PublicKey 28 33 29 34 // Repository settings 30 35 var ( ··· 109 114 SigningKey string 110 115 SigningName string 111 116 SigningEmail string 117 + Format string 112 118 InitialCommit []string 113 119 CRUDActions []string `ini:"CRUD_ACTIONS"` 114 120 Merges []string ··· 262 268 SigningKey string 263 269 SigningName string 264 270 SigningEmail string 271 + Format string 265 272 InitialCommit []string 266 273 CRUDActions []string `ini:"CRUD_ACTIONS"` 267 274 Merges []string ··· 271 278 SigningKey: "default", 272 279 SigningName: "", 273 280 SigningEmail: "", 281 + Format: "openpgp", 274 282 InitialCommit: []string{"always"}, 275 283 CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, 276 284 Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, ··· 376 384 log.Fatal("loadRepoArchiveFrom: %v", err) 377 385 } 378 386 Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool() 387 + 388 + if Repository.Signing.Format == "ssh" && Repository.Signing.SigningKey != "none" && Repository.Signing.SigningKey != "" { 389 + sshPublicKey, err := os.ReadFile(Repository.Signing.SigningKey) 390 + if err != nil { 391 + log.Fatal("Could not read repository signing key in %q: %v", Repository.Signing.SigningKey, err) 392 + } 393 + SSHInstanceKey, _, _, _, err = ssh.ParseAuthorizedKey(sshPublicKey) 394 + if err != nil { 395 + log.Fatal("Could not parse the SSH signing key %q: %v", sshPublicKey, err) 396 + } 397 + } 379 398 }
+59
modules/setting/repository_test.go
··· 1 + // Copyright 2025 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package setting 5 + 6 + import ( 7 + "fmt" 8 + "path/filepath" 9 + "testing" 10 + 11 + "github.com/stretchr/testify/assert" 12 + "github.com/stretchr/testify/require" 13 + "golang.org/x/crypto/ssh" 14 + ) 15 + 16 + func TestSSHInstanceKey(t *testing.T) { 17 + sshSigningKeyPath, err := filepath.Abs("../../tests/integration/ssh-signing-key.pub") 18 + require.NoError(t, err) 19 + 20 + t.Run("None value", func(t *testing.T) { 21 + cfg, err := NewConfigProviderFromData(` 22 + [repository.signing] 23 + FORMAT = ssh 24 + SIGNING_KEY = none 25 + `) 26 + require.NoError(t, err) 27 + 28 + loadRepositoryFrom(cfg) 29 + 30 + assert.Nil(t, SSHInstanceKey) 31 + }) 32 + 33 + t.Run("No value", func(t *testing.T) { 34 + cfg, err := NewConfigProviderFromData(` 35 + [repository.signing] 36 + FORMAT = ssh 37 + `) 38 + require.NoError(t, err) 39 + 40 + loadRepositoryFrom(cfg) 41 + 42 + assert.Nil(t, SSHInstanceKey) 43 + }) 44 + t.Run("Normal", func(t *testing.T) { 45 + iniStr := fmt.Sprintf(` 46 + [repository.signing] 47 + FORMAT = ssh 48 + SIGNING_KEY = %s 49 + `, sshSigningKeyPath) 50 + cfg, err := NewConfigProviderFromData(iniStr) 51 + require.NoError(t, err) 52 + 53 + loadRepositoryFrom(cfg) 54 + 55 + assert.NotNil(t, SSHInstanceKey) 56 + assert.Equal(t, "ssh-ed25519", SSHInstanceKey.Type()) 57 + assert.EqualValues(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH\n", ssh.MarshalAuthorizedKey(SSHInstanceKey)) 58 + }) 59 + }
+1
routers/api/v1/api.go
··· 865 865 m.Group("", func() { 866 866 m.Get("/version", misc.Version) 867 867 m.Get("/signing-key.gpg", misc.SigningKey) 868 + m.Get("/signing-key.ssh", misc.SSHSigningKey) 868 869 m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) 869 870 m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) 870 871 m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
+29
routers/api/v1/misc/signing.go
··· 7 7 "fmt" 8 8 "net/http" 9 9 10 + "forgejo.org/modules/setting" 10 11 asymkey_service "forgejo.org/services/asymkey" 11 12 "forgejo.org/services/context" 13 + 14 + "golang.org/x/crypto/ssh" 12 15 ) 13 16 14 17 // SigningKey returns the public key of the default signing key if it exists ··· 61 64 ctx.Error(http.StatusInternalServerError, "gpg export", fmt.Errorf("Error writing key content %w", err)) 62 65 } 63 66 } 67 + 68 + // SSHSigningKey returns the public SSH key of the default signing key if it exists 69 + func SSHSigningKey(ctx *context.APIContext) { 70 + // swagger:operation GET /signing-key.ssh miscellaneous getSSHSigningKey 71 + // --- 72 + // summary: Get default signing-key.ssh 73 + // produces: 74 + // - text/plain 75 + // responses: 76 + // "200": 77 + // description: "SSH public key in OpenSSH authorized key format" 78 + // schema: 79 + // type: string 80 + // "404": 81 + // "$ref": "#/responses/notFound" 82 + 83 + if setting.SSHInstanceKey == nil { 84 + ctx.NotFound() 85 + return 86 + } 87 + 88 + _, err := ctx.Write(ssh.MarshalAuthorizedKey(setting.SSHInstanceKey)) 89 + if err != nil { 90 + ctx.Error(http.StatusInternalServerError, "ssh export", err) 91 + } 92 + }
+7
services/asymkey/sign.go
··· 90 90 return "", nil 91 91 } 92 92 93 + if setting.Repository.Signing.Format == "ssh" { 94 + return setting.Repository.Signing.SigningKey, &git.Signature{ 95 + Name: setting.Repository.Signing.SigningName, 96 + Email: setting.Repository.Signing.SigningEmail, 97 + } 98 + } 99 + 93 100 if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { 94 101 // Can ignore the error here as it means that commit.gpgsign is not set 95 102 value, _, _ := git.NewCommand(ctx, "config", "--get", "commit.gpgsign").RunStdString(&git.RunOpts{Dir: repoPath})
+23
templates/swagger/v1_json.tmpl
··· 17315 17315 } 17316 17316 } 17317 17317 }, 17318 + "/signing-key.ssh": { 17319 + "get": { 17320 + "produces": [ 17321 + "text/plain" 17322 + ], 17323 + "tags": [ 17324 + "miscellaneous" 17325 + ], 17326 + "summary": "Get default signing-key.ssh", 17327 + "operationId": "getSSHSigningKey", 17328 + "responses": { 17329 + "200": { 17330 + "description": "SSH public key in OpenSSH authorized key format", 17331 + "schema": { 17332 + "type": "string" 17333 + } 17334 + }, 17335 + "404": { 17336 + "$ref": "#/responses/notFound" 17337 + } 17338 + } 17339 + } 17340 + }, 17318 17341 "/teams/{id}": { 17319 17342 "get": { 17320 17343 "produces": [
+38
tests/integration/api_misc_test.go
··· 1 + // Copyright 2025 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package integration 5 + 6 + import ( 7 + "net/http" 8 + "testing" 9 + 10 + "forgejo.org/modules/setting" 11 + "forgejo.org/modules/test" 12 + "forgejo.org/tests" 13 + 14 + "github.com/stretchr/testify/assert" 15 + "github.com/stretchr/testify/require" 16 + "golang.org/x/crypto/ssh" 17 + ) 18 + 19 + func TestAPISSHSigningKey(t *testing.T) { 20 + defer tests.PrepareTestEnv(t)() 21 + 22 + t.Run("No signing key", func(t *testing.T) { 23 + defer test.MockVariableValue(&setting.SSHInstanceKey, nil)() 24 + defer tests.PrintCurrentTest(t)() 25 + 26 + MakeRequest(t, NewRequest(t, "GET", "/api/v1/signing-key.ssh"), http.StatusNotFound) 27 + }) 28 + t.Run("With signing key", func(t *testing.T) { 29 + publicKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH\n" 30 + pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKey)) 31 + require.NoError(t, err) 32 + defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)() 33 + defer tests.PrintCurrentTest(t)() 34 + 35 + resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/signing-key.ssh"), http.StatusOK) 36 + assert.Equal(t, publicKey, resp.Body.String()) 37 + }) 38 + }
-304
tests/integration/gpg_git_test.go
··· 1 - // Copyright 2019 The Gitea Authors. All rights reserved. 2 - // SPDX-License-Identifier: MIT 3 - 4 - package integration 5 - 6 - import ( 7 - "encoding/base64" 8 - "fmt" 9 - "net/url" 10 - "os" 11 - "testing" 12 - 13 - auth_model "forgejo.org/models/auth" 14 - "forgejo.org/models/unittest" 15 - user_model "forgejo.org/models/user" 16 - "forgejo.org/modules/git" 17 - "forgejo.org/modules/process" 18 - "forgejo.org/modules/setting" 19 - api "forgejo.org/modules/structs" 20 - "forgejo.org/modules/test" 21 - "forgejo.org/tests" 22 - 23 - "github.com/ProtonMail/go-crypto/openpgp" 24 - "github.com/ProtonMail/go-crypto/openpgp/armor" 25 - "github.com/stretchr/testify/assert" 26 - "github.com/stretchr/testify/require" 27 - ) 28 - 29 - func TestGPGGit(t *testing.T) { 30 - tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring 31 - err := os.Chmod(tmpDir, 0o700) 32 - require.NoError(t, err) 33 - 34 - t.Setenv("GNUPGHOME", tmpDir) 35 - require.NoError(t, err) 36 - 37 - // Need to create a root key 38 - rootKeyPair, err := importTestingKey() 39 - require.NoError(t, err, "importTestingKey") 40 - 41 - defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, rootKeyPair.PrimaryKey.KeyIdShortString())() 42 - defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")() 43 - defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")() 44 - defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() 45 - defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() 46 - 47 - username := "user2" 48 - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) 49 - baseAPITestContext := NewAPITestContext(t, username, "repo1") 50 - 51 - onGiteaRun(t, func(t *testing.T, u *url.URL) { 52 - u.Path = baseAPITestContext.GitPath() 53 - 54 - t.Run("Unsigned-Initial", func(t *testing.T) { 55 - defer tests.PrintCurrentTest(t)() 56 - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 57 - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat 58 - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 59 - assert.NotNil(t, branch.Commit) 60 - assert.NotNil(t, branch.Commit.Verification) 61 - assert.False(t, branch.Commit.Verification.Verified) 62 - assert.Empty(t, branch.Commit.Verification.Signature) 63 - })) 64 - t.Run("CreateCRUDFile-Never", crudActionCreateFile( 65 - t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { 66 - assert.False(t, response.Verification.Verified) 67 - })) 68 - t.Run("CreateCRUDFile-Never", crudActionCreateFile( 69 - t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { 70 - assert.False(t, response.Verification.Verified) 71 - })) 72 - }) 73 - 74 - setting.Repository.Signing.CRUDActions = []string{"parentsigned"} 75 - t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { 76 - defer tests.PrintCurrentTest(t)() 77 - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 78 - t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( 79 - t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { 80 - assert.False(t, response.Verification.Verified) 81 - })) 82 - t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( 83 - t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { 84 - assert.False(t, response.Verification.Verified) 85 - })) 86 - }) 87 - 88 - setting.Repository.Signing.CRUDActions = []string{"never"} 89 - t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) { 90 - defer tests.PrintCurrentTest(t)() 91 - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 92 - t.Run("CreateCRUDFile-Never", crudActionCreateFile( 93 - t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { 94 - assert.False(t, response.Verification.Verified) 95 - })) 96 - }) 97 - 98 - setting.Repository.Signing.CRUDActions = []string{"always"} 99 - t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) { 100 - defer tests.PrintCurrentTest(t)() 101 - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 102 - t.Run("CreateCRUDFile-Always", crudActionCreateFile( 103 - t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { 104 - assert.NotNil(t, response.Verification) 105 - if response.Verification == nil { 106 - assert.FailNow(t, "no verification provided with response", "response: %v", response) 107 - } 108 - assert.True(t, response.Verification.Verified) 109 - if !response.Verification.Verified { 110 - t.FailNow() 111 - } 112 - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) 113 - })) 114 - t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile( 115 - t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { 116 - assert.NotNil(t, response.Verification) 117 - if response.Verification == nil { 118 - assert.FailNow(t, "no verification provided with response", "response: %v", response) 119 - } 120 - assert.True(t, response.Verification.Verified) 121 - if !response.Verification.Verified { 122 - t.FailNow() 123 - } 124 - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) 125 - })) 126 - }) 127 - 128 - setting.Repository.Signing.CRUDActions = []string{"parentsigned"} 129 - t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { 130 - defer tests.PrintCurrentTest(t)() 131 - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 132 - t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile( 133 - t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) { 134 - assert.NotNil(t, response.Verification) 135 - if response.Verification == nil { 136 - assert.FailNow(t, "no verification provided with response", "response: %v", response) 137 - } 138 - assert.True(t, response.Verification.Verified) 139 - if !response.Verification.Verified { 140 - t.FailNow() 141 - } 142 - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) 143 - })) 144 - }) 145 - 146 - setting.Repository.Signing.InitialCommit = []string{"always"} 147 - t.Run("AlwaysSign-Initial", func(t *testing.T) { 148 - defer tests.PrintCurrentTest(t)() 149 - testCtx := NewAPITestContext(t, username, "initial-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 150 - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat 151 - t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 152 - assert.NotNil(t, branch.Commit) 153 - if branch.Commit == nil { 154 - assert.FailNow(t, "no commit provided with branch", "branch: %v", branch) 155 - } 156 - assert.NotNil(t, branch.Commit.Verification) 157 - if branch.Commit.Verification == nil { 158 - assert.FailNow(t, "no verification provided with branch commit", "commit: %v", branch.Commit) 159 - } 160 - assert.True(t, branch.Commit.Verification.Verified) 161 - if !branch.Commit.Verification.Verified { 162 - t.FailNow() 163 - } 164 - assert.Equal(t, "gitea@fake.local", branch.Commit.Verification.Signer.Email) 165 - })) 166 - }) 167 - 168 - setting.Repository.Signing.CRUDActions = []string{"never"} 169 - t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) { 170 - defer tests.PrintCurrentTest(t)() 171 - testCtx := NewAPITestContext(t, username, "initial-always-never", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 172 - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat 173 - t.Run("CreateCRUDFile-Never", crudActionCreateFile( 174 - t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { 175 - assert.False(t, response.Verification.Verified) 176 - })) 177 - }) 178 - 179 - setting.Repository.Signing.CRUDActions = []string{"parentsigned"} 180 - t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) { 181 - defer tests.PrintCurrentTest(t)() 182 - testCtx := NewAPITestContext(t, username, "initial-always-parent", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 183 - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat 184 - t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( 185 - t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { 186 - assert.True(t, response.Verification.Verified) 187 - if !response.Verification.Verified { 188 - t.FailNow() 189 - return 190 - } 191 - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) 192 - })) 193 - }) 194 - 195 - setting.Repository.Signing.CRUDActions = []string{"always"} 196 - t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) { 197 - defer tests.PrintCurrentTest(t)() 198 - testCtx := NewAPITestContext(t, username, "initial-always-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 199 - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat 200 - t.Run("CreateCRUDFile-Always", crudActionCreateFile( 201 - t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { 202 - assert.True(t, response.Verification.Verified) 203 - if !response.Verification.Verified { 204 - t.FailNow() 205 - return 206 - } 207 - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) 208 - })) 209 - }) 210 - 211 - setting.Repository.Signing.Merges = []string{"commitssigned"} 212 - t.Run("UnsignedMerging", func(t *testing.T) { 213 - defer tests.PrintCurrentTest(t)() 214 - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 215 - t.Run("CreatePullRequest", func(t *testing.T) { 216 - pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t) 217 - require.NoError(t, err) 218 - t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) 219 - }) 220 - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 221 - assert.NotNil(t, branch.Commit) 222 - assert.NotNil(t, branch.Commit.Verification) 223 - assert.False(t, branch.Commit.Verification.Verified) 224 - assert.Empty(t, branch.Commit.Verification.Signature) 225 - })) 226 - }) 227 - 228 - setting.Repository.Signing.Merges = []string{"basesigned"} 229 - t.Run("BaseSignedMerging", func(t *testing.T) { 230 - defer tests.PrintCurrentTest(t)() 231 - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 232 - t.Run("CreatePullRequest", func(t *testing.T) { 233 - pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t) 234 - require.NoError(t, err) 235 - t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) 236 - }) 237 - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 238 - assert.NotNil(t, branch.Commit) 239 - assert.NotNil(t, branch.Commit.Verification) 240 - assert.False(t, branch.Commit.Verification.Verified) 241 - assert.Empty(t, branch.Commit.Verification.Signature) 242 - })) 243 - }) 244 - 245 - setting.Repository.Signing.Merges = []string{"commitssigned"} 246 - t.Run("CommitsSignedMerging", func(t *testing.T) { 247 - defer tests.PrintCurrentTest(t)() 248 - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 249 - t.Run("CreatePullRequest", func(t *testing.T) { 250 - pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t) 251 - require.NoError(t, err) 252 - t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) 253 - }) 254 - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 255 - assert.NotNil(t, branch.Commit) 256 - assert.NotNil(t, branch.Commit.Verification) 257 - assert.True(t, branch.Commit.Verification.Verified) 258 - })) 259 - }) 260 - }) 261 - } 262 - 263 - func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) { 264 - return doAPICreateFile(ctx, path, &api.CreateFileOptions{ 265 - FileOptions: api.FileOptions{ 266 - BranchName: from, 267 - NewBranchName: to, 268 - Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path), 269 - Author: api.Identity{ 270 - Name: user.FullName, 271 - Email: user.Email, 272 - }, 273 - Committer: api.Identity{ 274 - Name: user.FullName, 275 - Email: user.Email, 276 - }, 277 - }, 278 - ContentBase64: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("This is new text for %s", path))), 279 - }, callback...) 280 - } 281 - 282 - func importTestingKey() (*openpgp.Entity, error) { 283 - if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil { 284 - return nil, err 285 - } 286 - keyringFile, err := os.Open("tests/integration/private-testing.key") 287 - if err != nil { 288 - return nil, err 289 - } 290 - defer keyringFile.Close() 291 - 292 - block, err := armor.Decode(keyringFile) 293 - if err != nil { 294 - return nil, err 295 - } 296 - 297 - keyring, err := openpgp.ReadKeyRing(block.Body) 298 - if err != nil { 299 - return nil, fmt.Errorf("Keyring access failed: '%w'", err) 300 - } 301 - 302 - // There should only be one entity in this file. 303 - return keyring[0], nil 304 - }
+330
tests/integration/signing_git_test.go
··· 1 + // Copyright 2019 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package integration 5 + 6 + import ( 7 + "context" 8 + "encoding/base64" 9 + "fmt" 10 + "net/url" 11 + "os" 12 + "path/filepath" 13 + "testing" 14 + 15 + auth_model "forgejo.org/models/auth" 16 + "forgejo.org/models/unittest" 17 + user_model "forgejo.org/models/user" 18 + "forgejo.org/modules/git" 19 + "forgejo.org/modules/process" 20 + "forgejo.org/modules/setting" 21 + api "forgejo.org/modules/structs" 22 + "forgejo.org/modules/test" 23 + "forgejo.org/tests" 24 + 25 + "github.com/ProtonMail/go-crypto/openpgp" 26 + "github.com/ProtonMail/go-crypto/openpgp/armor" 27 + "github.com/stretchr/testify/assert" 28 + "github.com/stretchr/testify/require" 29 + "golang.org/x/crypto/ssh" 30 + ) 31 + 32 + func TestInstanceSigning(t *testing.T) { 33 + t.Cleanup(func() { 34 + // Cannot use t.Context(), it is in the done state. 35 + require.NoError(t, git.InitFull(context.Background())) //nolint:usetesting 36 + }) 37 + 38 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 39 + defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "UwU")() 40 + defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "fox@example.com")() 41 + defer test.MockProtect(&setting.Repository.Signing.InitialCommit)() 42 + defer test.MockProtect(&setting.Repository.Signing.CRUDActions)() 43 + 44 + t.Run("SSH", func(t *testing.T) { 45 + defer tests.PrintCurrentTest(t)() 46 + 47 + pubKeyContent, err := os.ReadFile("tests/integration/ssh-signing-key.pub") 48 + require.NoError(t, err) 49 + 50 + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyContent) 51 + require.NoError(t, err) 52 + signingKeyPath, err := filepath.Abs("tests/integration/ssh-signing-key") 53 + require.NoError(t, err) 54 + require.NoError(t, os.Chmod(signingKeyPath, 0o600)) 55 + defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)() 56 + defer test.MockVariableValue(&setting.Repository.Signing.Format, "ssh")() 57 + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, signingKeyPath)() 58 + 59 + // Ensure the git config is updated with the new signing format. 60 + require.NoError(t, git.InitFull(t.Context())) 61 + 62 + forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) { 63 + u2 := *u 64 + testCRUD(t, &u2, "ssh", objectFormat) 65 + }) 66 + }) 67 + 68 + t.Run("PGP", func(t *testing.T) { 69 + defer tests.PrintCurrentTest(t)() 70 + 71 + // Use a new GNUPGPHOME to avoid messing with the existing GPG keyring. 72 + tmpDir := t.TempDir() 73 + require.NoError(t, os.Chmod(tmpDir, 0o700)) 74 + t.Setenv("GNUPGHOME", tmpDir) 75 + 76 + rootKeyPair, err := importTestingKey() 77 + require.NoError(t, err) 78 + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, rootKeyPair.PrimaryKey.KeyIdShortString())() 79 + defer test.MockVariableValue(&setting.Repository.Signing.Format, "openpgp")() 80 + 81 + // Ensure the git config is updated with the new signing format. 82 + require.NoError(t, git.InitFull(t.Context())) 83 + 84 + forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) { 85 + u2 := *u 86 + testCRUD(t, &u2, "pgp", objectFormat) 87 + }) 88 + }) 89 + }) 90 + } 91 + 92 + func testCRUD(t *testing.T, u *url.URL, signingFormat string, objectFormat git.ObjectFormat) { 93 + t.Helper() 94 + setting.Repository.Signing.CRUDActions = []string{"never"} 95 + setting.Repository.Signing.InitialCommit = []string{"never"} 96 + 97 + username := "user2" 98 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) 99 + baseAPITestContext := NewAPITestContext(t, username, "repo1") 100 + u.Path = baseAPITestContext.GitPath() 101 + 102 + suffix := "-" + signingFormat + "-" + objectFormat.Name() 103 + 104 + t.Run("Unsigned-Initial", func(t *testing.T) { 105 + defer tests.PrintCurrentTest(t)() 106 + 107 + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 108 + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) 109 + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 110 + assert.NotNil(t, branch.Commit) 111 + assert.NotNil(t, branch.Commit.Verification) 112 + assert.False(t, branch.Commit.Verification.Verified) 113 + assert.Empty(t, branch.Commit.Verification.Signature) 114 + })) 115 + t.Run("CreateCRUDFile-Never", crudActionCreateFile( 116 + t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { 117 + assert.False(t, response.Verification.Verified) 118 + })) 119 + t.Run("CreateCRUDFile-Never", crudActionCreateFile( 120 + t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { 121 + assert.False(t, response.Verification.Verified) 122 + })) 123 + }) 124 + 125 + t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { 126 + defer tests.PrintCurrentTest(t)() 127 + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} 128 + 129 + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 130 + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( 131 + t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { 132 + assert.False(t, response.Verification.Verified) 133 + })) 134 + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( 135 + t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { 136 + assert.False(t, response.Verification.Verified) 137 + })) 138 + }) 139 + 140 + t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) { 141 + defer tests.PrintCurrentTest(t)() 142 + setting.Repository.Signing.InitialCommit = []string{"never"} 143 + 144 + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 145 + t.Run("CreateCRUDFile-Never", crudActionCreateFile( 146 + t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { 147 + assert.False(t, response.Verification.Verified) 148 + })) 149 + }) 150 + 151 + t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) { 152 + defer tests.PrintCurrentTest(t)() 153 + setting.Repository.Signing.CRUDActions = []string{"always"} 154 + 155 + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 156 + t.Run("CreateCRUDFile-Always", crudActionCreateFile( 157 + t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { 158 + require.NotNil(t, response.Verification) 159 + assert.True(t, response.Verification.Verified) 160 + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) 161 + })) 162 + t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile( 163 + t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { 164 + require.NotNil(t, response.Verification) 165 + assert.True(t, response.Verification.Verified) 166 + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) 167 + })) 168 + }) 169 + 170 + t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { 171 + defer tests.PrintCurrentTest(t)() 172 + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} 173 + 174 + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 175 + t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile( 176 + t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) { 177 + require.NotNil(t, response.Verification) 178 + assert.True(t, response.Verification.Verified) 179 + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) 180 + })) 181 + }) 182 + 183 + t.Run("AlwaysSign-Initial", func(t *testing.T) { 184 + defer tests.PrintCurrentTest(t)() 185 + setting.Repository.Signing.InitialCommit = []string{"always"} 186 + 187 + testCtx := NewAPITestContext(t, username, "initial-always"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 188 + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) 189 + t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 190 + require.NotNil(t, branch.Commit) 191 + require.NotNil(t, branch.Commit.Verification) 192 + assert.True(t, branch.Commit.Verification.Verified) 193 + assert.Equal(t, "fox@example.com", branch.Commit.Verification.Signer.Email) 194 + })) 195 + }) 196 + 197 + t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) { 198 + defer tests.PrintCurrentTest(t)() 199 + setting.Repository.Signing.CRUDActions = []string{"never"} 200 + 201 + testCtx := NewAPITestContext(t, username, "initial-always-never"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 202 + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) 203 + t.Run("CreateCRUDFile-Never", crudActionCreateFile( 204 + t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { 205 + assert.False(t, response.Verification.Verified) 206 + })) 207 + }) 208 + 209 + t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) { 210 + defer tests.PrintCurrentTest(t)() 211 + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} 212 + 213 + testCtx := NewAPITestContext(t, username, "initial-always-parent"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 214 + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) 215 + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( 216 + t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { 217 + assert.True(t, response.Verification.Verified) 218 + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) 219 + })) 220 + }) 221 + 222 + t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) { 223 + defer tests.PrintCurrentTest(t)() 224 + setting.Repository.Signing.CRUDActions = []string{"always"} 225 + 226 + testCtx := NewAPITestContext(t, username, "initial-always-always"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 227 + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) 228 + t.Run("CreateCRUDFile-Always", crudActionCreateFile( 229 + t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { 230 + assert.True(t, response.Verification.Verified) 231 + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) 232 + })) 233 + }) 234 + 235 + t.Run("UnsignedMerging", func(t *testing.T) { 236 + defer tests.PrintCurrentTest(t)() 237 + setting.Repository.Signing.Merges = []string{"commitssigned"} 238 + 239 + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 240 + t.Run("CreatePullRequest", func(t *testing.T) { 241 + pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t) 242 + require.NoError(t, err) 243 + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) 244 + }) 245 + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 246 + require.NotNil(t, branch.Commit) 247 + require.NotNil(t, branch.Commit.Verification) 248 + assert.False(t, branch.Commit.Verification.Verified) 249 + assert.Empty(t, branch.Commit.Verification.Signature) 250 + })) 251 + }) 252 + 253 + t.Run("BaseSignedMerging", func(t *testing.T) { 254 + defer tests.PrintCurrentTest(t)() 255 + setting.Repository.Signing.Merges = []string{"basesigned"} 256 + 257 + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 258 + t.Run("CreatePullRequest", func(t *testing.T) { 259 + pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t) 260 + require.NoError(t, err) 261 + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) 262 + }) 263 + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 264 + require.NotNil(t, branch.Commit) 265 + require.NotNil(t, branch.Commit.Verification) 266 + assert.False(t, branch.Commit.Verification.Verified) 267 + assert.Empty(t, branch.Commit.Verification.Signature) 268 + })) 269 + }) 270 + 271 + t.Run("CommitsSignedMerging", func(t *testing.T) { 272 + defer tests.PrintCurrentTest(t)() 273 + setting.Repository.Signing.Merges = []string{"commitssigned"} 274 + 275 + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) 276 + t.Run("CreatePullRequest", func(t *testing.T) { 277 + pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t) 278 + require.NoError(t, err) 279 + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) 280 + }) 281 + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { 282 + require.NotNil(t, branch.Commit) 283 + require.NotNil(t, branch.Commit.Verification) 284 + assert.True(t, branch.Commit.Verification.Verified) 285 + })) 286 + }) 287 + } 288 + 289 + func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) { 290 + return doAPICreateFile(ctx, path, &api.CreateFileOptions{ 291 + FileOptions: api.FileOptions{ 292 + BranchName: from, 293 + NewBranchName: to, 294 + Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path), 295 + Author: api.Identity{ 296 + Name: user.FullName, 297 + Email: user.Email, 298 + }, 299 + Committer: api.Identity{ 300 + Name: user.FullName, 301 + Email: user.Email, 302 + }, 303 + }, 304 + ContentBase64: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("This is new text for %s", path))), 305 + }, callback...) 306 + } 307 + 308 + func importTestingKey() (*openpgp.Entity, error) { 309 + if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil { 310 + return nil, err 311 + } 312 + keyringFile, err := os.Open("tests/integration/private-testing.key") 313 + if err != nil { 314 + return nil, err 315 + } 316 + defer keyringFile.Close() 317 + 318 + block, err := armor.Decode(keyringFile) 319 + if err != nil { 320 + return nil, err 321 + } 322 + 323 + keyring, err := openpgp.ReadKeyRing(block.Body) 324 + if err != nil { 325 + return nil, fmt.Errorf("Keyring access failed: '%w'", err) 326 + } 327 + 328 + // There should only be one entity in this file. 329 + return keyring[0], nil 330 + }
+7
tests/integration/ssh-signing-key
··· 1 + -----BEGIN OPENSSH PRIVATE KEY----- 2 + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 + QyNTUxOQAAACBXkQvBnxcl7YstH9RO4S7++wV1u3uvLRbyHLPP9vWKhwAAAJhlmhmkZZoZ 4 + pAAAAAtzc2gtZWQyNTUxOQAAACBXkQvBnxcl7YstH9RO4S7++wV1u3uvLRbyHLPP9vWKhw 5 + AAAEDnOTuE2rDECN+2OsuUbQgGrMSY22tn+IF5JG5nuyJinVeRC8GfFyXtiy0f1E7hLv77 6 + BXW7e68tFvIcs8/29YqHAAAAE2d1c3RlZEBndXN0ZWQtYmVhc3QBAg== 7 + -----END OPENSSH PRIVATE KEY-----
+1
tests/integration/ssh-signing-key.pub
··· 1 + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH