mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
- `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>
124 lines
4.2 KiB
Go
124 lines
4.2 KiB
Go
// 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)
|
|
}
|
|
}
|