Skip to content

Improve instance wide ssh commit signing #34341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from

Conversation

ChristopherHX
Copy link
Contributor

@ChristopherHX ChristopherHX commented May 2, 2025

  • Signed SSH commits can look in the UI like on GitHub, just like gpg keys today in Gitea
  • No user account of the SIGNING_EMAIL required anymore to get the verified badge
  • SSH format can be added in gitea config
  • No gitconfig changes needed
  • Set gpg.format git key for signing command
  • Previously only the default gpg key had global trust in Gitea
    • SSH Signing worked before with DEFAULT_TRUST_MODEL=committer, but not with model default and manually changing the .gitconfig

e.g. the following is all needed after ssh-keygen, no trouble with installing and setting up gpg or hacking around a hidden .gitconfig for ssh key usage

[repository.signing]
SIGNING_KEY = /data/id_ed25519.pub
SIGNING_NAME = Gitea
SIGNING_EMAIL = [email protected]
SIGNING_FORMAT = ssh
INITIAL_COMMIT = always
CRUD_ACTIONS = always
WIKI = always
MERGES = always

Where /data/id_ed25519 is the private key.

TRUSTED_SSH_KEYS can be a list of additional ssh public key contents to trust for every user of this instance

Closes #34329
Related #31392

* Signed SSH commits can look like on GitHub
* No user account of the committer needed
* SSH format can be added in gitea config
* No gitconfig changes needed
* Set gpg.format git key for signing command
* Previously only the default gpg key had global trust in Gitea
  * SSH Signing worked before with DEFAULT_TRUST_MODEL=committer, but not with model default and manually changing the .gitconfig

e.g. the following is all needed
```
[repository.signing]
SIGNING_KEY = /data/id_ed25519.pub
SIGNING_NAME = Gitea
SIGNING_EMAIL = [email protected]
SIGNING_FORMAT = ssh
INITIAL_COMMIT = always
CRUD_ACTIONS = always
WIKI = always
MERGES = always
```

`TRUSTED_SSH_KEYS` can be a list of additional ssh public keys to trust for every user of this instance
@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label May 2, 2025
@github-actions github-actions bot added modifies/api This PR adds API routes or modifies them modifies/go Pull requests that update Go code labels May 2, 2025
@ChristopherHX
Copy link
Contributor Author

ChristopherHX commented May 2, 2025

What do you think @brtwrst about this?

Except of an absent automatic setup this should now be even easier, by just editing a single file.

I found out that gpg supported global key verification for all users, but ssh not, this PR aims to change that.

No I have no idea how to write tests for this

@brtwrst
Copy link

brtwrst commented May 2, 2025

That looks awesome. Makes it super simple to set up and the TRUSTED_SSH_KEYS allows for "key rotation" without losing the green checkmark on older commits. How should TRUSTED_SSH_KEYS look in the app.ini?
Like this?
TRUSTED_SSH_KEYS="ssh-ed25519 AAAA... user1@hostname", "ssh-ed25519 AAAA... user2@hostname"
Or should it be a path to a file that includes the trusted public keys?

@ChristopherHX
Copy link
Contributor Author

I tested this like this TRUSTED_SSH_KEYS = ssh-ed25519 AAAA... and should support comma like other array like ini options.

Since the ssh keys are so simple idk if a double quote are even needed / supported.

File paths are not supported in this PR for this list.

@brtwrst
Copy link

brtwrst commented May 2, 2025

Ok, can't wait for this to make it in :)

Thank you for your work.

@lunny lunny added this to the 1.25.0 milestone May 3, 2025
@lunny lunny added the type/enhancement An improvement of existing functionality label May 3, 2025
@lunny
Copy link
Member

lunny commented May 11, 2025

app.example.ini needs to be updated.

