diff --git a/cmd/api.go b/cmd/api.go index 6c5c22a5..eccfc046 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -120,7 +120,10 @@ func runApi(requestCtx stdctx.Context, cmd *cli.Command) error { } // Create API client and make request - client := api.NewClient(ctx.Login) + client, err := api.NewClient(ctx.Login) + if err != nil { + return err + } resp, err := client.Do(request.Method, request.Endpoint, body, request.Headers) if err != nil { return fmt.Errorf("request failed: %w", err) diff --git a/cmd/login/add.go b/cmd/login/add.go index bcc71659..50f75a28 100644 --- a/cmd/login/add.go +++ b/cmd/login/add.go @@ -83,6 +83,16 @@ token. Equivalent to running 'tea login helper setup' afterwards.`, Aliases: []string{"i"}, Usage: "Disable TLS verification", }, + &cli.StringFlag{ + Name: "client-cert", + Sources: cli.EnvVars("GITEA_SERVER_CLIENT_CERT"), + Usage: "Path to a PEM encoded TLS client certificate, for servers requiring mutual TLS", + }, + &cli.StringFlag{ + Name: "client-key", + Sources: cli.EnvVars("GITEA_SERVER_CLIENT_KEY"), + Usage: "Path to the PEM encoded private key for --client-cert", + }, &cli.StringFlag{ Name: "ssh-agent-principal", Aliases: []string{"c"}, @@ -127,9 +137,11 @@ func runLoginAdd(requestCtx context.Context, cmd *cli.Command) error { // if OAuth flag is provided, use OAuth2 PKCE flow if cmd.Bool("oauth") { opts := auth.OAuthOptions{ - Name: cmd.String("name"), - URL: cmd.String("url"), - Insecure: cmd.Bool("insecure"), + Name: cmd.String("name"), + URL: cmd.String("url"), + Insecure: cmd.Bool("insecure"), + ClientCert: cmd.String("client-cert"), + ClientKey: cmd.String("client-key"), } // Only set clientID if provided @@ -164,6 +176,8 @@ func runLoginAdd(requestCtx context.Context, cmd *cli.Command) error { cmd.String("ssh-agent-principal"), cmd.String("ssh-agent-key"), cmd.Bool("insecure"), + cmd.String("client-cert"), + cmd.String("client-key"), sshAgent, !cmd.Bool("no-version-check"), cmd.Bool("git-credentials"), diff --git a/docs/CLI.md b/docs/CLI.md index 2363574b..01ab9f04 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -39,8 +39,12 @@ List Gitea logins Add a Gitea login +**--client-cert**="": Path to a PEM encoded TLS client certificate, for servers requiring mutual TLS + **--client-id**="": OAuth client ID (for use with --oauth) +**--client-key**="": Path to the PEM encoded private key for --client-cert + **--git-credentials, --helper, -j**: Register tea as a git credential helper for this login's URL, so 'git push' and 'git clone' over HTTPS authenticate silently using the stored token **--insecure, -i**: Disable TLS verification diff --git a/modules/api/client.go b/modules/api/client.go index 2892dd7c..1b56a49b 100644 --- a/modules/api/client.go +++ b/modules/api/client.go @@ -4,7 +4,6 @@ package api import ( - "crypto/tls" "fmt" "io" "log" @@ -24,21 +23,28 @@ type Client struct { } // NewClient creates a new API client from a Login config -func NewClient(login *config.Login) *Client { +func NewClient(login *config.Login) (*Client, error) { // Refresh OAuth token if expired or near expiry if err := login.RefreshOAuthTokenIfNeeded(); err != nil { log.Printf("Warning: failed to refresh OAuth token: %v", err) } + // A misconfigured client certificate is fatal rather than a fallback to a + // plain connection: the request would otherwise be rejected by the server + // with the same opaque error the certificate is meant to avoid. + tlsConfig, err := login.TLSConfig() + if err != nil { + return nil, err + } httpClient := &http.Client{ - Transport: httputil.WrapTransport(&tls.Config{InsecureSkipVerify: login.Insecure}), + Transport: httputil.WrapTransport(tlsConfig), } return &Client{ baseURL: strings.TrimSuffix(login.URL, "/"), token: login.GetAccessToken(), httpClient: httpClient, - } + }, nil } // Do executes an HTTP request with authentication headers diff --git a/modules/auth/oauth.go b/modules/auth/oauth.go index ccda4ab7..ff3bbbed 100644 --- a/modules/auth/oauth.go +++ b/modules/auth/oauth.go @@ -7,7 +7,6 @@ import ( "context" "crypto/rand" "crypto/sha256" - "crypto/tls" "encoding/base64" "fmt" "net" @@ -47,6 +46,8 @@ type OAuthOptions struct { Name string URL string Insecure bool + ClientCert string + ClientKey string ClientID string RedirectURL string Port int @@ -77,7 +78,7 @@ func OAuthLoginWithFullOptions(ctx context.Context, opts OAuthOptions) error { return err } - return createLoginFromToken(ctx, opts.Name, serverURL, token, opts.Insecure) + return createLoginFromToken(ctx, opts, serverURL, token) } // performBrowserOAuthFlow performs the browser-based OAuth2 PKCE flow and returns the token. @@ -127,7 +128,11 @@ func performBrowserOAuthFlow(ctx context.Context, opts OAuthOptions) (serverURL codeChallenge := generateCodeChallenge(codeVerifier) // Set up the OAuth2 config - ctx = context.WithValue(ctx, oauth2.HTTPClient, createHTTPClient(opts.Insecure)) + httpClient, err := createHTTPClient(opts) + if err != nil { + return "", nil, err + } + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) // Configure the OAuth2 endpoints authURL := fmt.Sprintf("%s/login/oauth/authorize", normalizedURL) @@ -198,11 +203,17 @@ func performBrowserOAuthFlow(ctx context.Context, opts OAuthOptions) (serverURL return serverURL, token, nil } -// createHTTPClient creates an HTTP client with optional insecure setting -func createHTTPClient(insecure bool) *http.Client { - return &http.Client{ - Transport: httputil.WrapTransport(&tls.Config{InsecureSkipVerify: insecure}), +// createHTTPClient creates an HTTP client honoring the login's TLS settings, +// so the OAuth flow reaches servers behind mutual-TLS proxies too. +func createHTTPClient(opts OAuthOptions) (*http.Client, error) { + l := config.Login{Insecure: opts.Insecure, ClientCert: opts.ClientCert, ClientKey: opts.ClientKey} + tlsConfig, err := l.TLSConfig() + if err != nil { + return nil, err } + return &http.Client{ + Transport: httputil.WrapTransport(tlsConfig), + }, nil } // generateCodeVerifier creates a cryptographically random string for PKCE @@ -372,7 +383,8 @@ var openBrowser = func(url string) error { } // createLoginFromToken creates a login entry using the obtained access token -func createLoginFromToken(ctx context.Context, name, serverURL string, token *oauth2.Token, insecure bool) error { +func createLoginFromToken(ctx context.Context, opts OAuthOptions, serverURL string, token *oauth2.Token) error { + name := opts.Name if name == "" { var err error name, err = task.GenerateLoginName(serverURL, "") @@ -387,7 +399,9 @@ func createLoginFromToken(ctx context.Context, name, serverURL string, token *oa URL: serverURL, Token: token.AccessToken, // temporarily set for Client() validation AuthMethod: config.AuthMethodOAuth, - Insecure: insecure, + Insecure: opts.Insecure, + ClientCert: opts.ClientCert, + ClientKey: opts.ClientKey, VersionCheck: true, Created: time.Now().Unix(), } @@ -435,6 +449,8 @@ func ReauthenticateLogin(ctx context.Context, login *config.Login) error { Name: login.Name, URL: login.URL, Insecure: login.Insecure, + ClientCert: login.ClientCert, + ClientKey: login.ClientKey, ClientID: config.DefaultClientID, RedirectURL: fmt.Sprintf("http://%s:%d", redirectHost, redirectPort), Port: redirectPort, diff --git a/modules/config/login.go b/modules/config/login.go index 7ae7968b..1d880895 100644 --- a/modules/config/login.go +++ b/modules/config/login.go @@ -44,8 +44,14 @@ type Login struct { Default bool `yaml:"default"` SSHHost string `yaml:"ssh_host"` // optional path to the private key - SSHKey string `yaml:"ssh_key"` - Insecure bool `yaml:"insecure"` + SSHKey string `yaml:"ssh_key"` + Insecure bool `yaml:"insecure"` + // ClientCert is an optional path to a PEM encoded TLS client certificate, + // presented to servers that require mutual TLS (e.g. a reverse proxy + // configured with nginx' ssl_verify_client). ClientKey is its private key; + // both must be set together. + ClientCert string `yaml:"client_cert,omitempty"` + ClientKey string `yaml:"client_key,omitempty"` SSHCertPrincipal string `yaml:"ssh_certificate_principal"` SSHAgent bool `yaml:"ssh_agent"` SSHKeyFingerprint string `yaml:"ssh_key_agent_pub"` @@ -63,6 +69,52 @@ type Login struct { TokenExpiry int64 `yaml:"token_expiry,omitempty"` } +// TLSConfig returns the TLS configuration to use for HTTPS requests to this +// login's server. It returns nil when the login needs neither relaxed +// verification nor a client certificate, so callers keep Go's default TLS +// behaviour. +func (l *Login) TLSConfig() (*tls.Config, error) { + cert, err := l.clientCertificate() + if err != nil { + return nil, err + } + if cert == nil && !l.Insecure { + return nil, nil + } + + cfg := &tls.Config{InsecureSkipVerify: l.Insecure} + if cert != nil { + cfg.Certificates = []tls.Certificate{*cert} + } + return cfg, nil +} + +// clientCertificate loads the login's PEM client certificate/key pair, and +// returns nil when the login does not configure one. +func (l *Login) clientCertificate() (*tls.Certificate, error) { + if l.ClientCert == "" && l.ClientKey == "" { + return nil, nil + } + if l.ClientCert == "" || l.ClientKey == "" { + return nil, errors.New("client_cert and client_key must be set together to use a TLS client certificate") + } + + certFile, err := utils.AbsPathWithExpansion(l.ClientCert) + if err != nil { + return nil, fmt.Errorf("invalid client certificate path %q: %w", l.ClientCert, err) + } + keyFile, err := utils.AbsPathWithExpansion(l.ClientKey) + if err != nil { + return nil, fmt.Errorf("invalid client key path %q: %w", l.ClientKey, err) + } + + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, fmt.Errorf("failed to load TLS client certificate: %w", err) + } + return &cert, nil +} + // IsOAuth returns true if this login uses OAuth with secure credential storage. func (l *Login) IsOAuth() bool { return l.AuthMethod == AuthMethodOAuth @@ -417,8 +469,12 @@ func doOAuthRefresh(ctx context.Context, l *Login) (*oauth2.Token, error) { Expiry: expiry, } + tlsConfig, err := l.TLSConfig() + if err != nil { + return nil, err + } httpClient := &http.Client{ - Transport: httputil.WrapTransport(&tls.Config{InsecureSkipVerify: l.Insecure}), + Transport: httputil.WrapTransport(tlsConfig), } ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) @@ -458,16 +514,16 @@ func (l *Login) ClientWithoutRefresh(options ...gitea.ClientOption) *gitea.Clien // fails fast instead of hanging forever. These bound connection setup and // time-to-first-response-byte only, so slow-but-progressing transfers (e.g. // large attachment uploads) are unaffected. + tlsConfig, err := l.TLSConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to configure TLS for login %s: %s\n", l.Name, err) + os.Exit(1) + } httpClient := &http.Client{ - Transport: httputil.WrapTransport(nil), + Transport: httputil.WrapTransport(tlsConfig), } if l.Insecure { - cookieJar, _ := cookiejar.New(nil) // New with nil options never returns an error - - httpClient = &http.Client{ - Jar: cookieJar, - Transport: httputil.WrapTransport(&tls.Config{InsecureSkipVerify: true}), - } + httpClient.Jar, _ = cookiejar.New(nil) // New with nil options never returns an error } // versioncheck must be prepended in options to make sure we don't hit any version checks in the sdk diff --git a/modules/config/login_tls_test.go b/modules/config/login_tls_test.go new file mode 100644 index 00000000..17fc2d77 --- /dev/null +++ b/modules/config/login_tls_test.go @@ -0,0 +1,170 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package config + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "gitea.dev/tea/modules/httputil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTLSConfigWithoutSettingsIsNil(t *testing.T) { + t.Parallel() + + cfg, err := (&Login{}).TLSConfig() + require.NoError(t, err) + assert.Nil(t, cfg, "a login with default TLS settings must not override the transport's TLS config") +} + +func TestTLSConfigInsecureOnly(t *testing.T) { + t.Parallel() + + cfg, err := (&Login{Insecure: true}).TLSConfig() + require.NoError(t, err) + require.NotNil(t, cfg) + assert.True(t, cfg.InsecureSkipVerify) + assert.Empty(t, cfg.Certificates) +} + +func TestTLSConfigRequiresBothCertAndKey(t *testing.T) { + t.Parallel() + + for name, login := range map[string]*Login{ + "cert without key": {ClientCert: "/tmp/client.crt"}, + "key without cert": {ClientKey: "/tmp/client.key"}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + _, err := login.TLSConfig() + require.Error(t, err) + assert.Contains(t, err.Error(), "must be set together") + }) + } +} + +func TestTLSConfigReportsUnreadableCertificate(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + login := &Login{ + ClientCert: filepath.Join(dir, "missing.crt"), + ClientKey: filepath.Join(dir, "missing.key"), + } + + _, err := login.TLSConfig() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to load TLS client certificate") +} + +func TestTLSConfigLoadsClientCertificate(t *testing.T) { + t.Parallel() + + _, certPath, keyPath := writeTestClientCert(t) + + cfg, err := (&Login{ClientCert: certPath, ClientKey: keyPath}).TLSConfig() + require.NoError(t, err) + require.NotNil(t, cfg) + assert.False(t, cfg.InsecureSkipVerify) + require.Len(t, cfg.Certificates, 1) +} + +// TestClientCertAuthenticatesAgainstMutualTLSServer is the end-to-end case from +// issue #451: a server that rejects any client not presenting a certificate, +// standing in for a reverse proxy configured with nginx' ssl_verify_client. +func TestClientCertAuthenticatesAgainstMutualTLSServer(t *testing.T) { + t.Parallel() + + clientCert, certPath, keyPath := writeTestClientCert(t) + + clientCAs := x509.NewCertPool() + clientCAs.AddCert(clientCert) + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + server.TLS = &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCAs, + } + server.StartTLS() + defer server.Close() + + // The server's own certificate is self-signed, so skip verification of it; + // the point under test is the certificate tea presents, not the one it gets. + withoutCert := &Login{URL: server.URL, Insecure: true} + withCert := &Login{URL: server.URL, Insecure: true, ClientCert: certPath, ClientKey: keyPath} + + _, err := doGet(t, withoutCert, server.URL) + require.Error(t, err, "server must reject a client that presents no certificate") + + resp, err := doGet(t, withCert, server.URL) + require.NoError(t, err, "client certificate should satisfy the server's mutual TLS requirement") + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func doGet(t *testing.T, login *Login, url string) (*http.Response, error) { + t.Helper() + + tlsConfig, err := login.TLSConfig() + require.NoError(t, err) + + client := &http.Client{Transport: httputil.WrapTransport(tlsConfig)} + return client.Get(url) +} + +// writeTestClientCert generates a self-signed certificate usable for client +// authentication and writes the PEM pair into a temp dir. +func writeTestClientCert(t *testing.T) (cert *x509.Certificate, certPath, keyPath string) { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "tea-test-client"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + + cert, err = x509.ParseCertificate(der) + require.NoError(t, err) + + keyDER, err := x509.MarshalECPrivateKey(key) + require.NoError(t, err) + + dir := t.TempDir() + certPath = filepath.Join(dir, "client.crt") + keyPath = filepath.Join(dir, "client.key") + + require.NoError(t, os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600)) + require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}), 0o600)) + + return cert, certPath, keyPath +} diff --git a/modules/context/context_login.go b/modules/context/context_login.go index 65535a75..b4f75e93 100644 --- a/modules/context/context_login.go +++ b/modules/context/context_login.go @@ -41,6 +41,8 @@ func GetLoginByEnvVar() *config.Login { Token: token, SSHHost: giteaInstanceSSHHost, Insecure: insecure, + ClientCert: os.Getenv("GITEA_INSTANCE_CLIENT_CERT"), + ClientKey: os.Getenv("GITEA_INSTANCE_CLIENT_KEY"), SSHKey: "", SSHCertPrincipal: "", SSHKeyFingerprint: "", diff --git a/modules/git/backend_test.go b/modules/git/backend_test.go index b514c5f0..7982ab6f 100644 --- a/modules/git/backend_test.go +++ b/modules/git/backend_test.go @@ -64,7 +64,7 @@ func TestCanSwitchBackends(t *testing.T) { require.Equal(t, "open:demo", repo.WorkTree()) require.Equal(t, "fake", CurrentBackendName()) - cloned, err := Clone("target", "https://example.com/repo.git", nil, 1, false) + cloned, err := Clone("target", "https://example.com/repo.git", nil, CloneOptions{Depth: 1}) require.NoError(t, err) require.Equal(t, "clone:target:https://example.com/repo.git", cloned.WorkTree()) } diff --git a/modules/git/cli_backend.go b/modules/git/cli_backend.go index d458de2c..d3fd5cee 100644 --- a/modules/git/cli_backend.go +++ b/modules/git/cli_backend.go @@ -38,11 +38,25 @@ func (b cliBackend) Open(path string) (RepositoryBackend, error) { return &cliRepository{workTree: strings.TrimSpace(out)}, nil } -func (b cliBackend) Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error) { - extraConfigs := make([]string, 0, 1) +// cloneTLSConfigs translates the TLS related clone options into "git -c" +// settings, so a clone against a server with a private CA or one demanding a +// client certificate uses the same material as tea's own API requests. +func cloneTLSConfigs(opts CloneOptions) []string { + configs := make([]string, 0, 3) if opts.Insecure { - extraConfigs = append(extraConfigs, "http.sslVerify=false") + configs = append(configs, "http.sslVerify=false") } + if opts.ClientCert != "" { + configs = append(configs, "http.sslCert="+opts.ClientCert) + } + if opts.ClientKey != "" { + configs = append(configs, "http.sslKey="+opts.ClientKey) + } + return configs +} + +func (b cliBackend) Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error) { + extraConfigs := cloneTLSConfigs(opts) args := []string{"clone"} if opts.Depth > 0 { diff --git a/modules/git/clone_tls_test.go b/modules/git/clone_tls_test.go new file mode 100644 index 00000000..a3732fb6 --- /dev/null +++ b/modules/git/clone_tls_test.go @@ -0,0 +1,42 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCloneTLSConfigs(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + opts CloneOptions + want []string + }{ + "defaults add nothing": { + opts: CloneOptions{}, + want: []string{}, + }, + "insecure disables verification": { + opts: CloneOptions{Insecure: true}, + want: []string{"http.sslVerify=false"}, + }, + "client certificate is passed to git": { + opts: CloneOptions{ClientCert: "/certs/client.crt", ClientKey: "/certs/client.key"}, + want: []string{"http.sslCert=/certs/client.crt", "http.sslKey=/certs/client.key"}, + }, + "client certificate combines with insecure": { + opts: CloneOptions{Insecure: true, ClientCert: "/certs/client.crt", ClientKey: "/certs/client.key"}, + want: []string{"http.sslVerify=false", "http.sslCert=/certs/client.crt", "http.sslKey=/certs/client.key"}, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, tc.want, cloneTLSConfigs(tc.opts)) + }) + } +} diff --git a/modules/git/network.go b/modules/git/network.go index aac21ced..25d98b2d 100644 --- a/modules/git/network.go +++ b/modules/git/network.go @@ -4,8 +4,8 @@ package git // Clone clones a repository using the active backend. -func Clone(path, remoteURL string, auth *AuthMethod, depth int, insecure bool) (*TeaRepo, error) { - backend, err := currentBackend().Clone(path, remoteURL, auth, CloneOptions{Depth: depth, Insecure: insecure}) +func Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (*TeaRepo, error) { + backend, err := currentBackend().Clone(path, remoteURL, auth, opts) if err != nil { return nil, err } diff --git a/modules/git/types.go b/modules/git/types.go index 41fe7f36..341d675f 100644 --- a/modules/git/types.go +++ b/modules/git/types.go @@ -28,6 +28,11 @@ type AuthMethod struct { type CloneOptions struct { Depth int Insecure bool + // ClientCert and ClientKey are paths to a PEM encoded TLS client + // certificate and its key, passed to git as http.sslCert/http.sslKey so + // clones from mutual-TLS protected servers succeed. + ClientCert string + ClientKey string } // RepositoryBackend is the backend abstraction used by TeaRepo. diff --git a/modules/interact/login.go b/modules/interact/login.go index d0c0d2e2..2464d2e6 100644 --- a/modules/interact/login.go +++ b/modules/interact/login.go @@ -255,7 +255,7 @@ func CreateLogin(ctx context.Context) error { printTitleAndContent("Check version of Gitea instance:", strconv.FormatBool(versionCheck)) } - return task.CreateLogin(ctx, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint, insecure, sshAgent, versionCheck, helper) + return task.CreateLogin(ctx, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint, insecure, "", "", sshAgent, versionCheck, helper) } func parseSSHPubkeySelection(display string) (sshKey, sshCertPrincipal, sshKeyFingerprint string, sshAgent bool, err error) { diff --git a/modules/task/login_create.go b/modules/task/login_create.go index 5b0b6720..f6d737fc 100644 --- a/modules/task/login_create.go +++ b/modules/task/login_create.go @@ -72,7 +72,7 @@ func HasGitCredentialHelper(login config.Login) bool { } // CreateLogin create a login to be stored in config -func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string, insecure, sshAgent, versionCheck, addHelper bool) error { +func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string, insecure bool, clientCert, clientKey string, sshAgent, versionCheck, addHelper bool) error { // checks ... // ... if we have a url if len(giteaURL) == 0 { @@ -120,6 +120,8 @@ func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, ssh URL: serverURL.String(), Token: token, Insecure: insecure, + ClientCert: clientCert, + ClientKey: clientKey, SSHKey: sshKey, SSHCertPrincipal: sshCertPrincipal, SSHKeyFingerprint: sshKeyFingerprint, diff --git a/modules/task/repo_clone.go b/modules/task/repo_clone.go index 185c6bf2..f2a34044 100644 --- a/modules/task/repo_clone.go +++ b/modules/task/repo_clone.go @@ -43,7 +43,12 @@ func RepoClone( path = repoName } - repo, err := local_git.Clone(path, originURL.String(), auth, depth, login.Insecure) + repo, err := local_git.Clone(path, originURL.String(), auth, local_git.CloneOptions{ + Depth: depth, + Insecure: login.Insecure, + ClientCert: login.ClientCert, + ClientKey: login.ClientKey, + }) if err != nil { return nil, err } diff --git a/tests/integration/helpers_test.go b/tests/integration/helpers_test.go index 2f131aae..800701dc 100644 --- a/tests/integration/helpers_test.go +++ b/tests/integration/helpers_test.go @@ -96,7 +96,7 @@ func createIntegrationLogin(t *testing.T) *config.Login { require.NotEmpty(t, integrationToken, "integration token setup failed") - require.NoError(t, task.CreateLogin(t.Context(), "integration", integrationToken, "", "", "", "", "", integrationGiteaURL, "", "", true, false, false, false)) + require.NoError(t, task.CreateLogin(t.Context(), "integration", integrationToken, "", "", "", "", "", integrationGiteaURL, "", "", true, "", "", false, false, false)) login, err := config.GetLoginByName("integration") require.NoError(t, err)