mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
refactor: add credential and Git packages (#1123)
- `internal/auth`: credentials in the OS keyring or a private file, OAuth login with token refresh, SSH HTTP-signature signers (key files with passphrase prompt, ssh-agent keys and certificates), and the Git credential helper protocol. - `internal/git`: cancellable Git subprocesses, repository and remote discovery (tracked remote via `branch.<name>.remote`), PR checkout by full ref, AGit push, branch cleanup. Nothing imports these packages yet. Reviewed-on: https://gitea.com/gitea/tea/pulls/1123 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
This commit is contained in:
1 parent
2b218a5bae
commit
88b91faa54
28 files changed
+3044
-3
No files matched your search
@@ -0,0 +1,123 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/tea/internal/settings"
|
||||
"gitea.dev/tea/internal/transport"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestHTTPSignAttachesSignatureHeaders(t *testing.T) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
signer, err := ssh.NewSignerFromKey(key)
|
||||
require.NoError(t, err)
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "id_rsa")
|
||||
block, err := ssh.MarshalPrivateKey(key, "")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(path, pem.EncodeToMemory(block), 0o600))
|
||||
|
||||
var sawSignature, sawDigest bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sawSignature = r.Header.Get("Signature") != ""
|
||||
sawDigest = r.Header.Get("Digest") != ""
|
||||
require.Empty(t, r.Header.Get("Authorization"))
|
||||
_, _ = w.Write([]byte(`{"login":"alice"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
httpSign, err := newHTTPSign(path, false, "", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ssh.FingerprintSHA256(signer.PublicKey()), ssh.FingerprintSHA256(httpSign.PublicKey()))
|
||||
|
||||
client := transport.New(server.URL, "", false)
|
||||
client.Signer = httpSign
|
||||
response, err := client.Do(t.Context(), http.MethodGet, "user", nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, response.Body.Close())
|
||||
require.True(t, sawSignature)
|
||||
require.False(t, sawDigest)
|
||||
|
||||
sawSignature, sawDigest = false, false
|
||||
response, err = client.Do(t.Context(), http.MethodPost, "user", bytes.NewReader([]byte(`{"ok":true}`)), nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, response.Body.Close())
|
||||
require.True(t, sawSignature)
|
||||
require.True(t, sawDigest)
|
||||
|
||||
account := settings.Account{AuthMethod: "httpsign", SSHKey: path}
|
||||
fromAccount, err := SignerForAccount(account, func() ([]byte, error) {
|
||||
t.Error("unencrypted key asked for a passphrase")
|
||||
return nil, nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fromAccount)
|
||||
}
|
||||
|
||||
func TestEncryptedSSHKeyUsesInjectedPassphrase(t *testing.T) {
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
block, err := ssh.MarshalPrivateKeyWithPassphrase(key, "", []byte("correct horse"))
|
||||
require.NoError(t, err)
|
||||
path := filepath.Join(t.TempDir(), "id_ed25519")
|
||||
require.NoError(t, os.WriteFile(path, pem.EncodeToMemory(block), 0o600))
|
||||
account := settings.Account{AuthMethod: settings.AuthHTTPSign, SSHKey: path}
|
||||
|
||||
_, err = SignerForAccount(account, nil)
|
||||
require.ErrorContains(t, err, "passphrase protected; add it to ssh-agent")
|
||||
|
||||
prompts := 0
|
||||
signer, err := SignerForAccount(account, func() ([]byte, error) { prompts++; return []byte("correct horse"), nil })
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, prompts)
|
||||
require.Equal(t, ssh.KeyAlgoED25519, signer.PublicKey().Type())
|
||||
|
||||
_, err = SignerForAccount(account, func() ([]byte, error) { return []byte("wrong"), nil })
|
||||
require.ErrorContains(t, err, "cannot parse SSH key")
|
||||
}
|
||||
|
||||
func TestFindCertSignerHonorsValidity(t *testing.T) {
|
||||
_, caKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
ca, err := ssh.NewSignerFromKey(caKey)
|
||||
require.NoError(t, err)
|
||||
_, userKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
user, err := ssh.NewSignerFromKey(userKey)
|
||||
require.NoError(t, err)
|
||||
certSigner := func(validBefore uint64) ssh.Signer {
|
||||
cert := &ssh.Certificate{Key: user.PublicKey(), CertType: ssh.UserCert, ValidPrincipals: []string{"alice"}, ValidBefore: validBefore}
|
||||
require.NoError(t, cert.SignCert(rand.Reader, ca))
|
||||
signer, err := ssh.NewCertSigner(cert, user)
|
||||
require.NoError(t, err)
|
||||
return signer
|
||||
}
|
||||
for name, tc := range map[string]struct {
|
||||
validBefore uint64
|
||||
found bool
|
||||
}{
|
||||
"unlimited": {ssh.CertTimeInfinity, true},
|
||||
"valid": {uint64(time.Now().Add(time.Hour).Unix()), true},
|
||||
"expiring": {uint64(time.Now().Add(5 * time.Second).Unix()), false},
|
||||
"expired": {uint64(time.Now().Add(-time.Hour).Unix()), false},
|
||||
} {
|
||||
signer := certSigner(tc.validBefore)
|
||||
require.Equal(t, tc.found, findCertSigner([]ssh.Signer{signer}, "alice") != nil, name)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user