@github-actions github-actions bot added the docs-update-needed The document needs to be updated synchronously label May 11, 2025
@@ -27,7 +27,7 @@ type mergeContext struct {
doer *user_model.User
sig *git.Signature
committer *git.Signature
signKeyID string // empty for no-sign, non-empty to sign
signKey git.SigningKey // empty for no-sign, non-empty to sign
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment needs to be updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e749a14

I didn't want to risk anything here by using pointers to a struct (e.g. allow nil), so technically the comment was still correct.

The empty git.SigningKey struct <==> git.SigningKey.KeyID is an empty string <==> do not sign

@@ -101,7 +101,7 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque
// Determine if we should sign
sign, keyID, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch)
if sign {
mergeCtx.signKeyID = keyID
mergeCtx.signKey = keyID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the variable name keyID should be updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lunny
Copy link
Member

lunny commented May 31, 2025

The test could be an integration one. Change the settings dynamically and push a commit and verify the UI badge.

@ChristopherHX
Copy link
Contributor Author

The test could be an integration one. Change the settings dynamically and push a commit and verify the UI badge.

Seems like using the integration test gpg_git_test is a start, the old test doesn't seem to test using defaults coming from gitconfig.

Had to change how I verify those ssh commits to have the expected signer user/email.

  • TRUSTED_SSH_KEYS is still not covered.

@TheFox0x7
Copy link
Contributor

Correct me if I'm wrong but doesn't this depend on ssh-keygen if git is signing commits?

@ChristopherHX
Copy link
Contributor Author

Correct me if I'm wrong but doesn't this depend on ssh-keygen if git is signing commits?

Yes this seems to be correct https://github.com/git/git/blob/7014b55638da979331baf8dc31c4e1d697cf2d67/gpg-interface.c#L1085 included in openssh version 8.2p1+

On my device is ssh-keygen a readonly system component (which I do not know how to hide from git) and the gitea docker container has it preinstalled as well.

Maybe include this in the example ini explicitly as requirement?


We might be able to consider adding this to the initial admin setup page to autogenerate the ssh key pair like in my test case or allow to configure this directly in the webui setup. I assume once Gitea is installed the config file should no longer be changed by Gitea

@TheFox0x7
Copy link
Contributor

I'm asking because I remember #33783, which comes from rootless container image not having ssh-keygen.
I did work on adding the generation of multiple key types at startup and I did have and idea of making the validation into a cli thing but I don't think it's worth to do ATM.

I'll submit a PR for that sometime today then since it's a indirect requirement for this.

@ChristopherHX
Copy link
Contributor Author

ChristopherHX commented Jun 1, 2025

I was not aware that the rootless image does not have it.

making the validation into a cli thing

Gitea does not use git for validating commits, only signing.

I just asked ai for a replacement for ssh-keygen to sign

If this code could be added to gitea cli, we might not even need ssh-keygen tool if we tell git to use gitea for signing.

Not tested at all xd, but not so much top of my mind.

Commit signing stub as ssh-keygen replacement
package main

import (
	"crypto/rand"
	"fmt"
	"io"
	"os"
	"strings"

	"golang.org/x/crypto/ssh"
)

func main() {
	if len(os.Args) < 7 || os.Args[1] != "-Y" || os.Args[2] != "sign" || os.Args[3] != "-f" || os.Args[5] != "-n" {
		fmt.Println("Usage: go run main.go -Y sign -f <private-key-file> -n <namespace> [file-to-sign]")
		os.Exit(1)
	}

	keyFile := os.Args[4]   // Private key file
	namespace := os.Args[6] // Namespace (e.g., "git")

	// Read private key file
	keyData, err := os.ReadFile(keyFile)
	if err != nil {
		fmt.Println("Failed to read private key file:", err)
		os.Exit(1)
	}

	// Parse private key
	signer, err := ssh.ParsePrivateKey(keyData)
	if err != nil {
		fmt.Println("Failed to parse private key:", err)
		os.Exit(1)
	}

	var message []byte
	var sigFileName string

	if len(os.Args) > 7 {
		// Read from file if provided
		fileToSign := os.Args[7]
		message, err = os.ReadFile(fileToSign)
		if err != nil {
			fmt.Println("Failed to read file:", err)
			os.Exit(1)
		}
		// Generate signature filename
		sigFileName = fileToSign + ".sig"
	} else {
		fmt.Println("Signing data on standard input")
		// Read from stdin
		message, err = io.ReadAll(os.Stdin)
		if err != nil {
			fmt.Println("Failed to read stdin:", err)
			os.Exit(1)
		}
	}

	// Incorporate the namespace into the signed message
	signedData := append([]byte(namespace+"\n"), message...)

	// Sign the message
	signature, err := signer.Sign(rand.Reader, signedData)
	if err != nil {
		fmt.Println("Failed to sign message:", err)
		os.Exit(1)
	}

	sshSignature := fmt.Sprintf(
		"-----BEGIN SSH SIGNATURE-----\n%s\n-----END SSH SIGNATURE-----\n",
		strings.ToUpper(fmt.Sprintf("%x", signature.Blob)),
	)

	if sigFileName != "" {
		// Save signature to file
		err = os.WriteFile(sigFileName, []byte(sshSignature), 0644)
		if err != nil {
			fmt.Println("Failed to save signature file:", err)
			os.Exit(1)
		}
		fmt.Println("Signature saved to", sigFileName)
	} else {
		// Print signature to stdout
		fmt.Println(sshSignature)
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs-update-needed The document needs to be updated synchronously lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. modifies/api This PR adds API routes or modifies them modifies/go Pull requests that update Go code type/enhancement An improvement of existing functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow SSH key to be used for commit signatures
6 participants