Files
gitea-tea/internal/auth/httpsign_test.go
bircni 88b91faa54 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>
2026-10-11 17:09:37 +00:00

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)
}
}