Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add skel plugin for code sharing #473

Merged
merged 19 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,20 +116,6 @@ builds:
binary: plugins/failtoban
tags:
- full
- id: plugin_workingdirbykey
env:
- CGO_ENABLED=0
goos:
- linux
- windows
# - darwin
goarch:
- amd64
- arm64
main: ./plugin/workingdirbykey
binary: plugins/workingdirbykey
tags:
- full
- id: plugin_totp
env:
- CGO_ENABLED=0
Expand Down
312 changes: 312 additions & 0 deletions libplugin/skel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
package libplugin

import (
"bytes"
"crypto/subtle"
"fmt"
"time"

"github.com/patrickmn/go-cache"
"golang.org/x/crypto/ssh"

log "github.com/sirupsen/logrus"
)

type SkelPlugin struct {
cache *cache.Cache
listPipe func(ConnMetadata) ([]SkelPipe, error)
}

func NewSkelPlugin(listPipe func(ConnMetadata) ([]SkelPipe, error)) *SkelPlugin {
return &SkelPlugin{
cache: cache.New(1*time.Minute, 10*time.Minute),
listPipe: listPipe,
}
}

type SkelPipe interface {
From() []SkelPipeFrom
}

type SkelPipeFrom interface {
MatchConn(conn ConnMetadata) (SkelPipeTo, error)
}

type SkelPipeFromPassword interface {
SkelPipeFrom

TestPassword(conn ConnMetadata, password []byte) (bool, error)
}

type SkelPipeFromPublicKey interface {
SkelPipeFrom

AuthorizedKeys(conn ConnMetadata) ([]byte, error)
TrustedUserCAKeys(conn ConnMetadata) ([]byte, error)
}

type SkelPipeTo interface {
Host(conn ConnMetadata) string
User(conn ConnMetadata) string
IgnoreHostKey(conn ConnMetadata) bool
KnownHosts(conn ConnMetadata) ([]byte, error)
}

type SkelPipeToPassword interface {
SkelPipeTo

OverridePassword(conn ConnMetadata) ([]byte, error)
}

type SkelPipeToPrivateKey interface {
SkelPipeTo

PrivateKey(conn ConnMetadata) ([]byte, []byte, error)
}

func (p *SkelPlugin) CreateConfig() *SshPiperPluginConfig {
return &SshPiperPluginConfig{
NextAuthMethodsCallback: p.SupportedMethods,
PasswordCallback: p.PasswordCallback,
PublicKeyCallback: p.PublicKeyCallback,
VerifyHostKeyCallback: p.VerifyHostKeyCallback,
}
}

func (p *SkelPlugin) SupportedMethods(conn ConnMetadata) ([]string, error) {
set := make(map[string]bool)

pipes, err := p.listPipe(conn)
if err != nil {
return nil, err
}

for _, pipe := range pipes {
for _, from := range pipe.From() {

switch from.(type) {
case SkelPipeFromPublicKey:
set["publickey"] = true
default:
set["password"] = true
}

if len(set) == 2 {
break
}
}
}

var methods []string
for k := range set {
methods = append(methods, k)
}

return methods, nil
}

func (p *SkelPlugin) VerifyHostKeyCallback(conn ConnMetadata, hostname, netaddr string, key []byte) error {
item, found := p.cache.Get(conn.UniqueID())
if !found {
log.Warnf("connection expired when verifying host key for conn [%v]", conn.UniqueID())
return fmt.Errorf("connection expired")
}

to := item.(SkelPipeTo)

data, err := to.KnownHosts(conn)
if err != nil {
return err
}

return VerifyHostKeyFromKnownHosts(bytes.NewBuffer(data), hostname, netaddr, key)
}

func (p *SkelPlugin) match(conn ConnMetadata, verify func(SkelPipeFrom) (bool, error)) (SkelPipeFrom, SkelPipeTo, error) {
pipes, err := p.listPipe(conn)
if err != nil {
return nil, nil, err
}

for _, pipe := range pipes {
for _, from := range pipe.From() {

to, err := from.MatchConn(conn)
if err != nil {
return nil, nil, err
}

if to == nil {
continue
}

ok, err := verify(from)
if err != nil {
return nil, nil, err
}

if ok {
return from, to, nil
}
}
}

return nil, nil, fmt.Errorf("no matching pipe for username [%v] found", conn.User())
}

