Add support for authentication via ssh certificates and pub/privatekey #442

Merged
6543 merged 11 commits from 42wim/tea:sshcert into main 2022-09-14 19:00:09 +00:00
7 changed files with 172 additions and 63 deletions
Showing only changes of commit 62a335557e - Show all commits

View File

@ -100,6 +100,5 @@ func runLoginAdd(ctx *cli.Context) error {
ctx.String("ssh-certificate-principal"),
ctx.String("ssh-key-agent-public-key"),
ctx.Bool("insecure"),
ctx.Bool("ssh-certificate"),
sshKeyAgent)
}

4
go.mod
View File

@ -66,4 +66,6 @@ require (
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
replace code.gitea.io/sdk/gitea => gitea.com/42wim/go-sdk/gitea v0.0.0-20220615192105-8c821f6c3419
replace code.gitea.io/sdk/gitea => gitea.com/42wim/go-sdk/gitea v0.0.0-20220616000741-57eaee10e1a9
6543 marked this conversation as resolved Outdated
Outdated
Review

sdk pull got merged

sdk pull got merged
Outdated
Review

ok pulled in upstream go-sdk back

ok pulled in upstream go-sdk back
// replace code.gitea.io/sdk/gitea => ../go-sdk-main/gitea

4
go.sum
View File

@ -1,7 +1,7 @@
code.gitea.io/gitea-vet v0.2.1 h1:b30by7+3SkmiftK0RjuXqFvZg2q4p68uoPGuxhzBN0s=
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
gitea.com/42wim/go-sdk/gitea v0.0.0-20220615192105-8c821f6c3419 h1:nr6/27FiO35Nyd23HS481wh59614hhupte0OSEn383A=
gitea.com/42wim/go-sdk/gitea v0.0.0-20220615192105-8c821f6c3419/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE=
gitea.com/42wim/go-sdk/gitea v0.0.0-20220616000741-57eaee10e1a9 h1:fKSuHO9hZIP/UtXRw2NzdX9JfHXcNLCXKMB1o7u93y8=
gitea.com/42wim/go-sdk/gitea v0.0.0-20220616000741-57eaee10e1a9/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE=
gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b h1:CLYsMGcGLohESQDMth+RgJ4cB3CCHToxnj0zBbvB3sE=
gitea.com/noerw/unidiff-comments v0.0.0-20201219085024-64aec5658f2b/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI=
github.com/AlecAivazis/survey/v2 v2.3.1 h1:lzkuHA60pER7L4eYL8qQJor4bUWlJe4V0gqAT19tdOA=

View File

@ -25,12 +25,11 @@ 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"`
SSHCert bool `yaml:"ssh_certificate"`
SSHCertPrincipal string `yaml:"ssh_certificate_principal"`
SSHKeyAgent bool `yaml:"ssh_key_agent"`
SSHKeyAgentPub string `yaml:"ssh_key_agent_pub"`
SSHKey string `yaml:"ssh_key"`
Insecure bool `yaml:"insecure"`
SSHCertPrincipal string `yaml:"ssh_certificate_principal"`
SSHAgent bool `yaml:"ssh_agent"`
SSHKeyFingerprint string `yaml:"ssh_key_agent_pub"`
// User is username from gitea
User string `yaml:"user"`
// Created is auto created unix timestamp
@ -181,11 +180,12 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
options = append(options, gitea.SetToken(l.Token), gitea.SetHTTPClient(httpClient))
if l.SSHCert {
options = append(options, gitea.UseSSHCert(l.SSHCertPrincipal))
if l.SSHCertPrincipal != "" {
options = append(options, gitea.UseSSHCert(l.SSHCertPrincipal, l.SSHKey))
}
if l.SSHKeyAgent {
options = append(options, gitea.UseSSHPubkey(l.SSHKeyAgentPub))
if l.SSHKeyFingerprint != "" {
options = append(options, gitea.UseSSHPubkey(l.SSHKeyFingerprint, l.SSHKey))
}
client, err := gitea.NewClient(l.URL, options...)

View File

@ -6,6 +6,7 @@ package interact
import (
"fmt"
"regexp"
"strings"
"code.gitea.io/tea/modules/task"
@ -15,10 +16,9 @@ import (
// CreateLogin create an login interactive
func CreateLogin() error {
var name, token, user, passwd, sshKey, giteaURL, sshCertPrincipal, sshKeyAgentPub string
var sshCert = false
var name, token, user, passwd, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string
var insecure = false
var sshKeyAgent = false
var sshAgent = false
promptI := &survey.Input{Message: "URL of Gitea instance: "}
if err := survey.AskOne(promptI, &giteaURL, survey.WithValidator(survey.Required)); err != nil {
@ -40,41 +40,15 @@ func CreateLogin() error {
return err
}
promptYN := &survey.Confirm{
Message: "Do you want to use your SSH certificate to login? (needs a running ssh-agent with certificate loaded)",
Default: false,
}
if err = survey.AskOne(promptYN, &sshCert); err != nil {
loginMethod, err := promptSelect("Login with: ", []string{"token", "ssh-key/certificate"}, "", "")
if err != nil {
return err
}
if sshCert {
promptI = &survey.Input{Message: "Which SSH certificate principal to use? (if not specified the first one detected will be used): "}
if err = survey.AskOne(promptI, &sshCertPrincipal); err != nil {
return err
}
}
if !sshCert {
promptYN := &survey.Confirm{
Message: "Do you want to use your SSH key to login? (needs a running ssh-agent with your key loaded)",
Default: false,
}
if err = survey.AskOne(promptYN, &sshKeyAgent); err != nil {
return err
}
if sshKeyAgent {
promptI = &survey.Input{Message: "Which SSH key ? (paste your public key or fingerprint): "}
if err = survey.AskOne(promptI, &sshKeyAgentPub, survey.WithValidator(survey.Required)); err != nil {
return err
}
}
}
if !sshCert && !sshKeyAgent {
switch loginMethod {
case "token":
var hasToken bool
promptYN = &survey.Confirm{
promptYN := &survey.Confirm{
Message: "Do you have an access token?",
Default: false,
}
@ -98,10 +72,43 @@ func CreateLogin() error {
return err
}
}
case "ssh-key/certificate":
promptI = &survey.Input{Message: "SSH Key/Certificate Path (leave empty for auto-discovery in ~/.ssh and ssh-agent):"}
if err := survey.AskOne(promptI, &sshKey); err != nil {
return err
}
if sshKey == "" {
sshKey, err = promptSelect("Select ssh-key: ", task.ListSSHPubkey(), "", "")
if err != nil {
return err
}
// ssh certificate
if strings.Contains(sshKey, "principals") {
sshCertPrincipal = regexp.MustCompile(`.*?principals: (.*?)[,|\s]`).FindStringSubmatch(sshKey)[1]
if strings.Contains(sshKey, "(ssh-agent)") {
sshAgent = true
sshKey = ""
} else {
sshKey = regexp.MustCompile(`(.*?)$`).FindStringSubmatch(sshKey)[1]
sshKey = strings.TrimSuffix(sshKey, ".pub")
}
} else {
sshKeyFingerprint = regexp.MustCompile(`(SHA256:.*?)\s`).FindStringSubmatch(sshKey)[1]
if strings.Contains(sshKey, "(ssh-agent)") {
sshAgent = true
sshKey = ""
} else {
sshKey = regexp.MustCompile(`\((.*?)\)$`).FindStringSubmatch(sshKey)[1]
sshKey = strings.TrimSuffix(sshKey, ".pub")
}
}
}
}
var optSettings bool
promptYN = &survey.Confirm{
promptYN := &survey.Confirm{
Message: "Set Optional settings: ",
Default: false,
}
@ -123,5 +130,5 @@ func CreateLogin() error {
}
}
return task.CreateLogin(name, token, user, passwd, sshKey, giteaURL, sshCertPrincipal, sshKeyAgentPub, insecure, sshCert, sshKeyAgent)
return task.CreateLogin(name, token, user, passwd, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint, insecure, sshAgent)
}

View File

@ -16,7 +16,7 @@ import (
)
// CreateLogin create a login to be stored in config
func CreateLogin(name, token, user, passwd, sshKey, giteaURL, sshCertPrincipal, sshKeyAgentPub string, insecure, sshCert, sshKeyAgent bool) error {
func CreateLogin(name, token, user, passwd, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string, insecure, sshAgent bool) error {
// checks ...
// ... if we have a url
if len(giteaURL) == 0 {
@ -32,7 +32,7 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL, sshCertPrincipal,
return fmt.Errorf("token already been used, delete login '%s' first", login.Name)
}
if !sshCert && !sshKeyAgent {
if !sshAgent && sshCertPrincipal == "" && sshKey == "" {
// .. if we have enough information to authenticate
if len(token) == 0 && (len(user)+len(passwd)) == 0 {
return fmt.Errorf("No token set")
@ -50,19 +50,18 @@ func CreateLogin(name, token, user, passwd, sshKey, giteaURL, sshCertPrincipal,
}
login := config.Login{
Name: name,
URL: serverURL.String(),
Token: token,
Insecure: insecure,
SSHKey: sshKey,
SSHCert: sshCert,
SSHKeyAgent: sshKeyAgent,
SSHCertPrincipal: sshCertPrincipal,
SSHKeyAgentPub: sshKeyAgentPub,
Created: time.Now().Unix(),
Name: name,
URL: serverURL.String(),
Token: token,
Insecure: insecure,
SSHKey: sshKey,
SSHCertPrincipal: sshCertPrincipal,
SSHKeyFingerprint: sshKeyFingerprint,
SSHAgent: sshAgent,
Created: time.Now().Unix(),
}
if len(token) == 0 && !sshCert && !sshKeyAgent {
if len(token) == 0 && sshCertPrincipal == "" && !sshAgent && sshKey == "" {
if login.Token, err = generateToken(login, user, passwd); err != nil {
return err
}

View File

@ -0,0 +1,102 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package task
import (
"io/ioutil"
"path/filepath"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/utils"
"golang.org/x/crypto/ssh"
)
func ListSSHPubkey() []string {
var keys []string
keys = append(keys, getAgentKeys()...)
keys = append(keys, getLocalKeys()...)
return keys
}
func getAgentKeys() []string {
ag, err := gitea.GetAgent()
if err != nil {
return []string{}
}
akeys, err := ag.List()
if err != nil {
return nil
}
var keys []string
for _, akey := range akeys {
if key := parseKeys([]byte(akey.String()), "ssh-agent"); key != "" {
keys = append(keys, key)
}
}
return keys
}
func getLocalKeys() []string {
var keys []string
// enumerate ~/.ssh/*.pub files
glob, err := utils.AbsPathWithExpansion("~/.ssh/*.pub")
if err != nil {
return []string{}
}
localPubkeyPaths, err := filepath.Glob(glob)
if err != nil {
return []string{}
}
// parse each local key with present privkey & compare fingerprints to online keys
for _, pubkeyPath := range localPubkeyPaths {
var pubkeyFile []byte
pubkeyFile, err = ioutil.ReadFile(pubkeyPath)
if err != nil {
continue
}
if key := parseKeys(pubkeyFile, pubkeyPath); key != "" {
keys = append(keys, key)
}
}
return keys
}
func parseKeys(pkinput []byte, sshPath string) string {
pkey, comment, _, _, err := ssh.ParseAuthorizedKey(pkinput)
if err != nil {
return ""
}
if strings.Contains(pkey.Type(), "cert-v01@openssh.com") {
principals := pkey.(*ssh.Certificate).ValidPrincipals
return ssh.FingerprintSHA256(pkey) + " " + pkey.Type() + " " + comment +
" - principals: " + strings.Join(principals, ",") + " (" + sshPath + ")"
} else {
return ssh.FingerprintSHA256(pkey) + " " + pkey.Type() + " " + comment + " (" + sshPath + ")"
}
}
func getCertPrincipals(pkey ssh.PublicKey) []string {
var principals []string
if cert, ok := pkey.(*ssh.Certificate); ok {
for _, principal := range cert.ValidPrincipals {
principals = append(principals, principal)
}
}
return principals
}