func (p *SkelPlugin) PasswordCallback(conn ConnMetadata, password []byte) (*Upstream, error) {
_, to, err := p.match(conn, func(from SkelPipeFrom) (bool, error) {
frompass, ok := from.(SkelPipeFromPassword)

if !ok {
return false, nil
}

return frompass.TestPassword(conn, password)
})
if err != nil {
return nil, err
}

u, err := p.createUpstream(conn, to)
if err != nil {
return nil, err
}

toPass, ok := to.(SkelPipeToPassword)
if !ok {
return nil, fmt.Errorf("pipe to does not support password")
}

overridepassword, err := toPass.OverridePassword(conn)
if err != nil {
return nil, err
}

if overridepassword != nil {
u.Auth = CreatePasswordAuth(overridepassword)
} else {
u.Auth = CreatePasswordAuth(password)
}

return u, nil

}

func (p *SkelPlugin) PublicKeyCallback(conn ConnMetadata, publicKey []byte) (*Upstream, error) {

pubKey, err := ssh.ParsePublicKey(publicKey)
if err != nil {
return nil, err
}

pkcert, isCert := pubKey.(*ssh.Certificate)
if isCert {
// ensure cert is valid first

if pkcert.CertType != ssh.UserCert {
return nil, fmt.Errorf("only user certificates are supported, cert type: %v", pkcert.CertType)
}

certChecker := ssh.CertChecker{}
if err := certChecker.CheckCert(conn.User(), pkcert); err != nil {
return nil, err
}
}

_, to, err := p.match(conn, func(from SkelPipeFrom) (bool, error) {
// verify public key
fromPubKey, ok := from.(SkelPipeFromPublicKey)
if !ok {
return false, nil
}

verified := false

if isCert {
rest, err := fromPubKey.TrustedUserCAKeys(conn)
if err != nil {
return false, err
}

log.Debugf("trusted user ca keys: %v", rest)

var trustedca ssh.PublicKey
for len(rest) > 0 {
trustedca, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
if err != nil {
return false, err
}

if subtle.ConstantTimeCompare(trustedca.Marshal(), pkcert.SignatureKey.Marshal()) == 1 {
verified = true
break
}
}
} else {
rest, err := fromPubKey.AuthorizedKeys(conn)
if err != nil {
return false, err
}

var authedPubkey ssh.PublicKey
for len(rest) > 0 {
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
if err != nil {
return false, err
}

if subtle.ConstantTimeCompare(authedPubkey.Marshal(), publicKey) == 1 {
verified = true
break
}
}
}

return verified, nil
})

if err != nil {
return nil, err
}

u, err := p.createUpstream(conn, to)
if err != nil {
return nil, err
}

toPrivateKey, ok := to.(SkelPipeToPrivateKey)
if !ok {
return nil, fmt.Errorf("pipe to does not support private key")
}

priv, cert, err := toPrivateKey.PrivateKey(conn)
if err != nil {
return nil, err
}

u.Auth = CreatePrivateKeyAuth(priv, cert)

return u, nil
}

func (p *SkelPlugin) createUpstream(conn ConnMetadata, to SkelPipeTo) (*Upstream, error) {
host, port, err := SplitHostPortForSSH(to.Host(conn))
if err != nil {
return nil, err
}

user := to.User(conn)
if user == "" {
user = conn.User()
}

p.cache.SetDefault(conn.UniqueID(), to)

return &Upstream{
Host: host,
Port: int32(port), // port is already checked to be within int32 range in SplitHostPortForSSH
UserName: user,
IgnoreHostKey: to.IgnoreHostKey(conn),
}, err
}
5 changes: 3 additions & 2 deletions libplugin/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@ func SplitHostPortForSSH(addr string) (host string, port int, err error) {
h, p, err := net.SplitHostPort(host)
if err == nil {
host = h
port, err = strconv.Atoi(p)

var parsedPort int64
parsedPort, err = strconv.ParseInt(p, 10, 32)
if err != nil {
return
}
port = int(parsedPort)
} else if host != "" {
// test valid after concat :22
if _, _, err = net.SplitHostPort(host + ":22"); err == nil {
Expand Down
Loading
Loading