Initial import

This commit is contained in:
2025-10-10 10:05:13 +02:00
parent 3effc1597b
commit b96b6e7f8f
164 changed files with 5473 additions and 0 deletions

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"gopls": {
"formatting.local": "git.maze.io/maze/conduit"
},
"CodeGPT.apiKey": "Ollama"
}

96
auth/auth.go Normal file
View File

@@ -0,0 +1,96 @@
package auth
import (
"bytes"
"errors"
"strings"
"time"
"golang.org/x/crypto/ssh"
"git.maze.io/maze/conduit/logger"
)
var ErrUnauthorized = errors.New("unauthorized")
// Provider implements zero or more of the [Password], [PublicKey] interfaces.
type Provider any
// Password authenticator.
type Password interface {
VerifyPassword(meta ssh.ConnMetadata, password string) (Principal, error)
}
// Token authenticator.
type Token interface {
Instruction() string
Prompt() string
// Multiline accepts multiline input and wait for the user to supply
// an empty line.
Multiline() bool
VerifyToken(meta ssh.ConnMetadata, token string) (Principal, error)
}
// PublicKey authenticator.
type PublicKey interface {
VerifyPublicKey(meta ssh.ConnMetadata, publicKey ssh.PublicKey) (Principal, error)
}
type CertificateAuthority struct {
checker *ssh.CertChecker
keys []ssh.PublicKey
blob [][]byte
user bool
host bool
}
func NewUserCertificateAuthority(keys ...ssh.PublicKey) *CertificateAuthority {
var blob [][]byte
for _, key := range keys {
blob = append(blob, key.Marshal())
}
auth := &CertificateAuthority{
checker: &ssh.CertChecker{
Clock: time.Now,
},
keys: keys,
blob: blob,
user: true,
}
auth.checker.IsUserAuthority = auth.isUserAuthority
return auth
}
func (auth *CertificateAuthority) isUserAuthority(key ssh.PublicKey) bool {
if !auth.user {
return false
}
blob := key.Marshal()
for _, other := range auth.blob {
if bytes.Equal(blob, other) {
return true
}
}
return false
}
func (auth *CertificateAuthority) VerifyPublicKey(meta ssh.ConnMetadata, publicKey ssh.PublicKey) (Principal, error) {
log := logger.StandardLog.Values(logger.Values{
"client": meta.RemoteAddr().String(),
"key": strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey))),
"user": meta.User(),
"version": string(meta.ClientVersion()),
})
log.Debug("Verifying user certificate")
_, err := auth.checker.Authenticate(meta, publicKey)
if err != nil {
return nil, err
}
cert := publicKey.(*ssh.Certificate)
return UserCertificatePrincipal{cert}, nil
}

104
auth/file.go Normal file
View File

@@ -0,0 +1,104 @@
package auth
import (
"bufio"
"bytes"
"fmt"
"os"
"strings"
"github.com/go-crypt/crypt"
"golang.org/x/crypto/ssh"
"git.maze.io/maze/conduit/logger"
"git.maze.io/maze/conduit/ssh/sshutil"
)
type passwordFile map[string]string
func PasswordFile(name string) (Password, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
s := bufio.NewScanner(f)
p := make(passwordFile)
for s.Scan() {
line := s.Text()
if len(line) == 0 || line[0] == '#' || strings.HasPrefix(line, "//") {
continue
}
if i := strings.IndexByte(line, ':'); i > 0 {
p[line[:i]] = line[i+1:]
}
}
return p, s.Err()
}
func (auth passwordFile) VerifyPassword(meta ssh.ConnMetadata, password string) (Principal, error) {
encodedDigest := auth[meta.User()]
if ok, err := crypt.CheckPasswordWithPlainText(password, encodedDigest); err != nil {
return nil, err
} else if !ok {
return nil, ErrUnauthorized
}
return MakePasswordPrincipal(meta), nil
}
type publibKeyFile map[string][]ssh.PublicKey
func PublicKeyFile(name string) (PublicKey, error) {
log := logger.StandardLog.Value("path", name)
log.Trace("Parsing public keys file")
f, err := os.Open(name)
if err != nil {
return nil, err
}
var (
s = bufio.NewScanner(f)
p = make(publibKeyFile)
lineno int
)
for s.Scan() {
line := s.Text()
lineno++
if len(line) == 0 || line[0] == '#' || strings.HasPrefix(line, "//") {
continue
}
if i := strings.IndexByte(line, ':'); i > 0 {
//k, err := ssh.ParsePublicKey([]byte(line[i+1:]))
k, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line[i+1:]))
if err != nil {
return nil, fmt.Errorf("auth: invalid public key in %s:%d: %w", name, lineno, err)
}
log.Values(logger.Values{
"user": line[:i],
"key": k.Type(),
}).Trace("Parsed authorized public key")
p[line[:i]] = append(p[line[:i]], k)
}
}
return p, s.Err()
}
func (auth publibKeyFile) VerifyPublicKey(meta ssh.ConnMetadata, key ssh.PublicKey) (Principal, error) {
log := logger.StandardLog.Values(logger.Values{
"client": meta.RemoteAddr().String(),
"key_type": sshutil.KeyType(key),
"key_bits": sshutil.KeyBits(key),
"user": meta.User(),
"version": string(meta.ClientVersion()),
})
log.Debug("Verifying user public key")
blob := key.Marshal()
keys := auth[meta.User()]
for _, other := range keys {
if bytes.Equal(other.Marshal(), blob) {
return MakePublicKeyPrincipal(meta, key), nil
}
}
return nil, ErrUnauthorized
}

134
auth/file_test.go Normal file
View File

@@ -0,0 +1,134 @@
package auth
import (
"net"
"path/filepath"
"testing"
"golang.org/x/crypto/ssh"
)
var testAddr = &net.TCPAddr{
IP: net.ParseIP("127.1.2.3"),
Port: 22,
}
type testConnMetadata struct {
user string
sessionID []byte
clientVersion string
serverVersion string
laddr, raddr net.Addr
}
func (t testConnMetadata) User() string { return t.user }
func (t testConnMetadata) SessionID() []byte { return t.sessionID }
func (t testConnMetadata) ClientVersion() []byte { return []byte(t.clientVersion) }
func (t testConnMetadata) ServerVersion() []byte { return []byte(t.serverVersion) }
func (t testConnMetadata) RemoteAddr() net.Addr { return t.raddr }
func (t testConnMetadata) LocalAddr() net.Addr { return t.laddr }
var _ ssh.ConnMetadata = (*testConnMetadata)(nil)
func TestPasswordFile(t *testing.T) {
a, err := PasswordFile(filepath.Join("testdata", "passwd"))
if err != nil {
t.Fatal(err)
}
tests := []struct {
Username string
Password string
}{
{"example", "example"},
{"bcrypt", "example"},
}
for _, test := range tests {
t.Run(test.Username, func(it *testing.T) {
p, err := a.VerifyPassword(testConnMetadata{user: test.Username}, test.Password)
if err != nil {
it.Error(err)
} else {
it.Logf("%s: %s (%T)", p.Type(), p.Identity(), p)
}
})
}
}
func TestPublicKeyFile(t *testing.T) {
a, err := PublicKeyFile(filepath.Join("testdata", "pubkey"))
if err != nil {
t.Fatal(err)
}
tests := []struct {
Name string
Username string
PublicKey string
}{
{"single/ed25519", "test_a", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFo1lt6lEk+1VUrMbhlaVpkI0p1TFUGujHaKKn7+VoGb"},
{"dual/ed25519", "test_b", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICA9dQjNeX3eBvkOXJN+nJm1C2W9UtRiLbK9O87Mjkir"},
{"dual/rsa", "test_b", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFq82Pfsg7KjTU5LN4jikxITDQhCWB3TFxQdXTgYtKt40+gv88hZkemM1MYTzR30bUX/zcRsioUSwr3u7/2La7ti+BoilsHjrEx4w+nxNGCCe8D3M6K5Xi8MPL2AqbXFqkPSEpX+psrs+qILfNhs1lWAsN7GLP0cTIxPynFNECwJnUlleN0hsn8N8bQCoUInZQGmHwIHq62H+3IPbv7Vko3J0Zrqqo4OqfeV5BA0By7ZP+2Jd9ZsLJ2efaiALcs6oTk0v95wVQ36wp605x9ePYg6zHzIZDfpA400RqeuiZF5jpiG7q3eb0+CysfMbU0BpfeHmCq15PFYqre8HKAJZ3"},
}
for _, test := range tests {
t.Run(test.Username, func(it *testing.T) {
k, _, _, _, err := ssh.ParseAuthorizedKey([]byte(test.PublicKey))
if err != nil {
it.Fatal(err)
}
p, err := a.VerifyPublicKey(testConnMetadata{
user: test.Username,
laddr: testAddr,
raddr: testAddr,
}, k)
if err != nil {
it.Error(err)
} else {
it.Logf("%s: %s (%T)", p.Type(), p.Identity(), p)
}
})
}
}
func BenchmarkPasswordFileHits(b *testing.B) {
a, err := PasswordFile(filepath.Join("testdata", "passwd"))
if err != nil {
b.Fatal(err)
}
c := testConnMetadata{user: "example"}
b.ResetTimer()
for b.Loop() {
a.VerifyPassword(c, "example")
}
}
func BenchmarkPasswordFileMissPassword(b *testing.B) {
a, err := PasswordFile(filepath.Join("testdata", "passwd"))
if err != nil {
b.Fatal(err)
}
c := testConnMetadata{user: "example"}
b.ResetTimer()
for b.Loop() {
a.VerifyPassword(c, "invalid")
}
}
func BenchmarkPasswordFileMissPrincipal(b *testing.B) {
a, err := PasswordFile(filepath.Join("testdata", "passwd"))
if err != nil {
b.Fatal(err)
}
c := testConnMetadata{user: "invalid"}
b.ResetTimer()
for b.Loop() {
a.VerifyPassword(c, "example")
}
}

329
auth/handler.go Normal file
View File

@@ -0,0 +1,329 @@
package auth
import (
"bytes"
"errors"
"net"
"strings"
"time"
"golang.org/x/crypto/ssh"
"git.maze.io/maze/conduit/logger"
"git.maze.io/maze/conduit/policy"
"git.maze.io/maze/conduit/policy/input"
)
// None accepts no authentication, this is only useful for debugging/testing.
type None struct{}
func (None) HandleAccept(conn net.Conn, serverConfig *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
config := new(ssh.ServerConfig)
if serverConfig != nil {
*config = *serverConfig
}
config.NoClientAuth = true
return ssh.NewServerConn(conn, config)
}
// UserCertificate offers user certificate based authentication.
type UserCertificate struct {
// Loader is the policy loader.
Loader policy.Loader
// CA is our trusted user certificate authority public keys.
CA []ssh.PublicKey
// VerifyCallback can optionally be used to perform additional checks on the certificate.
VerifyCallback func(*ssh.Certificate) bool
}
func (auth UserCertificate) HandleAccept(conn net.Conn, serverConfig *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
log := logger.StandardLog.Value("client", conn.RemoteAddr().String())
policy, err := auth.Loader.LoadPolicy("conduit/auth/user_certificate")
if err != nil {
return nil, nil, nil, err
}
_ = policy
checker := &ssh.CertChecker{
Clock: time.Now,
}
checker.IsUserAuthority = func(key ssh.PublicKey) bool {
blob := key.Marshal()
for _, ca := range auth.CA {
if bytes.Equal(ca.Marshal(), blob) {
return true
}
}
return false
}
config := new(ssh.ServerConfig)
if serverConfig != nil {
*config = *serverConfig
}
config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if _, err := checker.Authenticate(conn, key); err != nil {
log.Err(err).Debug("Certificate check failed")
return nil, err
}
cert := key.(*ssh.Certificate)
principal := UserCertificatePrincipal{Certificate: cert}
var (
query = "data." + policy.Package()
input = struct {
Conn *input.ConnMetadata `json:"conn"`
Principal Principal `json:"principal"`
}{input.NewConnMetadata(conn), principal}
result struct {
Permit bool `mapstructure:"permit"`
}
)
if err = policy.Query(query, input, &result); err != nil {
log.Err(err).Warn("Policy query returned error")
return nil, err
}
if !result.Permit {
log.Debug("Policy rejected certificate")
return nil, ErrUnauthorized
}
return &cert.Permissions, nil
}
return ssh.NewServerConn(conn, config)
}
// MultiFactor offers policy-based MFA (multi factor authentication).
type MultiFactor struct {
// Loader for the policy rules.
Loader policy.Loader
// PublicKey enables public key authentication.
PublicKey PublicKey
// UserCA enables user certificate based authentication.
UserCA []ssh.PublicKey
// Password enables password authentication.
Password Password
// Token enabled token authentication.
Token Token
}
func (mfa *MultiFactor) HandleAccept(conn net.Conn, serverConfig *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
log := logger.StandardLog.Value("client", conn.RemoteAddr().String())
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
log.Err(err).Error("Recovered from panic in accept handler! This is a bug!")
}
}
}()
rules, err := mfa.Loader.LoadPolicy("conduit/auth/mfa")
if err != nil {
return nil, nil, nil, err
}
config := new(ssh.ServerConfig)
if serverConfig != nil {
*config = *serverConfig
}
var (
meta ssh.ConnMetadata
principals []Principal
tokenAuth = mfa.Token
passwordAuth = mfa.Password
publicKeyAuth = mfa.PublicKey
certificateAuth []ssh.PublicKey
)
checkPolicy := func() (perms *ssh.Permissions, err error) {
var (
query = "data." + rules.Package()
input = struct {
Conn *input.ConnMetadata `json:"conn"`
Principals []Principal `json:"principals"`
}{input.NewConnMetadata(meta), principals}
result struct {
Permit bool `mapstructure:"permit"`
PermitPassword bool `mapstructure:"permit_password"`
PermitToken bool `mapstructure:"permit_token"`
PermitCertificate bool `mapstructure:"permit_certificate"`
PermitPublicKey bool `mapstructure:"permit_publickey"`
}
)
if err = rules.Query(query, input, &result); err != nil {
log.Err(err).Warn("Policy query returned error")
return
}
if !result.PermitPassword && passwordAuth != nil {
log.Debug("Policy disabled password authentication")
passwordAuth = nil
}
if passwordAuth == nil && config.PasswordCallback != nil {
// Disable password authentication.
log.Debug("Policy disabled password callback")
config.PasswordCallback = nil
}
if !result.PermitToken && tokenAuth != nil {
log.Debug("Policy disabled token authentication")
tokenAuth = nil
}
if tokenAuth == nil && config.KeyboardInteractiveCallback != nil {
// Disable token authentication.
log.Debug("Policy disabled token authentication")
config.KeyboardInteractiveCallback = nil
}
if !result.PermitCertificate && len(certificateAuth) != 0 {
log.Debug("Policy disabled certificate authentication")
certificateAuth = nil
}
if !result.PermitPublicKey && publicKeyAuth != nil {
log.Debug("Policy disabled public key authentication")
publicKeyAuth = nil
}
if publicKeyAuth == nil && len(certificateAuth) == 0 && config.PublicKeyCallback != nil {
// Disable pubkey authentication.
log.Debug("Policy disabled public key callback")
config.PublicKeyCallback = nil
}
if !result.Permit {
log.Debug("Policy rejected principals; not authorized")
return nil, ErrUnauthorized
}
if len(principals) == 0 {
log.Debug("No valid principals; not authorized")
return nil, ErrUnauthorized
}
attr := principals[0].Attributes()
return &ssh.Permissions{
CriticalOptions: attr.Options,
Extensions: attr.Extensions,
}, nil
}
if mfa.PublicKey != nil || len(mfa.UserCA) > 0 {
log = log.Values(logger.Values{
"permit_pubkey": mfa.PublicKey != nil,
"permit_certificate": len(mfa.UserCA) > 0,
})
log.Trace("MFA enabling public key authentication")
var checker *ssh.CertChecker
if len(mfa.UserCA) > 0 {
checker = &ssh.CertChecker{
Clock: time.Now,
}
checker.IsUserAuthority = func(key ssh.PublicKey) bool {
blob := key.Marshal()
for _, ca := range mfa.UserCA {
if bytes.Equal(ca.Marshal(), blob) {
return true
}
}
return false
}
}
config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
var (
principal Principal
errs []error
)
if checker != nil {
if _, err := checker.Authenticate(conn, key); err != nil {
log.Err(err).Debug("Certificate check failed")
errs = append(errs, err)
} else {
principal = UserCertificatePrincipal{Certificate: key.(*ssh.Certificate)}
}
}
if principal == nil && mfa.PublicKey != nil {
if _, err := mfa.PublicKey.VerifyPublicKey(conn, key); err != nil {
log.Err(err).Debug("Public key check failed")
errs = append(errs, err)
} else {
principal = PublicKeyPrincipal{User: conn.User(), PublicKey: key}
}
}
if principal == nil {
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return nil, ErrUnauthorized
}
meta = conn
principals = append(principals, principal)
return checkPolicy()
}
}
if mfa.Password != nil {
log.Trace("MFA enabling password authentication")
config.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
if _, err := mfa.Password.VerifyPassword(conn, string(password)); err != nil {
return nil, err
}
meta = conn
principals = append(principals, PasswordPrincipal{User: conn.User()})
return checkPolicy()
}
}
if mfa.Token != nil {
log.Trace("MFA enabling token authentication")
config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
var (
multiline = mfa.Token.Multiline()
token string
)
for {
answers, err := challenge("", mfa.Token.Instruction(), []string{"Token: "}, []bool{false})
if err != nil {
return nil, err
} else if len(answers) != 1 {
return nil, ErrUnauthorized
}
if multiline {
if answers[0] == "" {
break
}
token += strings.TrimSpace(answers[0])
} else {
token = answers[0]
break
}
}
if _, err := mfa.Token.VerifyToken(conn, token); err != nil {
return nil, err
}
meta = conn
principals = append(principals, TokenPrincipal{User: conn.User()})
return checkPolicy()
}
}
return ssh.NewServerConn(conn, config)
}

148
auth/principal.go Normal file
View File

@@ -0,0 +1,148 @@
package auth
import (
"encoding/json"
"golang.org/x/crypto/ssh"
"git.maze.io/maze/conduit/policy/input"
)
const (
TypeCertificate = "certificate"
TypeToken = "token"
TypePassword = "password"
TypePublicKey = "publickey"
)
type Attributes struct {
Groups []string `json:"groups"`
Principals []string `json:"principals"`
Options map[string]string `json:"options"`
Extensions map[string]string `json:"extensions"`
Source any `json:"source"`
}
func MakeAttributes() Attributes {
return Attributes{
Groups: make([]string, 0),
Principals: make([]string, 0),
Options: make(map[string]string),
Extensions: make(map[string]string),
}
}
type Principal interface {
Type() string // "user", "host", "key", etc.
// Identity is the user identity.
Identity() string
// Attributes for the principal.
Attributes() Attributes
}
type PasswordPrincipal struct {
User string
Attr Attributes
}
func MakePasswordPrincipal(meta ssh.ConnMetadata) PasswordPrincipal {
attr := MakeAttributes()
attr.Source = meta
return PasswordPrincipal{
User: meta.User(),
Attr: attr,
}
}
func (p PasswordPrincipal) Type() string { return TypePassword }
func (p PasswordPrincipal) Identity() string { return p.User }
func (p PasswordPrincipal) Attributes() Attributes { return p.Attr }
func (p PasswordPrincipal) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Kind string `json:"type"`
User string `json:"identity"`
Attr Attributes `json:"attr"`
}{
p.Type(),
p.Identity(),
p.Attributes(),
})
}
type TokenPrincipal struct {
User string `json:"identity"`
Attr Attributes `json:"attr"`
}
func (p TokenPrincipal) Type() string { return TypeToken }
func (p TokenPrincipal) Identity() string { return p.User }
func (p TokenPrincipal) Attributes() Attributes { return p.Attr }
func (p TokenPrincipal) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Kind string `json:"type"`
User string `json:"identity"`
Attr Attributes `json:"attr"`
}{
p.Type(),
p.Identity(),
p.Attributes(),
})
}
type PublicKeyPrincipal struct {
User string
ssh.PublicKey
}
func MakePublicKeyPrincipal(meta ssh.ConnMetadata, key ssh.PublicKey) PublicKeyPrincipal {
attr := MakeAttributes()
attr.Source = meta
return PublicKeyPrincipal{
User: meta.User(),
PublicKey: key,
}
}
func (p PublicKeyPrincipal) Type() string { return TypePublicKey }
func (p PublicKeyPrincipal) Identity() string { return p.User }
func (p PublicKeyPrincipal) Attributes() Attributes { return MakeAttributes() }
func (p PublicKeyPrincipal) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Kind string `json:"type"`
User string `json:"identity"`
Attr Attributes `json:"attr"`
}{
p.Type(),
p.Identity(),
p.Attributes(),
})
}
type UserCertificatePrincipal struct {
*ssh.Certificate
}
func (cert UserCertificatePrincipal) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Kind string `json:"type"`
User string `json:"identity"`
Attr Attributes `json:"attr"`
}{
cert.Type(),
cert.Identity(),
cert.Attributes(),
})
}
func (cert UserCertificatePrincipal) Type() string { return TypeCertificate }
func (cert UserCertificatePrincipal) Identity() string { return cert.KeyId }
func (cert UserCertificatePrincipal) Attributes() Attributes {
return Attributes{
Principals: cert.ValidPrincipals,
Options: cert.CriticalOptions,
Extensions: cert.Extensions,
Source: input.NewCertificate(cert.Certificate),
}
}

105
auth/system_linux.go Normal file
View File

@@ -0,0 +1,105 @@
//go:build linux
package auth
import (
"bytes"
"errors"
"os"
"os/user"
"path/filepath"
"github.com/msteinert/pam"
"golang.org/x/crypto/ssh"
)
type system struct{}
type systemPrincipal struct {
*user.User
attributes Attributes
}
func newSystemPrincipal(name string) (*systemPrincipal, error) {
u, err := user.Lookup(name)
if err != nil {
return nil, err
}
a := Attributes{
Custom: make(map[string]any),
}
if gids, err := u.GroupIds(); err == nil {
a.Custom["groups"] = gids
for _, gid := range gids {
if g, err := user.LookupGroupId(gid); err == nil {
a.Groups = append(a.Groups, g.Name)
}
}
}
return &systemPrincipal{
User: u,
attributes: a,
}, nil
}
func (u systemPrincipal) Type() string {
return "user"
}
func (u systemPrincipal) Identity() string {
return u.Name
}
func (u systemPrincipal) Attributes() Attributes {
return u.attributes
}
func SystemPassword() Password {
return system{}
}
func (system) VerifyPassword(username, password string) (Principal, error) {
t, err := pam.StartFunc("sshd", username, func(s pam.Style, msg string) (string, error) {
switch s {
case pam.PromptEchoOff:
return password, nil
case pam.PromptEchoOn:
return username, nil
default:
return "", errors.New("unrecognized message style")
}
})
if err != nil {
return nil, err
}
if err = t.Authenticate(0); err != nil {
return nil, err
}
return newSystemPrincipal(username)
}
func (system) VerifyPublicKey(username string, key ssh.PublicKey) (Principal, error) {
p, err := newSystemPrincipal(username)
if err != nil {
return nil, err
}
rest, err := os.ReadFile(filepath.Join(p.HomeDir, ".ssh", "authorized_keys"))
if err != nil {
return nil, err
}
for len(rest) > 0 {
var out ssh.PublicKey
if out, _, _, rest, err = ssh.ParseAuthorizedKey(rest); err != nil {
return nil, err
}
if bytes.Equal(out.Marshal(), key.Marshal()) {
return p, nil
}
}
return nil, ErrAuthorized
}

3
auth/testdata/passwd vendored Normal file
View File

@@ -0,0 +1,3 @@
# This line is ignored
example:$argon2id$v=19$m=2097152,t=1,p=4$BjVeoTI4ntTQc0WkFQdLWg$OAUnkkyx5STI0Ixl+OSpv4JnI6J1TYWKuCuvIbUGHTY
bcrypt:$2y$10$jeTxJGC9SZ1KZgbBZoZeseq0H8Gi1yqxvGX43YnwodTYeUxvl6TPK

5
auth/testdata/pubkey vendored Normal file
View File

@@ -0,0 +1,5 @@
maze:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIjJv5YZX22u40Wr+DRHH6jnCjxqk1u7rvNc6ALsCncK
maze:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCce9LTIeQ4FvyHUMzOK0NhZsVtuWIdUDOdl3j+5rtvrdsVzlLp2eqaSQEwZwPl7qek0M7+5i8DoSzZjg9MrsUsCiSXqRY9/G3M/KwL+MaL0116R7uwkQX+ndKvUvnqpjKKOJ/PsBFmZlXKYPDy6SFJURpmRATiafenEkg5D64fxBFw1k66ZXcQ81aYAGjpV8nqE18DcfVfj6czadpQ5Ycf/D4InUTPKTuFa2lMdVqrZ1+S6DDGIQdG9HEI7IpzFfYvGEFQc2x4BaeHroNym/k9PtIH+4debEDkrZ7Aaq/ofXTWoWLR4KoZoHcyUSGlPT+M9/aICgqaza/VZfPgobbiXRQTwNfNe4lUcbAhracX4RQDJbyPlHMtGAuDrIi6WLyBKZp4ehZPIT00YbPMP5BRTcPPwUneYVL2D1vbgWLO+0GsYfdr7fsm5TPd6fkajNj08ZSOWYRdJuoZJkplqM1GETlXkv5ictZ9k2Nm6K0qxBKIxaaBMm46jvcTM9K2YRK7QdvLTrXQzUYvR6HqcHUN3rAMT0LuyKY69FzJXuKaO4HYHQB0MaXJW/CVIo1xR92sqHUmcKn5nwyccT4hUscPAx/lNE7AZ8qcTqUqYRACF9dJhKOZD2dyO8dBHgnCqkg5JfifmbdCSEc2nGrGPx3rHRj/JXNqI404T27yB26mQw==
test_a:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFo1lt6lEk+1VUrMbhlaVpkI0p1TFUGujHaKKn7+VoGb
test_b:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICA9dQjNeX3eBvkOXJN+nJm1C2W9UtRiLbK9O87Mjkir
test_b:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFq82Pfsg7KjTU5LN4jikxITDQhCWB3TFxQdXTgYtKt40+gv88hZkemM1MYTzR30bUX/zcRsioUSwr3u7/2La7ti+BoilsHjrEx4w+nxNGCCe8D3M6K5Xi8MPL2AqbXFqkPSEpX+psrs+qILfNhs1lWAsN7GLP0cTIxPynFNECwJnUlleN0hsn8N8bQCoUInZQGmHwIHq62H+3IPbv7Vko3J0Zrqqo4OqfeV5BA0By7ZP+2Jd9ZsLJ2efaiALcs6oTk0v95wVQ36wp605x9ePYg6zHzIZDfpA400RqeuiZF5jpiG7q3eb0+CysfMbU0BpfeHmCq15PFYqre8HKAJZ3

187
cmd/conduit/config.go Normal file
View File

@@ -0,0 +1,187 @@
package main
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsimple"
"git.maze.io/maze/conduit/auth"
"git.maze.io/maze/conduit/policy"
"git.maze.io/maze/conduit/provider"
"git.maze.io/maze/conduit/ssh"
)
type Config struct {
Listen []*ListenConfig `hcl:"listen,block"`
Provider []*ProviderConfig `hcl:"provider,block"`
}
type ListenConfig struct {
Addr string `hcl:"addr,label"`
Keys []string `hcl:"keys"`
MaxConnections int `hcl:"max_connections,optional"`
DebugSession bool `hcl:"debug_session,optional"`
AllowTunnel bool `hcl:"allow_tunnel,optional"`
Auth *AuthConfig `hcl:"auth,block"`
}
func (c ListenConfig) Server() (*ssh.Server, error) {
var keys []ssh.Signer
if len(c.Keys) == 0 {
return nil, fmt.Errorf("listen %q has no keys configured", c.Addr)
}
for _, name := range c.Keys {
k, err := ssh.LoadPrivateKey(name)
if err != nil {
return nil, err
}
keys = append(keys, k)
}
var (
server = ssh.NewServer(keys)
err error
)
if c.Auth == nil {
return nil, fmt.Errorf("listen %q has no auth configured", c.Addr)
}
if server.AcceptHandler, err = c.Auth.AcceptHandler(); err != nil {
return nil, fmt.Errorf("listen %q: %w", c.Addr, err)
}
if c.MaxConnections > 0 {
server.ConnectHandler = append(server.ConnectHandler, ssh.MaxConnections(c.MaxConnections))
}
if c.DebugSession {
server.ChannelHandler[ssh.ChannelTypeSession] = ssh.DebugSession()
}
if c.AllowTunnel {
//server.ChannelHandler[ssh.ChannelTypeDirectTCPIP] = ssh.ForwardTunnel(nil)
server.PortForwardHandler = ssh.PortForwardDialer(nil)
}
return server, nil
}
type AuthConfig struct {
Type string `hcl:"type,label"`
Body hcl.Body `hcl:",remain"`
}
func (c AuthConfig) AcceptHandler() (ssh.AcceptHandler, error) {
switch c.Type {
/*
case "password_file":
var config struct {
Path string `hcl:"path"`
}
if diag := gohcl.DecodeBody(c.Body, nil, &config); diag.HasErrors() {
return nil, diag
}
return auth.PasswordFile(config.Path)
case "pubkey_file":
var config struct {
Path string `hcl:"path"`
}
if diag := gohcl.DecodeBody(c.Body, nil, &config); diag.HasErrors() {
return nil, diag
}
return auth.PublicKeyFile(config.Path)
case "user_ca":
var config struct {
Keys []string `hcl:"keys"`
}
if diag := gohcl.DecodeBody(c.Body, nil, &config); diag.HasErrors() {
return nil, diag
}
keys := make([]ssh.PublicKey, 0, len(config.Keys))
for _, s := range config.Keys {
k, _, err := ssh.ParseAuthorizedKey([]byte(s))
if err != nil {
return nil, err
}
keys = append(keys, k)
}
return auth.NewUserCertificateAuthority(keys...), nil
*/
case "mfa":
var config struct {
Policy string `hcl:"policy"`
Token string `hcl:"token,optional"`
PasswordFile string `hcl:"password_file,optional"`
UserCA []string `hcl:"user_ca,optional"`
}
if diag := gohcl.DecodeBody(c.Body, nil, &config); diag.HasErrors() {
return nil, diag
}
userCAKeys := make([]ssh.PublicKey, len(config.UserCA))
for i, key := range config.UserCA {
k, _, err := ssh.ParseAuthorizedKey([]byte(key))
if err != nil {
return nil, err
}
userCAKeys[i] = k
}
var passwordAuth auth.Password
if config.PasswordFile != "" {
var err error
if passwordAuth, err = auth.PasswordFile(config.PasswordFile); err != nil {
return nil, err
}
}
var tokenAuth auth.Password
if config.Token != "" {
var err error
if tokenAuth, err = auth.PasswordFile(config.Token); err != nil {
return nil, err
}
}
_ = tokenAuth
return &auth.MultiFactor{
Loader: policy.FileSystemLoader{Root: config.Policy},
UserCA: userCAKeys,
//Token: tokenAuth,
Password: passwordAuth,
}, nil
default:
return nil, fmt.Errorf("auth: unsupported type %q", c.Type)
}
}
type ProviderConfig struct {
Name string `hcl:"name,label"`
Body hcl.Body `hcl:",remain"`
}
func (c ProviderConfig) Init() error {
return provider.Init(c.Name, c.Body)
}
func Load(name string) (*Config, error) {
var config = new(Config)
if err := hclsimple.DecodeFile(name, nil, config); err != nil {
return nil, err
}
for _, providerConfig := range config.Provider {
if err := providerConfig.Init(); err != nil {
return nil, err
}
}
return config, nil
}

47
cmd/conduit/main.go Normal file
View File

@@ -0,0 +1,47 @@
package main
import (
"flag"
"net"
"sync"
"git.maze.io/maze/conduit/logger"
"git.maze.io/maze/conduit/ssh"
_ "git.maze.io/maze/conduit/provider/okta" // Okta support
)
func main() {
configFlag := flag.String("config", "conduit.hcl", "configuration file path")
flag.Parse()
logger.SetLevel(logger.TraceLevel)
config, err := Load(*configFlag)
if err != nil {
logger.Err(err).Fatal("Error loading configuration")
}
servers := make([]*ssh.Server, len(config.Listen))
for i, listenConfig := range config.Listen {
if servers[i], err = listenConfig.Server(); err != nil {
logger.Err(err).Fatal("Error configuring listener")
}
}
var wg sync.WaitGroup
for i, server := range servers {
wg.Go(func() {
l, err := net.Listen("tcp", config.Listen[i].Addr)
if err != nil {
logger.StandardLog.Err(err).Fatal("Can't bind to listening address")
}
logger.StandardLog.Value("server", l.Addr().String()).Info("Server starting")
if err = server.Serve(l); err != nil {
logger.StandardLog.Err(err).Error("Error serving connections")
}
})
}
wg.Wait()
}

62
conduit.hcl Normal file
View File

@@ -0,0 +1,62 @@
listen ":2222" {
keys = [
"testdata/conduit.rsa",
"testdata/conduit.ed25519",
]
debug_session = true
allow_tunnel = true
auth "mfa" {
policy = "testdata/policy"
token = "auth/testdata/passwd"
user_ca = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCufO7OUqeLZkUX7qGatOk79nZTQGqCbHxTp6z8Nb+52HJiRDXgLfY/3zLX/3kOdjVrQujwEEfbD6IVOjF3gnDkgYvjnJROXEiv3k2UApYzrbJebFohcFPrrk3WqbzOeMQXciEuSNyJV33FMYBnZ8Y+Yrf5a4x5R0pxbmdCmxSOhihOIZYlKNjPq3UVfXwth/NW4KiHUkmuH6d4x4D3OMJ+xKeK9Eu05szBWwRHY3vplf0SYiwDd3xPlFalPG2UzA3j/+kdtQf0qNJGyWpjRjHv9BvJMP/G+Y2CckvygetYBfcvX9JGb/p8G1JyU55ODD5maxDrCSFm8aqDbvCLmeAb",
]
}
}
provider "auth0" {
client {
url = "https://dev-maze.eu.auth0.com"
client_id = "DANKWsl8WRGY2pXQoB7CCwSkfoB8ouda"
client_secret = "VSPT2o10WQoJZSO3Y18tmWpS6MG0uDNIytn1-cDwPhp7AiMo8gVsLIBUOdF-ilCP"
}
}
provider "okta" {
client {
org_url = "https://integrator-8134036.okta.com/"
client_id = "0oaw2uk8mnkKEJJ7s697"
token = "00ebjg06VGdf9Y_AdUf494gf5pcBlURl8of02irbRw"
private_key = <<EOT
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5ACQsZz90gHpJ
VVCRzyz69wJNlraiOwCmNmJrFRyVaMjVJQ/by1fYWV6hEL2c35BEMr/9wUnAyHe5
2qHq11ZTsLKwayp7pVQOXt0LbM7XCjtD3j+M+Fj8iWJSEf3CWAHsoPmmBlMEDAgR
Zm5RbrESVYVJmN/rZwRL6CYparyN0l+1J0nZDPpaF3g5qmr47xZB/aWtqUkQMk6q
eaGYo9zYgHVQYZFWS++zXlYRx/EMwfX+UyhQiwZQAcH8dr1UjEbGoBvGcckrKYKW
lYKwM/KSWQq0XYLPK3ypKDfmzZMVsnr36iqNWDq6Q1tHAiiMsQ32RSYGNghRorLF
voCH8185AgMBAAECggEAG5LR0WxyIMMldtiocZMXeTBnv3i/L97rcdqZQKyc3ggI
JvynKHNeXHi1ifwcxszri76krwWoIHvAnIrhp0cBLugfOfw/EL7LkHjDKXjGO3bi
7nKptjEt0jYH2YOk9tp3LvWvP/ehV/ETIsTJnImLCtiETTvj5AouscGgLnwiLmDz
TCzUlI/pr0kiLf/giOsG2EzLwGWcESBD761AmJavbXvtmZAwI+LPCXlysZinzbyg
UoEjiKhNbn/3bPUtqqRJ+UOWxj3FyujQDpm/4qlXAI4YRhtnoVhP10008wxvt7dq
bl4ptExph5v/amizhs2ZvvZ1n8kT04PnikFcv9proQKBgQD3O3sdZHHyDb4zFXwN
8Z+N8OYY9EG7Wc6QjU3ABeDIJxR23pC/67pyHGspJjZuAqN6ObkMVtuCDC1AZ6KY
sZR5e62U/r8jhhU4l+3AfgWHMZc/XdYf8JMtlGaG7u+3TwwRM+bymjHzvj3Lsbxa
8geNa/MVl960xe+aD6KV7SQ4KwKBgQC/j68W0tY0I5mZPUly0BNddUS7IjNwmi5a
8b1hhmkia+9fYA1dyNcFwlZycux4LdsFv/N6RvN/eVzXn9H6SgTjynyxtpiK/+aB
Jt0Njz14vy6WmidrV1zxXw4beJBrfy1hb+vxpdUxs/ZH63EMToZDnju4NVizCD9I
JMnBDzHQKwKBgAuP+5UHUpDodaG7+n5Ic5bW0lwOaFiTvaZjBWTaoYWa1kks5YYk
Ryb5D0XwZJFGjFC2DGJ4WXG+kgs2DZOoknIQB7E1LMlDhxCLgnIDMsz8078B63a4
8JksHJNo70saZk0TqVRlQ7rLheZV3KJAOXwytT6oSKEZtLf2zTrHyW7bAoGAFr2T
5323+BCR12MzKPISmnGlayGwQZnMDvfLp5wxNujhTc01SQDipchgQs3pzIqFCbWz
zbxGg8eAgghzAOdwlSogi2hFy5p9Xq+iZk2u2nq3qSE7tL52RiEmp5Q0cM50MLD8
rX8mQ/Q9NGR60x8vSS+rnz6V/QrpmELlwIlxPGkCgYEAt6Ew7alRpRBMEOHzGKFt
suM0v9vR3XiyqjjQOyZaqSe9oXNW3aWrvCnqhI2+W4C32Knpokt5VVyCxhUX3SL/
M8hUa8IWVIAvjOyKqX+wVA1/w+1SHbimOE2T4GTT8RctSkTnr3hxOLCtgkRYV82A
pJbp+yG31SS1DgPsZxGhmB8=
-----END PRIVATE KEY-----
EOT
}
}

7
core/core.go Normal file
View File

@@ -0,0 +1,7 @@
package core
import "git.maze.io/maze/conduit/ssh"
type Config struct {
server *ssh.Server
}

78
go.mod Normal file
View File

@@ -0,0 +1,78 @@
module git.maze.io/maze/conduit
go 1.25.0
replace git.maze.io/maze/conduit/provider/okta => ./provider/okta
require (
git.maze.io/maze/conduit/provider/okta v0.0.0-00010101000000-000000000000
github.com/go-crypt/crypt v0.4.6
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/hashicorp/hcl/v2 v2.24.0
github.com/msteinert/pam v1.2.0
github.com/open-policy-agent/opa v1.9.0
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
golang.org/x/crypto v0.42.0
)
require (
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/go-crypt/x v0.4.8 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kelseyhightower/envconfig v1.3.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
github.com/lestrrat-go/jwx/v3 v3.0.11 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/okta/okta-sdk-golang v1.1.0 // indirect
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/square/go-jose v2.4.1+incompatible // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vektah/gqlparser/v2 v2.5.30 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
github.com/zclconf/go-cty v1.16.3 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/square/go-jose.v2 v2.4.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

166
go.sum Normal file
View File

@@ -0,0 +1,166 @@
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/go-crypt/crypt v0.4.6 h1:pC2CdIsCAjhvse6Q9oXZH97cV2iGqFOxE8HQCjY1cNg=
github.com/go-crypt/crypt v0.4.6/go.mod h1:Ts3T2ORhE0nxel6/2mlQNZTZn7dcxRdtnSJjoDqUkbs=
github.com/go-crypt/x v0.4.8 h1:Cob6IxrSfWTc+MG8CBbNHBM4UqrBgEZDoK5t/SG4oZ4=
github.com/go-crypt/x v0.4.8/go.mod h1:ozw9N4MYuLKhR5x2REs1e4T/nrEAkbuVkcsh/HbYksg=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
github.com/lestrrat-go/jwx/v3 v3.0.11 h1:yEeUGNUuNjcez/Voxvr7XPTYNraSQTENJgtVTfwvG/w=
github.com/lestrrat-go/jwx/v3 v3.0.11/go.mod h1:XSOAh2SiXm0QgRe3DulLZLyt+wUuEdFo81zuKTLcvgQ=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE=
github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/okta/okta-sdk-golang v1.1.0 h1:sr/KYSMRhs4F2NWEbqWXqN4y4cKKcfzrtOiBqR/J6mI=
github.com/okta/okta-sdk-golang v1.1.0/go.mod h1:KEjmr3Zo+wP3gVa3XhwIvENBfh7L/iRUeIl6ruQYOK0=
github.com/open-policy-agent/opa v1.9.0 h1:QWFNwbcc29IRy0xwD3hRrMc/RtSersLY1Z6TaID3vgI=
github.com/open-policy-agent/opa v1.9.0/go.mod h1:72+lKmTda0O48m1VKAxxYl7MjP/EWFZu9fxHQK2xihs=
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 h1:pSCLCl6joCFRnjpeojzOpEYs4q7Vditq8fySFG5ap3Y=
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/square/go-jose v2.4.1+incompatible h1:KFYc54wTtgnd3x4B/Y7Zr1s/QaEx2BNzRsB3Hae5LHo=
github.com/square/go-jose v2.4.1+incompatible/go.mod h1:7MxpAF/1WTVUu8Am+T5kNy+t0902CaLWM4Z745MkOa8=
github.com/square/go-jose/v3 v3.0.0-20200225220504-708a9fe87ddc/go.mod h1:JbpHhNyeVc538vtj/ECJ3gPYm1VEitNjsLhm4eJQQbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y=
gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

37
internal/netutil/conn.go Normal file
View File

@@ -0,0 +1,37 @@
package netutil
import (
"errors"
"io"
"net"
"syscall"
"git.maze.io/maze/conduit/logger"
)
type ConnCloser struct {
net.Conn
Closer func() error
}
func (c *ConnCloser) Close() error {
if c.Closer == nil {
return c.Conn.Close()
}
return c.Closer()
}
// IsClosing checks if the error is because the connection was closed.
func IsClosing(err error) bool {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, syscall.ECONNRESET) {
return true
}
if err, ok := err.(net.Error); ok && err.Timeout() {
return true
}
if err, ok := err.(*net.OpError); ok && err.Op == "close" {
return true
}
logger.Debugf("not a closing error %T: %#+v", err, err)
return false
}

View File

@@ -0,0 +1,23 @@
package stringutil
import "sort"
func MapKeys(m map[string]string) <-chan string {
ch := make(chan string)
if m == nil {
close(ch)
} else {
go func(ch chan<- string) {
defer close(ch)
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
ch <- k
}
}(ch)
}
return ch
}

231
logger/log.go Normal file
View File

@@ -0,0 +1,231 @@
package logger
import "github.com/sirupsen/logrus"
type Level int
const (
TraceLevel Level = -1 + iota
DebugLevel
InfoLevel
WarnLevel
ErrorLevel
PanicLevel
FatalLevel
)
// Logger is a generic logging interface, similar to logrus's [logrus.ValueLogger].
// It is used in Styx for logging, so that users can plug in their own logging implementations.
type Logger interface {
SetLevel(Level)
GetLevel() Level
Trace(...any)
Tracef(string, ...any)
Debug(...any)
Debugf(string, ...any)
Info(...any)
Infof(string, ...any)
Warn(...any)
Warnf(string, ...any)
Error(...any)
Errorf(string, ...any)
Panic(...any)
Panicf(string, ...any)
Fatal(...any)
Fatalf(string, ...any)
}
type Structured interface {
Logger
// Err adds an error to the log entry and returns the new logger.
Err(error) Structured
// Value returns a new logger with the specified Value added to the log entry.
Value(string, any) Structured
// Values returns a new logger with the specified Values added to the log entry.
Values(Values) Structured
}
type Values map[string]any
// Alias.
type V = Values
// StandardLog is the logger used by the package-level exported functions.
var StandardLog = NewStandardLogger()
// SetLogger sets the logger used by the package-level exported functions.
func SetLogger(logger Structured) {
StandardLog = logger
}
// Get returns the logger used by the package-level exported functions.
func Get() Structured {
return StandardLog
}
type standardLogger struct {
*logrus.Logger
}
// NewStandardLogger returns a new Structured logger that wraps the standard logrus logger.
func NewStandardLogger() Structured {
return standardLogger{logrus.StandardLogger()}
}
type standardLoggerEntry struct {
standardLogger
*logrus.Entry
}
func SetLevel(level Level) {
StandardLog.SetLevel(level)
}
func (l standardLogger) SetLevel(level Level) {
switch level {
case TraceLevel:
l.Logger.SetLevel(logrus.TraceLevel)
case DebugLevel:
l.Logger.SetLevel(logrus.DebugLevel)
case InfoLevel:
l.Logger.SetLevel(logrus.InfoLevel)
case WarnLevel:
l.Logger.SetLevel(logrus.WarnLevel)
case ErrorLevel:
l.Logger.SetLevel(logrus.ErrorLevel)
case PanicLevel:
l.Logger.SetLevel(logrus.PanicLevel)
case FatalLevel:
l.Logger.SetLevel(logrus.FatalLevel)
}
}
func GetLevel() Level {
return StandardLog.GetLevel()
}
func (l standardLogger) GetLevel() Level {
switch l.Logger.GetLevel() {
case logrus.TraceLevel:
return TraceLevel
case logrus.DebugLevel:
return DebugLevel
case logrus.InfoLevel:
return InfoLevel
case logrus.WarnLevel:
return WarnLevel
case logrus.ErrorLevel:
return ErrorLevel
case logrus.PanicLevel:
return PanicLevel
case logrus.FatalLevel:
return FatalLevel
default:
return InfoLevel
}
}
func Err(err error) Structured {
return StandardLog.Err(err)
}
func (l standardLogger) Err(err error) Structured {
return standardLoggerEntry{l, l.Logger.WithError(err)}
}
func Value(key string, value any) Structured {
return StandardLog.Value(key, value)
}
func (l standardLogger) Value(key string, value any) Structured {
return standardLoggerEntry{l, l.Logger.WithField(key, value)}
}
func (l standardLogger) Values(Values Values) Structured {
return standardLoggerEntry{l, l.Logger.WithFields(logrus.Fields(Values))}
}
func (l standardLoggerEntry) Err(err error) Structured {
return standardLoggerEntry{l.standardLogger, l.Entry.WithError(err)}
}
func (l standardLoggerEntry) Value(key string, value any) Structured {
return standardLoggerEntry{l.standardLogger, l.Entry.WithField(key, value)}
}
func (l standardLoggerEntry) Values(Values Values) Structured {
return standardLoggerEntry{l.standardLogger, l.Entry.WithFields(logrus.Fields(Values))}
}
// Trace logs a message at level Trace on the standard logger.
func Trace(args ...any) {
StandardLog.Trace(args...)
}
// Tracef logs a message at level Trace on the standard logger.
func Tracef(format string, args ...any) {
StandardLog.Tracef(format, args...)
}
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...any) {
StandardLog.Debug(args...)
}
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...any) {
StandardLog.Debugf(format, args...)
}
// Info logs a message at level Info on the standard logger.
func Info(args ...any) {
StandardLog.Info(args...)
}
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...any) {
StandardLog.Infof(format, args...)
}
// Warn logs a message at level Warn on the standard logger.
func Warn(args ...any) {
StandardLog.Warn(args...)
}
// Warnf logs a message at level Warn on the standard logger.
func Warnf(format string, args ...any) {
StandardLog.Warnf(format, args...)
}
// Error logs a message at level Error on the standard logger.
func Error(args ...any) {
StandardLog.Error(args...)
}
// Errorf logs a message at level Error on the standard logger.
func Errorf(format string, args ...any) {
StandardLog.Errorf(format, args...)
}
// Panic logs a message at level Panic on the standard logger.
func Panic(args ...any) {
StandardLog.Panic(args...)
}
// Panicf logs a message at level Panic on the standard logger.
func Panicf(format string, args ...any) {
StandardLog.Panicf(format, args...)
}
// Fatal logs a message at level Fatal on the standard logger then the process will exit.
func Fatal(args ...any) {
StandardLog.Fatal(args...)
}
// Fatalf logs a message at level Fatal on the standard logger then the process will exit.
func Fatalf(format string, args ...any) {
StandardLog.Fatalf(format, args...)
}

42
policy/input/net.go Normal file
View File

@@ -0,0 +1,42 @@
package input
import (
"net"
)
// Addr represents a [net.Addr].
type Addr struct {
Network string `json:"network"` // Type of address.
IP string `json:"ip"` // IP address.
Port int `json:"port,omitempty"` // Port (if any).
}
func NewAddr(addr net.Addr) *Addr {
if addr == nil {
return nil
}
switch addr := addr.(type) {
case *net.IPAddr:
return &Addr{
Network: addr.Network(),
IP: addr.IP.String(),
}
case *net.TCPAddr:
return &Addr{
Network: addr.Network(),
IP: addr.IP.String(),
Port: addr.Port,
}
case *net.UDPAddr:
return &Addr{
Network: addr.Network(),
IP: addr.IP.String(),
Port: addr.Port,
}
default:
return &Addr{
Network: addr.Network(),
IP: addr.String(),
}
}
}

98
policy/input/ssh.go Normal file
View File

@@ -0,0 +1,98 @@
package input
import (
"time"
"golang.org/x/crypto/ssh"
"git.maze.io/maze/conduit/ssh/sshutil"
)
// Certificate represents a [ssh.Certificate].
type Certificate struct {
Nonce []byte `json:"nonce"`
Key *PublicKey `json:"key"`
Serial uint64 `json:"serial"`
CertType uint32 `json:"type"`
KeyId string `json:"key_id"`
ValidPrincipals []string `json:"valid_principals"`
ValidAfter *time.Time `json:"valid_after"`
ValidBefore *time.Time `json:"valid_before"`
SignatureKey *PublicKey `json:"signature_key"`
Signature []byte `json:"signature"`
SignatureFormat string `json:"signature_format"`
}
// NewCertificate converts an [ssh.Certificate] to [Certificate] input.
func NewCertificate(cert *ssh.Certificate) *Certificate {
if cert == nil {
return nil
}
c := &Certificate{
Nonce: cert.Nonce,
Key: NewPublicKey(cert.Key),
Serial: cert.Serial,
CertType: cert.CertType,
KeyId: cert.KeyId,
ValidPrincipals: cert.ValidPrincipals,
SignatureKey: NewPublicKey(cert.SignatureKey),
}
if cert.ValidAfter > 0 {
t := time.Unix(int64(cert.ValidAfter), 0)
c.ValidAfter = &t
}
if cert.ValidBefore > 0 {
t := time.Unix(int64(cert.ValidBefore), 0)
c.ValidBefore = &t
}
if cert.Signature != nil {
c.Signature = cert.Signature.Blob
c.SignatureFormat = cert.Signature.Format
}
return c
}
// ConnMetadata is a Rego input that represents a [ssh.ConnMetadata].
type ConnMetadata struct {
User string `json:"user"` // User is the user ID for this connection.
SessionID []byte `json:"session_id"` // SessionID is the session hash, also denoted by H.
ClientVersion string `json:"client_version"` // ClientVersion is the client's version.
ServerVersion string `json:"server_version"` // ServerVersion is the server's version
RemoteAddr *Addr `json:"remote_addr"` // RemoteAddr is the remote address for this connection.
LocalAddr *Addr `json:"local_addr"` // LocalAddr is the local address for this connection.
}
// NewConnMetadata converts an [ssh.ConnMetadata] to [ConnMetadata] input.
func NewConnMetadata(meta ssh.ConnMetadata) *ConnMetadata {
if meta == nil {
return nil
}
return &ConnMetadata{
User: meta.User(),
SessionID: meta.SessionID(),
ClientVersion: string(meta.ClientVersion()),
ServerVersion: string(meta.ServerVersion()),
RemoteAddr: NewAddr(meta.RemoteAddr()),
LocalAddr: NewAddr(meta.LocalAddr()),
}
}
// PublicKey represents a [ssh.PublicKey].
type PublicKey struct {
Type string `json:"type"`
Bits int `json:"bits"`
Fingerprint string `json:"fingerprint"`
}
// NewPublicKey converts an [ssh.PublicKey] to [PublicKey] input.
func NewPublicKey(key ssh.PublicKey) *PublicKey {
if key == nil {
return nil
}
return &PublicKey{
Type: sshutil.KeyType(key),
Bits: sshutil.KeyBits(key),
Fingerprint: ssh.FingerprintSHA256(key),
}
}

157
policy/policy.go Normal file
View File

@@ -0,0 +1,157 @@
package policy
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"github.com/go-viper/mapstructure/v2"
"github.com/open-policy-agent/opa/v1/rego"
"git.maze.io/maze/conduit/logger"
)
type Policy interface {
// Package name.
Package() string
// Query is the policy query.
Query(query string, input, result any) error
// Verify the policy definition.
Verify() error
}
type Loader interface {
LoadPolicy(name string) (Policy, error)
}
type FileSystemLoader struct {
Root string
}
func (l FileSystemLoader) LoadPolicy(name string) (Policy, error) {
path := name
if !strings.ContainsRune(filepath.Base(name), '.') || filepath.Ext(path) == "" {
path += ".rego"
}
log := logger.StandardLog.Value("policy", path)
if !filepath.IsAbs(path) {
var err error
if path, err = filepath.Abs(filepath.Join(l.Root, path)); err != nil {
log.Err(err).Warn("Error resolving absolute policy path")
return nil, err
}
}
b, err := os.ReadFile(path)
if err != nil {
log.Err(err).Value("path", path).Warn("Error reading policy file")
return nil, err
}
pkg, err := decodePackage(b)
if err != nil {
log.Err(err).Warn("Error decoding policy package")
return nil, err
}
return &policyFile{
path: []string{path},
pkg: pkg,
}, nil
}
func decodePackage(b []byte) (name string, err error) {
s := bufio.NewScanner(bytes.NewReader(b))
for s.Scan() {
line := s.Text()
if strings.HasPrefix(line, "package ") {
return strings.TrimSpace(line[len("package "):]), nil
}
}
if err = s.Err(); err != nil {
return
}
return "", errors.New("policy: no package name found")
}
type policyFile struct {
path []string
pkg string
}
func (p policyFile) Package() string {
return p.pkg
}
func (p policyFile) Query(query string, input, value any) error {
log := logger.StandardLog.Values(logger.Values{
"policy": p.path[0],
"package": p.pkg,
"query": query,
})
log.Trace("Policy query evaluating")
options := []func(*rego.Rego){
rego.Dump(os.Stderr),
rego.Query(query),
rego.Load(p.path, nil),
rego.Strict(true),
}
if input != nil {
debug := json.NewEncoder(os.Stdout)
debug.SetIndent("", " ")
debug.Encode(input)
options = append(options, rego.Input(input))
}
ctx := context.TODO()
q, err := rego.New(options...).PrepareForEval(ctx)
if err != nil {
log.Err(err).Warn("Policy query prepare failed")
return err
}
results, err := q.Eval(ctx)
if err != nil {
log.Err(err).Warn("Policy evaluation failed")
return err
}
log = log.Value("results", len(results))
if value == nil {
log.Debug("Policy query results processing ended, nil return value")
return nil
}
log.Debug("Policy query results processing")
mapped := make(map[string]any)
for _, result := range results {
for _, expr := range result.Expressions {
if value, ok := expr.Value.(map[string]any); ok {
for k, v := range value {
mapped[k] = v
}
} else {
log.Debugf("Unhandled expression value %T", expr)
}
}
}
log.Value("mapped", mapped).Debug("Policy query results done")
return mapstructure.Decode(mapped, value)
}
func (p policyFile) Verify() error {
_, err := rego.New(
rego.Strict(true),
rego.Load(p.path, nil),
).PrepareForEval(context.TODO())
return err
}

29
provider/okta/go.mod Normal file
View File

@@ -0,0 +1,29 @@
module git.maze.io/maze/conduit/provider/okta
go 1.25.0
replace git.maze.io/maze/conduit => ../..
require (
git.maze.io/maze/conduit v0.0.0-00010101000000-000000000000
github.com/hashicorp/hcl/v2 v2.24.0
github.com/okta/okta-sdk-golang v1.1.0
)
require (
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/kelseyhightower/envconfig v1.3.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 // indirect
github.com/square/go-jose v2.4.1+incompatible // indirect
github.com/zclconf/go-cty v1.16.3 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
gopkg.in/square/go-jose.v2 v2.4.1 // indirect
)

58
provider/okta/go.sum Normal file
View File

@@ -0,0 +1,58 @@
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/okta/okta-sdk-golang v1.1.0 h1:sr/KYSMRhs4F2NWEbqWXqN4y4cKKcfzrtOiBqR/J6mI=
github.com/okta/okta-sdk-golang v1.1.0/go.mod h1:KEjmr3Zo+wP3gVa3XhwIvENBfh7L/iRUeIl6ruQYOK0=
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 h1:pSCLCl6joCFRnjpeojzOpEYs4q7Vditq8fySFG5ap3Y=
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/square/go-jose v2.4.1+incompatible h1:KFYc54wTtgnd3x4B/Y7Zr1s/QaEx2BNzRsB3Hae5LHo=
github.com/square/go-jose v2.4.1+incompatible/go.mod h1:7MxpAF/1WTVUu8Am+T5kNy+t0902CaLWM4Z745MkOa8=
github.com/square/go-jose/v3 v3.0.0-20200225220504-708a9fe87ddc/go.mod h1:JbpHhNyeVc538vtj/ECJ3gPYm1VEitNjsLhm4eJQQbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y=
gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

60
provider/okta/provider.go Normal file
View File

@@ -0,0 +1,60 @@
package okta
import (
"context"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/okta/okta-sdk-golang/okta"
"git.maze.io/maze/conduit/provider"
)
func init() {
provider.Register(&provider.Config{
Name: "okta",
Init: setup,
})
}
var (
configuration []okta.ConfigSetter
client *okta.Client
)
func setup(body hcl.Body) (err error) {
var config struct {
Client struct {
OrgURL string `hcl:"org_url"`
Token string `hcl:"token,optional"`
ClientID string `hcl:"client_id"`
PrivateKey string `hcl:"private_key,optional"`
JWT string `hcl:"jwt,optional"`
} `hcl:"client,block"`
}
if diag := gohcl.DecodeBody(body, nil, &config); diag.HasErrors() {
return diag
}
configuration = []okta.ConfigSetter{
okta.WithOrgUrl(config.Client.OrgURL),
okta.WithClientId(config.Client.ClientID),
}
if config.Client.Token != "" {
configuration = append(configuration, okta.WithToken(config.Client.Token))
}
if config.Client.PrivateKey != "" {
configuration = append(configuration, okta.WithPrivateKey(config.Client.PrivateKey))
}
if client, err = okta.NewClient(context.TODO(), configuration...); err != nil {
return fmt.Errorf("okta: %w", err)
}
if _, _, err = client.User.ListUsers(nil); err != nil {
return fmt.Errorf("okta: %w", err)
}
return
}

29
provider/provider.go Normal file
View File

@@ -0,0 +1,29 @@
package provider
import (
"fmt"
"github.com/hashicorp/hcl/v2"
)
type Config struct {
// Name is a unique identifier for the provider.
Name string
// Init is called once to initialize the provider from the matching configuration block.
Init func(hcl.Body) error
}
var providerConfigs = make(map[string]*Config)
func Init(name string, body hcl.Body) error {
p, ok := providerConfigs[name]
if ok {
return p.Init(body)
}
return fmt.Errorf("provider: no %q provider available", name)
}
func Register(provider *Config) {
providerConfigs[provider.Name] = provider
}

132
recorder/asciicast.go Normal file
View File

@@ -0,0 +1,132 @@
package recorder
import (
"encoding/json"
"fmt"
"io"
"strconv"
"sync"
"time"
)
type asciiCastRecorder struct {
wc io.WriteCloser
mu sync.Mutex
header asciiCastHeader
last time.Time
closed bool
}
type asciiCastHeader struct {
Version int `json:"version"`
Term asciiCastTerm `json:"term"`
Timestamp int64 `json:"timestamp"`
Title string `json:"title,omitempty"`
Env map[string]string `json:"env,omitempty"`
}
type asciiCastTerm struct {
Columns int `json:"cols"`
Rows int `json:"rows"`
Type string `json:"type"`
}
type asciiCastDuration float64
func (d asciiCastDuration) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatFloat(float64(d), 'f', 6, 64)), nil
}
type asciiCastFrame struct {
Delay float64
Data []byte
}
func (f asciiCastFrame) MarshalJSON() ([]byte, error) {
s, _ := json.Marshal(string(f.Data))
return []byte(fmt.Sprintf(`[%.6f, %s]`, f.Delay, s)), nil
}
func newAsciiCastRecorder(wc io.WriteCloser, info Info) (*asciiCastRecorder, error) {
now := time.Now()
if err := json.NewEncoder(wc).Encode(asciiCastHeader{
Version: 3,
Term: asciiCastTerm{
Columns: info.Columns,
Rows: info.Rows,
Type: info.TerminalType,
},
Timestamp: now.Unix(),
Title: info.Title,
}); err != nil {
return nil, err
}
if _, err := io.WriteString(wc, "\n"); err != nil {
return nil, err
}
return &asciiCastRecorder{
wc: wc,
last: now,
}, nil
}
func (r *asciiCastRecorder) Close() error {
return r.wc.Close()
}
func (r *asciiCastRecorder) writeFrame(kind rune, p []byte) (int, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return 0, io.ErrClosedPipe
}
var (
now = time.Now()
delay = now.Sub(r.last)
)
v, _ := json.Marshal(string(p))
_, err := io.WriteString(r.wc, "["+strconv.FormatFloat(delay.Seconds(), 'f', 6, 64)+", \""+string(kind)+"\", "+string(v)+"]\n")
r.last = now
return len(p), err
}
type asciiCastWriter struct {
r *asciiCastRecorder
kind rune
}
func (w *asciiCastWriter) Close() error {
return w.r.Close()
}
func (w *asciiCastWriter) Write(p []byte) (int, error) {
return w.r.writeFrame(w.kind, p)
}
func (r *asciiCastRecorder) Reads() io.WriteCloser {
return &asciiCastWriter{r, 'o'}
}
func (r *asciiCastRecorder) Writes() io.WriteCloser {
return &asciiCastWriter{r, 'i'}
}
func (r *asciiCastRecorder) Resize(columns, rows int) {
var (
now = time.Now()
delay = now.Sub(r.last)
)
r.mu.Lock()
_, _ = fmt.Fprintf(r.wc, "[%.6f, \"r\", \"%dx%d\"]\n", delay.Seconds(), columns, rows)
r.last = now
r.mu.Unlock()
}
var (
_ Recorder = (*asciiCastRecorder)(nil)
_ Resizer = (*asciiCastRecorder)(nil)
)

68
recorder/recorder.go Normal file
View File

@@ -0,0 +1,68 @@
package recorder
import (
"errors"
"io"
"os"
)
type Format int
const (
Text Format = iota
TTYRec
ASCIICastv3
)
var (
ErrFormat = errors.New("recorder: unknown format")
)
type Recorder interface {
Reads() io.WriteCloser
Writes() io.WriteCloser
}
type Resizer interface {
Resize(columns, rows int)
}
type Info struct {
Columns int
Rows int
TerminalType string
Title string
Env map[string]string
}
func New(name string, format Format, info Info) (Recorder, error) {
f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0o640)
if err != nil {
return nil, err
}
return NewWriter(f, format, info)
}
func NewWriter(wc io.WriteCloser, format Format, info Info) (Recorder, error) {
switch format {
case Text:
return textRecorder{wc}, nil
case ASCIICastv3:
return newAsciiCastRecorder(wc, info)
case TTYRec:
return newTTYRecRecorder(wc), nil
default:
return nil, ErrFormat
}
}
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error {
return nil
}

15
recorder/text.go Normal file
View File

@@ -0,0 +1,15 @@
package recorder
import "io"
type textRecorder struct {
io.WriteCloser
}
func (r textRecorder) Reads() io.WriteCloser {
return r.WriteCloser
}
func (r textRecorder) Writes() io.WriteCloser {
return r.WriteCloser
}

72
recorder/ttyrec.go Normal file
View File

@@ -0,0 +1,72 @@
package recorder
import (
"encoding/binary"
"io"
"sync"
"syscall"
"time"
)
type ttyRecordHeader struct {
Sec int32
USec int32
Len int32
}
func (h *ttyRecordHeader) Now() {
var (
now = time.Now()
tv = syscall.NsecToTimeval(now.UnixNano())
)
h.Sec = int32(tv.Sec)
h.USec = tv.Usec
}
func (h *ttyRecordHeader) WriteTo(w io.Writer) (int64, error) {
var b [6]byte
binary.LittleEndian.PutUint32(b[0:], uint32(h.Sec))
binary.LittleEndian.PutUint32(b[2:], uint32(h.USec))
binary.LittleEndian.PutUint32(b[4:], uint32(h.Len))
n, err := w.Write(b[:])
return int64(n), err
}
type ttyRecRecorder struct {
mu sync.Mutex
wc io.WriteCloser
header *ttyRecordHeader
}
func newTTYRecRecorder(wc io.WriteCloser) *ttyRecRecorder {
return &ttyRecRecorder{
wc: wc,
header: new(ttyRecordHeader),
}
}
func (r *ttyRecRecorder) Close() error {
return r.wc.Close()
}
func (w *ttyRecRecorder) Write(p []byte) (n int, err error) {
w.mu.Lock()
defer w.mu.Unlock()
w.header.Now()
if _, err = w.header.WriteTo(w.wc); err != nil {
return
}
if _, err = w.wc.Write(p); err != nil {
return
}
return len(p) + 9, nil
}
func (r *ttyRecRecorder) Reads() io.WriteCloser {
return r
}
func (r *ttyRecRecorder) Writes() io.WriteCloser {
return nopCloser{io.Discard}
}

52
ssh/channel.go Normal file
View File

@@ -0,0 +1,52 @@
package ssh
import (
"errors"
"io"
"golang.org/x/crypto/ssh"
)
type DupeChannel struct {
ssh.Channel
// Reads writes read actions.
Reads io.WriteCloser
// Writer writes write actions.
Writes io.WriteCloser
}
func (c DupeChannel) Close() error {
var errs []error
for _, closer := range []io.Closer{
c.Channel,
c.Reads,
c.Writes,
} {
if closer == nil {
continue
}
if cerr := closer.Close(); cerr != nil {
errs = append(errs, cerr)
}
}
return errors.Join(errs...)
}
func (c DupeChannel) Read(p []byte) (n int, err error) {
if c.Reads == nil {
return c.Channel.Read(p)
}
return io.TeeReader(c.Channel, c.Reads).Read(p)
}
func (c DupeChannel) Write(p []byte) (n int, err error) {
if c.Writes == nil {
return c.Channel.Write(p)
}
if n, err = c.Channel.Write(p); n > 0 {
_, _ = c.Writes.Write(p[:])
}
return
}

28
ssh/client.go Normal file
View File

@@ -0,0 +1,28 @@
package ssh
import (
"net"
"golang.org/x/crypto/ssh"
)
type Client struct {
netConn net.Conn
client *ssh.Client
}
type ClientConfig struct {
ssh.ClientConfig
}
func NewClient(conn net.Conn, config *ClientConfig) (*Client, error) {
sshConn, channels, requests, err := ssh.NewClientConn(conn, conn.RemoteAddr().String(), &config.ClientConfig)
if err != nil {
return nil, err
}
return &Client{
netConn: conn,
client: ssh.NewClient(sshConn, channels, requests),
}, nil
}

46
ssh/compat.go Normal file
View File

@@ -0,0 +1,46 @@
package ssh
import (
"golang.org/x/crypto/ssh"
)
const (
ChannelTypeDefault = ""
ChannelTypeAgent = "auth-agent@openssh.com"
ChannelTypeDirectTCPIP = "direct-tcpip"
ChannelTypeSession = "session"
)
const (
RequestTypeAgent = "auth-agent-req@openssh.com"
RequestTypeEnv = "env"
RequestTypeExec = "exec"
RequestTypePTY = "pty-req"
RequestTypeShell = "shell"
RequestTypeWindowChange = "window-change"
)
// Type aliases for convenience.
type (
CertChecker = ssh.CertChecker
Certificate = ssh.Certificate
Conn = ssh.Conn
ConnMetadata = ssh.ConnMetadata
Permissions = ssh.Permissions
PublicKey = ssh.PublicKey
ServerConfig = ssh.ServerConfig
Signer = ssh.Signer
)
func MarshalAuthorizedKey(in PublicKey) []byte {
return ssh.MarshalAuthorizedKey(in)
}
func ParseAuthorizedKey(in []byte) (out PublicKey, options []string, err error) {
out, _, options, _, err = ssh.ParseAuthorizedKey(in)
return
}
func ParsePublicKey(in []byte) (out PublicKey, err error) {
return ssh.ParsePublicKey(in)
}

204
ssh/context.go Normal file
View File

@@ -0,0 +1,204 @@
package ssh
import (
"encoding/binary"
"encoding/hex"
"io"
"math/rand"
"net"
"time"
"golang.org/x/crypto/ssh"
"git.maze.io/maze/conduit/logger"
)
var seed = rand.NewSource(time.Now().UnixNano())
type Context interface {
ssh.ConnMetadata
// ID is the unique identifier.
ID() string
// Conn is the [ssh.Conn].
Conn() ssh.Conn
// NetConn is the underlying [net.Conn].
NetConn() net.Conn
// Close the client connection.
Close() error
}
type sshContext struct {
id uint64
server *Server
netConn net.Conn
conn *ssh.ServerConn
log logger.Structured
}
func newSSHContext(server *Server, netConn net.Conn, conn *ssh.ServerConn, log logger.Structured) *sshContext {
ctx := &sshContext{
id: uint64(seed.Int63()),
server: server,
netConn: netConn,
conn: conn,
}
ctx.log = log.Value("context", ctx.ID())
return ctx
}
// User returns the user ID for this connection.
func (ctx *sshContext) User() string {
return ctx.conn.User()
}
// SessionID returns the session hash, also denoted by H.
func (ctx *sshContext) SessionID() []byte {
return ctx.conn.SessionID()
}
// ClientVersion returns the client's version string as hashed
// into the session ID.
func (ctx *sshContext) ClientVersion() []byte {
return ctx.conn.ClientVersion()
}
// ServerVersion returns the server's version string as hashed
// into the session ID.
func (ctx *sshContext) ServerVersion() []byte {
return ctx.conn.ServerVersion()
}
// RemoteAddr returns the remote address for this connection.
func (ctx *sshContext) RemoteAddr() net.Addr {
return ctx.netConn.RemoteAddr()
}
// LocalAddr returns the local address for this connection.
func (ctx *sshContext) LocalAddr() net.Addr {
return ctx.netConn.LocalAddr()
}
func (ctx *sshContext) handleChannels(channels <-chan ssh.NewChannel) (err error) {
for newChan := range channels {
var (
kind = newChan.ChannelType()
log = ctx.log.Value("channel", kind)
)
log.Trace("Client requested new channel")
handler, ok := ctx.server.ChannelHandler[kind]
if !ok {
handler = ctx.server.ChannelHandler[ChannelTypeDefault]
}
if handler != nil {
var (
channel ssh.Channel
requests <-chan *ssh.Request
)
if channel, requests, err = newChan.Accept(); err != nil {
return
}
if err = handler.HandleChannel(ctx, channel, requests, newChan.ExtraData()); err != nil {
return
}
} else if kind == ChannelTypeDirectTCPIP && ctx.server.PortForwardHandler != nil {
if err = ctx.handleDirectTCPIP(newChan); err != nil {
return
}
} else {
ctx.log.Debug("Rejecting unsupported channel type")
if err = newChan.Reject(ssh.Prohibited, ""); err != nil {
return
}
}
}
// Our client hang up.
return io.EOF
}
func (ctx *sshContext) handleDirectTCPIP(newChan ssh.NewChannel) (err error) {
var payload struct {
Host string
Port uint32
OriginAddr string
OriginPort uint32
}
if err = ssh.Unmarshal(newChan.ExtraData(), &payload); err != nil {
_ = newChan.Reject(ssh.Prohibited, "")
return
}
var ip net.IP
if ip = net.ParseIP(payload.Host); ip == nil {
// Not an IP
var ips []net.IP
if ips, err = net.LookupIP(payload.Host); err != nil {
_ = newChan.Reject(ssh.ConnectionFailed, err.Error())
return
} else if len(ips) == 0 {
_ = newChan.Reject(ssh.ConnectionFailed, "")
return
}
ip = ips[0]
}
var (
raddr = &net.TCPAddr{
IP: ip,
Port: int(payload.Port),
}
laddr = &net.TCPAddr{
IP: net.ParseIP(payload.OriginAddr),
Port: int(payload.OriginPort),
}
)
if payload.OriginAddr == "" && payload.OriginPort == 0 {
laddr = nil
}
var conn net.Conn
if conn, err = ctx.server.PortForwardHandler.HandlePortForwardRequest(ctx, raddr, laddr); err != nil {
_ = newChan.Reject(ssh.ConnectionFailed, err.Error())
return
}
defer func() { _ = conn.Close() }()
var (
channel ssh.Channel
requests <-chan *ssh.Request
)
if channel, requests, err = newChan.Accept(); err != nil {
return
}
defer func() { _ = channel.Close() }()
go ssh.DiscardRequests(requests)
go io.Copy(channel, conn)
_, err = io.Copy(conn, channel)
return
}
func (ctx *sshContext) Conn() ssh.Conn {
return ctx.conn
}
func (ctx *sshContext) NetConn() net.Conn {
return ctx.netConn
}
func (ctx *sshContext) Close() error {
return ctx.conn.Close()
}
func (ctx *sshContext) ID() string {
var b [8]byte
binary.BigEndian.PutUint64(b[:], ctx.id)
return hex.EncodeToString(b[:])
}

352
ssh/handler.go Normal file
View File

@@ -0,0 +1,352 @@
package ssh
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"sort"
"sync"
"sync/atomic"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"git.maze.io/maze/conduit/internal/netutil"
"git.maze.io/maze/conduit/internal/stringutil"
"git.maze.io/maze/conduit/logger"
"git.maze.io/maze/conduit/ssh/sshutil"
)
type ConnectHandler interface {
HandleConnect(net.Conn) (net.Conn, error)
}
type ConnectHandlerFunc func(net.Conn) (net.Conn, error)
func (f ConnectHandlerFunc) HandleConnect(c net.Conn) (net.Conn, error) {
return f(c)
}
type AcceptHandler interface {
HandleAccept(net.Conn, *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error)
}
// MaxConnections limits the maximum number of connections.
func MaxConnections(max int) ConnectHandler {
var connections atomic.Int64
return ConnectHandlerFunc(func(c net.Conn) (net.Conn, error) {
if max <= 0 {
return nil, nil
} else if connections.Load() >= int64(max) {
return nil, errors.New("server: maximum number of connections reached")
}
var (
once sync.Once
cc = &netutil.ConnCloser{
Conn: c,
Closer: func() error {
once.Do(func() { connections.Add(-1) })
return c.Close()
},
}
)
connections.Add(1)
return cc, nil
})
}
type ChannelHandler interface {
HandleChannel(Context, ssh.Channel, <-chan *ssh.Request, []byte) error
}
type ChannelHandlerFunc func(Context, ssh.Channel, <-chan *ssh.Request, []byte) error
func (f ChannelHandlerFunc) HandleChannel(ctx Context, channel ssh.Channel, requests <-chan *ssh.Request, extra []byte) error {
return f(ctx, channel, requests, extra)
}
type debugSessionInfo struct {
start time.Time
duration time.Duration
method string
agent agent.Agent
agentRequest bool
agentChannel ssh.Channel
agentError error
env map[string]string
pty *sshutil.PTYRequest
windowChange *sshutil.WindowChangeRequest
unsupported []string
}
func newDebugSessionInfo() *debugSessionInfo {
return &debugSessionInfo{
start: time.Now(),
env: make(map[string]string),
}
}
// DebugSession is a session channel handler that will print debug information to the client, which
// may aid with troubleshooting SSH connectivity or client issues.
func DebugSession() ChannelHandler {
debugPrivateKey, _ := rsa.GenerateKey(rand.Reader, 1024)
return ChannelHandlerFunc(func(ctx Context, channel ssh.Channel, requests <-chan *ssh.Request, _ []byte) error {
defer channel.Close()
var (
log = ctx.(*sshContext).log
done = make(chan struct{}, 1)
info = newDebugSessionInfo()
conn = ctx.Conn()
err error
)
go func() {
var (
reply bool
reesponse []byte
)
for request := range requests {
log.Values(logger.Values{
"request": request.Type,
}).Trace("New session channel request")
switch request.Type {
case RequestTypeAgent:
info.agentRequest = true
agentChannel, agentRequests, err := conn.OpenChannel(ChannelTypeAgent, nil)
if err != nil {
info.agentError = err
} else {
go ssh.DiscardRequests(agentRequests)
info.agent = agent.NewClient(agentChannel)
info.agentChannel = agentChannel
}
reply = true
case RequestTypeEnv:
var payload *sshutil.EnvRequest
if payload, err = sshutil.ParseEnvRequest(request.Payload); err != nil {
log.Err(err).Debug("Corrupted env request payload, discarding")
} else {
log.Values(logger.Values{
"key": payload.Key,
"value": payload.Value,
}).Trace("Client requested env variable")
info.env[payload.Key] = payload.Value
}
reply = true
case RequestTypePTY:
if info.pty, err = sshutil.ParsePTYRequest(request.Payload); err != nil {
log.Err(err).Debug("Corrupted pty request payload, discarding")
} else {
log.Values(logger.Values{
"term": info.pty.Term,
"size": fmt.Sprintf("%dx%d", info.pty.Columns, info.pty.Rows),
}).Trace("Client requested PTY")
}
reply = true
case RequestTypeExec, RequestTypeShell:
info.method = request.Type
select {
case <-done:
default:
close(done)
}
//return
case RequestTypeWindowChange:
if info.windowChange, err = sshutil.ParseWindowChangeRequest(request.Payload); err != nil {
log.Err(err).Debug("Corrupted window change request payload, discarding")
} else {
log.Values(logger.Values{
"size": fmt.Sprintf("%dx%d", info.windowChange.Columns, info.windowChange.Rows),
}).Trace("Client requested window change")
}
reply = true
default:
log.Values(logger.Values{
"request": request.Type,
"payload": hex.EncodeToString(request.Payload),
}).Trace("Client requested something we don't understand, ignored")
info.unsupported = append(info.unsupported, request.Type)
}
if request.WantReply {
if err := request.Reply(reply, reesponse); err != nil {
log.Err(err).Debug("Error sending session channel request reply: terminating")
_ = channel.Close()
select {
case <-done:
default:
close(done)
}
return
}
}
}
select {
case <-done:
default:
close(done)
}
}()
select {
case <-done:
case <-time.After(10 * time.Second):
io.WriteString(channel, "Timeout waiting for your client to send either an exec or shell request\r\n")
}
// Any requests that follow are ignored from hereon forward.
go ssh.DiscardRequests(requests)
// Attempt to request agent forwarding
if info.agent == nil && !info.agentRequest {
agentChannel, agentRequests, err := conn.OpenChannel(ChannelTypeAgent, nil)
if err != nil {
info.agentError = err
} else {
go ssh.DiscardRequests(agentRequests)
info.agent = agent.NewClient(agentChannel)
info.agentChannel = agentChannel
}
}
info.duration = time.Since(info.start)
printSessionInfo(ctx, channel, info, debugPrivateKey)
return nil
})
}
func printSessionInfo(ctx Context, channel ssh.Channel, info *debugSessionInfo, key crypto.PrivateKey) {
if info.agentChannel != nil {
defer info.agentChannel.Close()
}
conn := ctx.Conn().(*ssh.ServerConn)
fmt.Fprintf(channel, "It took your client %s to request %s\r\n\r\n", info.duration, info.method)
fmt.Fprintf(channel, "SSH connection information:\r\n")
fmt.Fprintf(channel, " Server:\r\n")
fmt.Fprintf(channel, " Version: \x1b[1m%s\x1b[0m\r\n", conn.ServerVersion())
fmt.Fprintf(channel, " Client:\r\n")
fmt.Fprintf(channel, " Version: \x1b[1m%s\x1b[0m\r\n", conn.ClientVersion())
fmt.Fprintf(channel, " Username: \x1b[1m%s\x1b[0m\r\n", conn.User())
if conn.Permissions != nil {
if conn.Permissions.CriticalOptions != nil {
fmt.Fprint(channel, " Options:\r\n")
for k := range stringutil.MapKeys(conn.Permissions.CriticalOptions) {
fmt.Fprintf(channel, " ✅ %s=%s\r\n", k, conn.Permissions.CriticalOptions[k])
}
}
if conn.Permissions.Extensions != nil {
fmt.Fprint(channel, " Extensions:\r\n")
for k := range stringutil.MapKeys(conn.Permissions.Extensions) {
fmt.Fprintf(channel, " ✅ %s=%s\r\n", k, conn.Permissions.Extensions[k])
}
}
}
fmt.Fprintf(channel, " Environment: (%d variables sent)\r\n", len(info.env))
for k := range stringutil.MapKeys(info.env) {
fmt.Fprintf(channel, " ✅ %s=%s\r\n", k, info.env[k])
}
if info.pty == nil {
fmt.Fprint(channel, " PTY: not requested\r\n")
} else {
fmt.Fprintf(channel, " PTY: %d cols, %d rows, %dx%d, %q\r\n",
info.pty.Columns, info.pty.Rows, info.pty.Width, info.pty.Height, info.pty.Term)
}
if info.windowChange == nil {
fmt.Fprint(channel, " Window: not requested\r\n")
} else {
fmt.Fprintf(channel, " Window: %d cols, %d rows, %dx%d",
info.windowChange.Columns, info.windowChange.Rows,
info.windowChange.Width, info.windowChange.Height)
}
if len(info.unsupported) > 0 {
fmt.Fprintf(channel, " Requests: (%d unsupported):\r\n", len(info.unsupported))
sort.Strings(info.unsupported)
for _, v := range info.unsupported {
fmt.Fprintf(channel, " ❌ %s\r\n", v)
}
} else {
fmt.Fprint(channel, " Requests: no unsupported requests\r\n")
}
if info.agentRequest {
if info.agentError != nil {
fmt.Fprint(channel, " Agent: requested but unavailable:\r\n")
fmt.Fprintf(channel, " ❌ %v\r\n", info.agentError)
}
} else {
if info.agentError == nil {
fmt.Fprint(channel, " Agent: not requested by client, but accepted, upgrade your client!\r\n")
} else {
fmt.Fprint(channel, " Agent: not requested by client, and client refused our attempt (good!):\r\n")
fmt.Fprintf(channel, " ❌ %v\r\n", info.agentError)
}
}
if info.agent != nil {
fmt.Fprint(channel, " Agent: available, checking keys:\r\n")
keys, err := info.agent.List()
if err != nil {
fmt.Fprintf(channel, " ❌ %v\r\n", err)
} else {
for _, key := range keys {
blob := make([]byte, 8)
rand.Reader.Read(blob)
if _, err := info.agent.Sign(key, blob); err != nil {
fmt.Fprint(channel, " ❌ ")
} else {
fmt.Fprint(channel, " ✅ ")
}
fmt.Fprintf(channel, "%-4d %s %s (%s)\r\n",
sshutil.KeyBits(key), sshutil.KeyFingerprint(key),
key.Comment, sshutil.KeyType(key))
}
}
fmt.Fprint(channel, " Agent: available, checking add/remove:\r\n")
if err = info.agent.Add(agent.AddedKey{
PrivateKey: key,
LifetimeSecs: 5,
Comment: "Test by conduit",
}); err != nil {
fmt.Fprintf(channel, " ❌ Add: %v\r\n", err)
} else {
fmt.Fprint(channel, " ✅ Add\r\n")
}
pk, _ := ssh.NewPublicKey(key.(*rsa.PrivateKey).Public())
if err = info.agent.Remove(pk); err != nil {
fmt.Fprintf(channel, " ❌ Remove: %v\r\n", err)
} else {
fmt.Fprint(channel, " ✅ Remove\r\n")
}
}
}
func StaticSession(message string) ChannelHandler {
return ChannelHandlerFunc(func(ctx Context, channel ssh.Channel, requests <-chan *ssh.Request, _ []byte) error {
go ssh.DiscardRequests(requests)
io.WriteString(channel, message+"\r\n")
return channel.Close()
})
}

47
ssh/handler_tunnel.go Normal file
View File

@@ -0,0 +1,47 @@
package ssh
import (
"context"
"errors"
"net"
"git.maze.io/maze/conduit/logger"
"golang.org/x/crypto/ssh"
)
type Dialer interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
func ForwardTunnel(dialer Dialer) ChannelHandler {
if dialer == nil {
dialer = new(net.Dialer)
}
return ChannelHandlerFunc(func(ctx Context, channel ssh.Channel, requests <-chan *ssh.Request, _ []byte) error {
return errors.New("byez!")
})
}
type PortForwardRequestHandler interface {
HandlePortForwardRequest(ctx Context, raddr, laddr net.Addr) (net.Conn, error)
}
type PortForwardRequestHandlerFunc func(Context, net.Addr, net.Addr) (net.Conn, error)
func (f PortForwardRequestHandlerFunc) HandlePortForwardRequest(ctx Context, raddr, laddr net.Addr) (net.Conn, error) {
return f(ctx, raddr, laddr)
}
func PortForwardDialer(dialer Dialer) PortForwardRequestHandler {
if dialer == nil {
dialer = new(net.Dialer)
}
return PortForwardRequestHandlerFunc(func(ctx Context, raddr, laddr net.Addr) (net.Conn, error) {
log := ctx.(*sshContext).log.Values(logger.Values{
"laddr": laddr.String(),
"raddr": raddr.String(),
})
log.Debug("Dialing port forwarding request")
return dialer.DialContext(context.Background(), raddr.Network(), raddr.String())
})
}

37
ssh/keys.go Normal file
View File

@@ -0,0 +1,37 @@
package ssh
import (
"os"
"strings"
"golang.org/x/crypto/ssh"
"git.maze.io/maze/conduit/logger"
)
func LoadPrivateKey(name string) (ssh.Signer, error) {
if strings.Contains(name, "-----BEGIN") && strings.Contains(name, "PRIVATE KEY-----") {
logger.StandardLog.Debug("Loading private key from string")
return ssh.ParsePrivateKey([]byte(name))
}
logger.StandardLog.Value("path", name).Debug("Loading private key")
b, err := os.ReadFile(name)
if err != nil {
return nil, err
}
return ssh.ParsePrivateKey(b)
}
func LoadPrivateKeyWithPassphrase(name string, passphrase []byte) (ssh.Signer, error) {
if strings.Contains(name, "-----BEGIN") && strings.Contains(name, "PRIVATE KEY-----") {
logger.StandardLog.Debug("Loading private key from string (with passphrase)")
return ssh.ParsePrivateKeyWithPassphrase([]byte(name), passphrase)
}
logger.StandardLog.Value("path", name).Debug("Loading private key (with passphrase)")
b, err := os.ReadFile(name)
if err != nil {
return nil, err
}
return ssh.ParsePrivateKeyWithPassphrase(b, passphrase)
}

115
ssh/server.go Normal file
View File

@@ -0,0 +1,115 @@
package ssh
import (
"net"
"golang.org/x/crypto/ssh"
"git.maze.io/maze/conduit/auth"
"git.maze.io/maze/conduit/internal/netutil"
"git.maze.io/maze/conduit/logger"
)
type Server struct {
// ConnectHandler gets called before accepting new connections.
ConnectHandler []ConnectHandler
// AcceptHandler accepts new SSH connections, it takes care of authenticating, etc.
AcceptHandler AcceptHandler
// Handler per channel type.
ChannelHandler map[string]ChannelHandler
// PortForwardHandler
PortForwardHandler PortForwardRequestHandler
// FIPSMode enables FIPS 140-2 compatible ciphers, key exchanges, etc.
FIPSMode bool // TODO(maze): implement
// Logger for our server.
Logger logger.Structured
// serverConfig is our SSH server configuration.
serverConfig *ssh.ServerConfig
}
func NewServer(keys []ssh.Signer) *Server {
config := new(ssh.ServerConfig)
config.SetDefaults()
config.ServerVersion = "SSH-2.0-conduit"
for _, key := range keys {
config.AddHostKey(key)
}
return &Server{
ChannelHandler: make(map[string]ChannelHandler),
Logger: logger.StandardLog,
serverConfig: config,
}
}
func (s *Server) Serve(l net.Listener) error {
for {
c, err := l.Accept()
if err != nil {
return err
}
go s.handle(c)
}
}
func (s *Server) handle(c net.Conn) {
log := s.Logger.Values(logger.Values{
"client": c.RemoteAddr().String(),
"server": c.LocalAddr().String(),
})
log.Debug("New client connection")
defer func() {
if err := c.Close(); err != nil && !netutil.IsClosing(err) {
log = log.Err(err)
}
log.Debug("Closing client connection")
}()
for _, h := range s.ConnectHandler {
n, err := h.HandleConnect(c)
if err != nil {
log.Err(err).Warn("Error from connect handler, closing client connection")
return
} else if n != nil {
log.Debugf("Replacing client connection with %T", n)
c = n
}
}
// Configure our SSH server.
handler := s.AcceptHandler
if handler == nil {
log.Warn("No accept handler configured, using NO AUTHENTICATION")
handler = auth.None{}
}
// We made it, now let's talk some SSH.
sshConn, channels, requests, err := handler.HandleAccept(c, s.serverConfig)
if err != nil {
log.Err(err).Warn("Error establishing SSH session with client")
return
}
go ssh.DiscardRequests(requests)
log = log.Value("user", sshConn.User())
ctx := newSSHContext(s, c, sshConn, log)
log = log.Value("context", ctx.ID())
log.Value("version", string(sshConn.ClientVersion())).Info("New SSH client")
if err = ctx.handleChannels(channels); err != nil {
if netutil.IsClosing(err) {
log.Err(err).Debug("Client handler terminated")
} else {
log.Err(err).Warn("Error handling channel requests")
}
return
}
}

62
ssh/sshutil/key.go Normal file
View File

@@ -0,0 +1,62 @@
package sshutil
import (
"crypto/sha256"
"encoding/base64"
"math/big"
"golang.org/x/crypto/ssh"
)
func KeyBits(key ssh.PublicKey) int {
if key == nil {
return 0
}
switch key.Type() {
case ssh.KeyAlgoECDSA256:
return 256
case ssh.KeyAlgoSKECDSA256:
return 256
case ssh.KeyAlgoECDSA384:
return 384
case ssh.KeyAlgoECDSA521:
return 521
case ssh.KeyAlgoED25519:
return 256
case ssh.KeyAlgoSKED25519:
return 256
case ssh.KeyAlgoRSA:
var w struct {
Name string
E *big.Int
N *big.Int
Rest []byte `ssh:"rest"`
}
_ = ssh.Unmarshal(key.Marshal(), &w)
return w.N.BitLen()
default:
return 0
}
}
func KeyType(key ssh.PublicKey) string {
if key == nil {
return "<nil>"
}
switch key.Type() {
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoSKECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
return "ECDSA"
case ssh.KeyAlgoED25519, ssh.KeyAlgoSKED25519:
return "ED25519"
case ssh.KeyAlgoRSA:
return "RSA"
default:
return key.Type()
}
}
func KeyFingerprint(key ssh.PublicKey) string {
h := sha256.New()
h.Write(key.Marshal())
return "SHA256:" + base64.RawStdEncoding.EncodeToString(h.Sum(nil))
}

47
ssh/sshutil/request.go Normal file
View File

@@ -0,0 +1,47 @@
package sshutil
import "golang.org/x/crypto/ssh"
type EnvRequest struct {
Key, Value string
}
func ParseEnvRequest(data []byte) (*EnvRequest, error) {
r := new(EnvRequest)
if err := ssh.Unmarshal(data, r); err != nil {
return nil, err
}
return r, nil
}
type PTYRequest struct {
Term string
Columns uint32
Rows uint32
Width uint32
Height uint32
ModeList []byte
}
func ParsePTYRequest(data []byte) (*PTYRequest, error) {
r := new(PTYRequest)
if err := ssh.Unmarshal(data, r); err != nil {
return nil, err
}
return r, nil
}
type WindowChangeRequest struct {
Columns uint32
Rows uint32
Width uint32
Height uint32
}
func ParseWindowChangeRequest(data []byte) (*WindowChangeRequest, error) {
r := new(WindowChangeRequest)
if err := ssh.Unmarshal(data, r); err != nil {
return nil, err
}
return r, nil
}

248
storage/codec/binary.go Normal file
View File

@@ -0,0 +1,248 @@
package codec
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"math"
"golang.org/x/crypto/ssh"
)
func init() {
Register("binray", func() Codec { return binaryCodec{order: binary.NativeEndian} })
Register("be", func() Codec { return binaryCodec{order: binary.BigEndian} })
Register("le", func() Codec { return binaryCodec{order: binary.LittleEndian} })
Register("ssh", func() Codec { return sshCodec{} })
}
type binaryCodec struct {
order binary.ByteOrder
scratch [16]byte
}
func (binaryCodec) Type() string { return "binary" }
func (codec binaryCodec) Encode(value any) ([]byte, error) {
switch value := value.(type) {
case bool:
if value {
return []byte{1}, nil
}
return []byte{0}, nil
case *bool:
if *value {
return []byte{1}, nil
}
return []byte{0}, nil
case int:
codec.order.PutUint64(codec.scratch[:], uint64(value))
return codec.scratch[:8], nil
case *int:
return codec.Encode(*value)
case int8:
return []byte{uint8(value)}, nil
case *int8:
return []byte{uint8(*value)}, nil
case int16:
codec.order.PutUint16(codec.scratch[:], uint16(value))
return codec.scratch[:2], nil
case *int16:
return codec.Encode(*value)
case int32:
codec.order.PutUint32(codec.scratch[:], uint32(value))
return codec.scratch[:4], nil
case *int32:
return codec.Encode(*value)
case int64:
codec.order.PutUint64(codec.scratch[:], uint64(value))
return codec.scratch[:8], nil
case *int64:
return codec.Encode(*value)
case uint:
codec.order.PutUint64(codec.scratch[:], uint64(value))
return codec.scratch[:8], nil
case *uint:
return codec.Encode(*value)
case uint8:
return []byte{value}, nil
case *uint8:
return []byte{*value}, nil
case uint16:
codec.order.PutUint16(codec.scratch[:], value)
return codec.scratch[:2], nil
case *uint16:
return codec.Encode(*value)
case uint32:
codec.order.PutUint32(codec.scratch[:], value)
return codec.scratch[:4], nil
case *uint32:
return codec.Encode(*value)
case uint64:
codec.order.PutUint64(codec.scratch[:], value)
return codec.scratch[:8], nil
case *uint64:
return codec.Encode(*value)
case float32:
codec.order.PutUint32(codec.scratch[:], math.Float32bits(value))
return codec.scratch[:4], nil
case *float32:
codec.order.PutUint32(codec.scratch[:], math.Float32bits(*value))
return codec.scratch[:4], nil
case float64:
codec.order.PutUint64(codec.scratch[:], math.Float64bits(value))
return codec.scratch[:8], nil
case *float64:
codec.order.PutUint64(codec.scratch[:], math.Float64bits(*value))
return codec.scratch[:8], nil
case complex64:
codec.order.PutUint32(codec.scratch[0:], math.Float32bits(real(value)))
codec.order.PutUint32(codec.scratch[4:], math.Float32bits(imag(value)))
return codec.scratch[:8], nil
case *complex64:
codec.order.PutUint32(codec.scratch[0:], math.Float32bits(real(*value)))
codec.order.PutUint32(codec.scratch[4:], math.Float32bits(imag(*value)))
return codec.scratch[:8], nil
case complex128:
codec.order.PutUint64(codec.scratch[0:], math.Float64bits(real(value)))
codec.order.PutUint64(codec.scratch[4:], math.Float64bits(imag(value)))
return codec.scratch[:16], nil
case *complex128:
codec.order.PutUint64(codec.scratch[0:], math.Float64bits(real(*value)))
codec.order.PutUint64(codec.scratch[4:], math.Float64bits(imag(*value)))
return codec.scratch[:16], nil
case string:
n := binary.PutUvarint(codec.scratch[:], uint64(len(value)))
return append(codec.scratch[:n], []byte(value)...), nil
case *string:
n := binary.PutUvarint(codec.scratch[:], uint64(len(*value)))
return append(codec.scratch[:n], []byte(*value)...), nil
case []byte:
n := binary.PutUvarint(codec.scratch[:], uint64(len(value)))
return append(codec.scratch[:n], value...), nil
case *[]byte:
n := binary.PutUvarint(codec.scratch[:], uint64(len(*value)))
return append(codec.scratch[:n], *value...), nil
default:
return nil, fmt.Errorf("codec: don't know how to binary encode %T", value)
}
}
func (codec binaryCodec) Decode(data []byte, value any) error {
switch value := value.(type) {
case *bool:
if len(data) < 1 {
return io.ErrUnexpectedEOF
}
*value = data[0] != 0
case *int:
if len(data) < 8 {
return io.ErrUnexpectedEOF
}
*value = int(codec.order.Uint64(data))
case *int8:
if len(data) < 1 {
return io.ErrUnexpectedEOF
}
*value = int8(data[0])
case *int16:
if len(data) < 2 {
return io.ErrUnexpectedEOF
}
*value = int16(codec.order.Uint16(data))
case *int32:
if len(data) < 4 {
return io.ErrUnexpectedEOF
}
*value = int32(codec.order.Uint32(data))
case *int64:
if len(data) < 8 {
return io.ErrUnexpectedEOF
}
*value = int64(codec.order.Uint64(data))
case *uint:
if len(data) < 8 {
return io.ErrUnexpectedEOF
}
*value = uint(codec.order.Uint64(data))
case *uint8:
if len(data) < 1 {
return io.ErrUnexpectedEOF
}
*value = data[0]
case *uint16:
if len(data) < 1 {
return io.ErrUnexpectedEOF
}
*value = codec.order.Uint16(data)
case *uint32:
if len(data) < 4 {
return io.ErrUnexpectedEOF
}
*value = codec.order.Uint32(data)
case *uint64:
if len(data) < 8 {
return io.ErrUnexpectedEOF
}
*value = codec.order.Uint64(data)
case *float32:
if len(data) < 4 {
return io.ErrUnexpectedEOF
}
*value = math.Float32frombits(codec.order.Uint32(data))
case *float64:
if len(data) < 8 {
return io.ErrUnexpectedEOF
}
*value = math.Float64frombits(codec.order.Uint64(data))
case *complex64:
if len(data) < 8 {
return io.ErrUnexpectedEOF
}
*value = complex(
math.Float32frombits(codec.order.Uint32(data[0:])),
math.Float32frombits(codec.order.Uint32(data[4:])),
)
case *complex128:
if len(data) < 16 {
return io.ErrUnexpectedEOF
}
*value = complex(
math.Float64frombits(codec.order.Uint64(data[0:])),
math.Float64frombits(codec.order.Uint64(data[8:])),
)
case *string:
r := bytes.NewReader(data)
n, err := binary.ReadUvarint(r)
if err != nil {
return err
}
if uint64(r.Len()) < n {
return io.ErrUnexpectedEOF
}
*value = string(data[len(data)-r.Len():])
case *[]byte:
r := bytes.NewReader(data)
n, err := binary.ReadUvarint(r)
if err != nil {
return err
}
if uint64(r.Len()) < n {
return io.ErrUnexpectedEOF
}
copy(*value, data[len(data)-r.Len():])
default:
return fmt.Errorf("codec: don't know how to binary decode %T", value)
}
return nil
}
type sshCodec struct{}
func (sshCodec) Type() string { return "ssh" }
func (sshCodec) Encode(value any) ([]byte, error) { return ssh.Marshal(value), nil }
func (sshCodec) Decode(data []byte, value any) error { return ssh.Unmarshal(data, value) }

66
storage/codec/codec.go Normal file
View File

@@ -0,0 +1,66 @@
package codec
import (
"fmt"
"io"
)
type Codec interface {
Type() string
Encode(any) ([]byte, error)
Decode([]byte, any) error
}
type Stream interface {
Encode(any) error
Decode(any) error
}
var (
codecs = make(map[string]func() Codec)
streams = make(map[string]func(io.Reader, io.Writer) Stream)
)
func Register(name string, create func() Codec) {
if _, dupe := codecs[name]; dupe {
panic(fmt.Sprintf("codec: duplicate codec %q registered", name))
}
codecs[name] = create
}
func New(name string) (Codec, error) {
if f, ok := codecs[name]; ok {
return f(), nil
}
return nil, fmt.Errorf("codec: no %q codec registered", name)
}
func Must(name string) Codec {
c, err := New(name)
if err != nil {
panic(err)
}
return c
}
func RegisterStream(name string, create func(io.Reader, io.Writer) Stream) {
if _, dupe := streams[name]; dupe {
panic(fmt.Sprintf("codec: duplicate stream %q registered", name))
}
streams[name] = create
}
func NewStream(name string, r io.Reader, w io.Writer) (Stream, error) {
if f, ok := streams[name]; ok {
return f(r, w), nil
}
return nil, fmt.Errorf("codec: no %q stream registered", name)
}
func MustStream(name string, r io.Reader, w io.Writer) Stream {
s, err := NewStream(name, r, w)
if err != nil {
panic(err)
}
return s
}

77
storage/codec/default.go Normal file
View File

@@ -0,0 +1,77 @@
package codec
import (
"encoding/json"
"io"
"github.com/goccy/go-yaml"
)
func init() {
Register("json", func() Codec { return jsonCodec{} })
Register("yaml", func() Codec { return yamlCodec{} })
}
type jsonCodec struct{}
func (jsonCodec) Type() string { return "json" }
func (jsonCodec) Encode(value any) ([]byte, error) { return json.Marshal(value) }
func (jsonCodec) Decode(data []byte, value any) error { return json.Unmarshal(data, value) }
type jsonStream struct {
decoder *json.Decoder
encoder *json.Encoder
}
type JSONOption func(*json.Encoder)
func NewJSONStream(r io.Reader, w io.Writer, options ...JSONOption) Stream {
s := &jsonStream{
decoder: json.NewDecoder(r),
encoder: json.NewEncoder(w),
}
for _, option := range options {
option(s.encoder)
}
return s
}
func JSONIndent(prefix, indent string) JSONOption {
return func(e *json.Encoder) {
e.SetIndent(prefix, indent)
}
}
func JSONEscapeHTML(on bool) JSONOption {
return func(e *json.Encoder) {
e.SetEscapeHTML(on)
}
}
func (s jsonStream) Encode(value any) error { return s.encoder.Encode(value) }
func (s jsonStream) Decode(value any) error { return s.decoder.Decode(value) }
type yamlCodec struct{}
func (yamlCodec) Type() string { return "yaml" }
func (yamlCodec) Encode(value any) ([]byte, error) { return yaml.Marshal(value) }
func (yamlCodec) Decode(data []byte, value any) error { return yaml.Unmarshal(data, value) }
type yamlStream struct {
decoder *yaml.Decoder
encoder *yaml.Encoder
}
func NewYaMLStream(r io.Reader, w io.Writer, options ...yaml.EncodeOption) Stream {
s := &yamlStream{
decoder: yaml.NewDecoder(r),
encoder: yaml.NewEncoder(w),
}
for _, option := range options {
option(s.encoder)
}
return s
}
func (s yamlStream) Encode(value any) error { return s.encoder.Encode(value) }
func (s yamlStream) Decode(value any) error { return s.decoder.Decode(value) }

225
storage/io.go Normal file
View File

@@ -0,0 +1,225 @@
package storage
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"syscall"
)
type ValueError struct {
Value any
}
func (err ValueError) Error() string {
return fmt.Sprintf("kv: can't store value of type %T", err.Value)
}
type readSeekerKV struct {
mu sync.Mutex
r io.ReadSeeker
c io.Closer
sep rune
}
func (kv *readSeekerKV) scan(key string) (data string, ok bool, err error) {
kv.mu.Lock()
if _, err = kv.r.Seek(0, io.SeekStart); err != nil {
return
}
s := bufio.NewScanner(kv.r)
for s.Scan() {
line := s.Text()
if i := strings.IndexRune(line, kv.sep); i > -1 && line[:i] == key {
kv.mu.Unlock()
return line[i+1:], true, nil
}
}
err = s.Err()
kv.mu.Unlock()
return
}
func (kv *readSeekerKV) Has(key string) bool {
_, ok, err := kv.scan(key)
return ok && err == nil
}
func (kv *readSeekerKV) Get(key string) (value any, ok bool) {
value, ok, _ = kv.scan(key)
return
}
func (kv *readSeekerKV) Close() error {
if kv.c == nil {
return nil
}
return kv.c.Close()
}
func OpenKV(name string, sep rune) (KV, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
return &readSeekerKV{
r: f,
sep: sep,
}, nil
}
type readSeekWriterKV struct {
readSeekerKV
w io.WriteSeeker
}
type truncater interface {
Truncate(int64) error
}
func (kv *readSeekWriterKV) Set(key string, value any) error {
var line string
switch value := value.(type) {
case string:
line = key + string(kv.sep) + value + "\n"
case []string:
line = key + string(kv.sep) + strings.Join(value, string(kv.sep)) + "\n"
default:
return ValueError{Value: value}
}
kv.mu.Lock()
defer kv.mu.Unlock()
if _, err := kv.r.Seek(0, io.SeekStart); err != nil {
return err
}
var (
lines []string
found bool
scanner = bufio.NewScanner(kv.r)
)
for scanner.Scan() {
text := scanner.Text()
if i := strings.IndexRune(text, kv.sep); i > -1 {
if found = text[:i] == key; found {
lines = append(lines, line)
} else {
lines = append(lines, text)
}
} else {
lines = append(lines, line)
}
}
if !found {
lines = append(lines, line)
}
// Writing strategy: if it's a file, write to a new file and move it over.
if f, ok := kv.w.(*os.File); ok {
return kv.replaceFile(f, lines)
}
if t, ok := kv.w.(truncater); ok {
if err := t.Truncate(0); err != nil {
return err
}
}
if _, err := kv.w.Seek(0, io.SeekStart); err != nil {
return err
}
for _, line := range lines {
if _, err := io.WriteString(kv.w, line+"\n"); err != nil {
return err
}
}
return nil
}
func (kv *readSeekWriterKV) replaceFile(f *os.File, lines []string) (err error) {
var (
prev = f.Name()
info os.FileInfo
)
if info, err = f.Stat(); err != nil {
return
}
var (
name = "." + filepath.Base(f.Name()) + ".*"
n *os.File
)
if n, err = os.CreateTemp(filepath.Dir(f.Name()), name); err != nil {
return
}
name = n.Name()
// Replicate original file mode
if err = os.Chmod(name, info.Mode()); err != nil {
_ = n.Close()
_ = os.Remove(name)
return
}
// Replicate original file ownership
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
// This may fail if we aren't allowed, ignore it.
_ = os.Chown(name, int(stat.Uid), int(stat.Gid))
}
// Write lines to tempfile.
for _, line := range lines {
if _, err = io.WriteString(n, line+"\n"); err != nil {
_ = n.Close()
_ = os.Remove(name)
return
}
}
if err = n.Close(); err != nil {
_ = os.Remove(name)
return
}
// Close original file and replace it.
_ = f.Close()
if err = os.Rename(name, prev); err != nil {
return
}
// Reopen file and replace our readers/writers/closers
if f, err = os.OpenFile(prev, os.O_APPEND|os.O_RDWR, info.Mode()|os.ModePerm); err != nil {
return
}
kv.r = f
kv.w = f
kv.c = f
return
}
func OpenWritableKV(name string, sep rune, perm os.FileMode) (WritableKV, error) {
f, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, perm)
if err != nil {
return nil, err
}
return &readSeekWriterKV{
readSeekerKV: readSeekerKV{
r: f,
c: f,
sep: sep,
},
w: f,
}, nil
}

124
storage/io_test.go Normal file
View File

@@ -0,0 +1,124 @@
package storage
import (
"errors"
"os"
"path/filepath"
"reflect"
"testing"
)
func TestKV(t *testing.T) {
kv, err := OpenKV(filepath.Join("testdata", "kv"), ':')
if err != nil {
t.Skip(err)
return
}
tests := []struct {
Key string
Value any
OK bool
}{
{"test", "data", true},
{"empty", "", true},
{"", "emptykey", true},
{"nonexistant", nil, false},
{"ignored line because not relevant", nil, false},
}
for _, test := range tests {
t.Run(test.Key, func(it *testing.T) {
value, ok := kv.Get(test.Key)
if ok != test.OK {
t.Errorf("expected ok %t, got %t", test.OK, ok)
}
if ok && !reflect.DeepEqual(value, test.Value) {
t.Errorf("expected value %v, got %v", test.Value, value)
}
})
}
}
func TestWritableKV(t *testing.T) {
orgfile := filepath.Join("testdata", "kv")
b, err := os.ReadFile(orgfile)
if err != nil {
t.Skip(err)
}
w, err := os.CreateTemp(os.TempDir(), "kv.*")
if err != nil {
t.Skip(err)
return
}
testfile := w.Name()
t.Logf("using a copy of %s as %s", orgfile, testfile)
defer os.Remove(testfile)
if _, err = w.Write(b); err != nil {
return
}
if err = w.Close(); err != nil {
return
}
kv, err := OpenWritableKV(testfile, ':', 0640)
if err != nil {
t.Skip(err)
return
}
defer kv.Close()
t.Run("write", func(t *testing.T) {
writeTests := []struct {
Key string
Value any
Error error
}{
{"test", "newdata", nil},
{"strings", []string{"test", "data"}, nil},
{"int", 42, ValueError{Value: 42}},
}
for _, test := range writeTests {
t.Run(test.Key, func(it *testing.T) {
err := kv.Set(test.Key, test.Value)
if err != nil {
if test.Error == nil {
it.Errorf("unepxected error %q (%T)", err, err)
} else if !errors.Is(err, test.Error) {
it.Errorf("expected error %q, but got %q (%T)", test.Error, err, err)
}
return
} else if err == nil && test.Error != nil {
it.Errorf("expected error %q, but got nil", err)
}
})
}
})
t.Run("read", func(t *testing.T) {
readTests := []struct {
Key string
Value any
OK bool
}{
{"test", "newdata", true},
{"empty", "", true},
{"", "emptykey", true},
{"strings", []string{"test", "data"}, true},
{"nonexistant", nil, false},
{"ignored line because not relevant", nil, false},
}
for _, test := range readTests {
t.Run(test.Key, func(it *testing.T) {
value, ok := kv.Get(test.Key)
if ok != test.OK {
t.Errorf("expected ok %t, got %t", test.OK, ok)
}
if ok && !reflect.DeepEqual(value, test.Value) {
t.Errorf("expected value %v, got %v", test.Value, value)
}
})
}
})
}

61
storage/kv.go Normal file
View File

@@ -0,0 +1,61 @@
package storage
import (
"encoding/json"
"git.maze.io/maze/conduit/storage/codec"
)
// KV implements a key-value store.
type KV interface {
// Has checks if the key is present.
Has(key string) bool
// Get a value by key.
Get(key string) (value []byte, ok bool)
}
// WritableKV can store key-value items.
type WritableKV interface {
KV
// Close the key-value storage.
Close() error
// DeleteKey removes a key.
Delete(key string) error
// Set a value by key.
Set(key string, value []byte) error
}
type EncodedKV interface {
Has(key string) bool
Get(key string, value any) (ok bool, err error)
}
type EncodedWritableKV interface {
EncodedKV
Close() error
Delete(key string) error
Set(key string, value []byte) error
}
type encodedKV struct {
kv KV
encode func(any) ([]byte, error)
decode func([]byte, any) error
}
func (kv encodedKV) Has(key)
func NewEncodedKV(kv KV, codec codec.Codec) EncodedKV {
return &encodedKV{
kv: kv,
encode: json.Marshal,
decode: json.Unmarshal,
}
}

4
storage/testdata/kv vendored Normal file
View File

@@ -0,0 +1,4 @@
test:data
ignored line because not relevant
empty:
:emptykey

7
testdata/conduit.ed25519 vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDRyBi3zVFplREzSzhuwqZInx5wRAk67DCAeODMiZ4XhQAAAKDwAZrd8AGa
3QAAAAtzc2gtZWQyNTUxOQAAACDRyBi3zVFplREzSzhuwqZInx5wRAk67DCAeODMiZ4XhQ
AAAEDhHNQENt5Ldinm1TSlGJTVdjniydYpsTNXYmyLK2BUHNHIGLfNUWmVETNLOG7Cpkif
HnBECTrsMIB44MyJnheFAAAAGENvbmR1aXQgZGVtbyBwcml2YXRlIGtleQECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/conduit.ed25519.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINHIGLfNUWmVETNLOG7CpkifHnBECTrsMIB44MyJnheF Conduit demo private key

27
testdata/conduit.rsa vendored Normal file
View File

@@ -0,0 +1,27 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEArnzuzlKni2ZFF+6hmrTpO/Z2U0Bqgmx8U6es/DW/udhyYkQ14C32
P98y1/95DnY1a0Lo8BBH2w+iFToxd4Jw5IGL45yUTlxIr95NlAKWM62yXmxaIXBT665N1q
m8znjEF3IhLkjciVd9xTGAZ2fGPmK3+WuMeUdKcW5nQpsUjoYoTiGWJSjYz6t1FX18LYfz
VuCoh1JJrh+neMeA9zjCfsSnivRLtObMwVsER2N76ZX9EmIsA3d8T5RWpTxtlMwN4//pHb
UH9KjSRslqY0Yx7/QbyTD/xvmNgnJL8oHrWAX3L1/SRm/6fBtSclOeTgw+ZmsQ6wkhZvGq
g27wi5ngGwAAA9DJal7zyWpe8wAAAAdzc2gtcnNhAAABAQCufO7OUqeLZkUX7qGatOk79n
ZTQGqCbHxTp6z8Nb+52HJiRDXgLfY/3zLX/3kOdjVrQujwEEfbD6IVOjF3gnDkgYvjnJRO
XEiv3k2UApYzrbJebFohcFPrrk3WqbzOeMQXciEuSNyJV33FMYBnZ8Y+Yrf5a4x5R0pxbm
dCmxSOhihOIZYlKNjPq3UVfXwth/NW4KiHUkmuH6d4x4D3OMJ+xKeK9Eu05szBWwRHY3vp
lf0SYiwDd3xPlFalPG2UzA3j/+kdtQf0qNJGyWpjRjHv9BvJMP/G+Y2CckvygetYBfcvX9
JGb/p8G1JyU55ODD5maxDrCSFm8aqDbvCLmeAbAAAAAwEAAQAAAQEAkqLjffjwXLIhtq8Q
mJcYuw+w+N3VpK3O/e6X7YyuB1zjI7n3HOMDY0IL1IIaFhE5a17bq4PDH1HQAM7a63hvr1
k/WpUn/YKIg2PrBkv2No/uqnOceyWPIS1mtNQIm+vZv2pmgCMzUyh3xdSH+F65t4v22GGN
uA41fYYuuUbiy7KHsJy0JYE04VMO7XipWUOJoaqOBosS/gnRKd4e1axa8kAbOQbpzi4C7a
EVNHiwiWwSuuBtLzWC1NvTMNVn6dQCmKHcLCdumxZ0iXzz/nvFFkfNkYE5Gn0wdeMZK4Wh
ffAOqjcA94D+UNzK0pMzO1o4H54wPraJy+0LUP0LPQAm4QAAAIBPHtw0GedG6SKH4hMhsO
KeKq4tXHu7blSjsMtV+OGYAhl3LT4JOCDUjLczUU8gKbI6wwR3YIk9K/1s7G2DOYbil01F
5IphOYEYRyzxrDjKm/hnI0GhRaScipgxICqu1HlIR1CtC36ZRpamhufC0P0h4+eY1t1saT
4Tppu3NAlSLwAAAIEA2EVuuPOhgQxoK21cxG4mdpjYmNbhBbr1xPWO3mmxN3iyilE5wuSY
A/r3HDJl2qzLpaG9CQ4VT9GywuGX3lTXiOOc98zNAqyUYgDmCcZqlsaeSt+WJVBWgBRCz4
BwM9MZ4wLSzvBWSZiIls3OZuVWjLGDZuImyfWmN8YV5aPCMsUAAACBAM6KkL5IURjyD3gi
sQA/1Qtz4j7fMou69GqhMhZPhvt/BPGe761ciH1+4ixweb+lHCqZjW5MB9LKYH1Dpjq7X1
maiOLcPYPBKvSwYQmCcGKct1DXY8623QKYZHqmBa7NJZUjuvC2reIc/a2uwWRV0CKXexpw
0mMIZroWkk5xUXVfAAAAGENvbmR1aXQgZGVtbyBwcml2YXRlIGtleQEC
-----END OPENSSH PRIVATE KEY-----

1
testdata/conduit.rsa.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCufO7OUqeLZkUX7qGatOk79nZTQGqCbHxTp6z8Nb+52HJiRDXgLfY/3zLX/3kOdjVrQujwEEfbD6IVOjF3gnDkgYvjnJROXEiv3k2UApYzrbJebFohcFPrrk3WqbzOeMQXciEuSNyJV33FMYBnZ8Y+Yrf5a4x5R0pxbmdCmxSOhihOIZYlKNjPq3UVfXwth/NW4KiHUkmuH6d4x4D3OMJ+xKeK9Eu05szBWwRHY3vplf0SYiwDd3xPlFalPG2UzA3j/+kdtQf0qNJGyWpjRjHv9BvJMP/G+Y2CckvygetYBfcvX9JGb/p8G1JyU55ODD5maxDrCSFm8aqDbvCLmeAb Conduit demo private key

7
testdata/example.ed25519 vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCcAzG/ziVMEPefEVafcSXLygbyZ6mgbUqvn2GQhJ+awwAAAJhCdsmgQnbJ
oAAAAAtzc2gtZWQyNTUxOQAAACCcAzG/ziVMEPefEVafcSXLygbyZ6mgbUqvn2GQhJ+aww
AAAEAO9YENyIi8hSQ1VYifWhK/+r0q6+zieVq/QIKttoxapZwDMb/OJUwQ958RVp9xJcvK
BvJnqaBtSq+fYZCEn5rDAAAAEEV4YW1wbGUgdXNlciBrZXkBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

1
testdata/example.ed25519-cert.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIH2JTPouysbfVmVz7meUXWd3Gy0vbaDFBlg27Eg1i1MNAAAAIJwDMb/OJUwQ958RVp9xJcvKBvJnqaBtSq+fYZCEn5rDAAAAAAAAAAAAAAABAAAAB2V4YW1wbGUAAAATAAAAB2V4YW1wbGUAAAAEZGVtbwAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEArnzuzlKni2ZFF+6hmrTpO/Z2U0Bqgmx8U6es/DW/udhyYkQ14C32P98y1/95DnY1a0Lo8BBH2w+iFToxd4Jw5IGL45yUTlxIr95NlAKWM62yXmxaIXBT665N1qm8znjEF3IhLkjciVd9xTGAZ2fGPmK3+WuMeUdKcW5nQpsUjoYoTiGWJSjYz6t1FX18LYfzVuCoh1JJrh+neMeA9zjCfsSnivRLtObMwVsER2N76ZX9EmIsA3d8T5RWpTxtlMwN4//pHbUH9KjSRslqY0Yx7/QbyTD/xvmNgnJL8oHrWAX3L1/SRm/6fBtSclOeTgw+ZmsQ6wkhZvGqg27wi5ngGwAAARQAAAAMcnNhLXNoYTItNTEyAAABAFK5fNyZp5ZwEXsDaOcsxP3+PqycVunqeGig0WdN61amDShHu3r+tO03iDlu1C8nVWdGfwIrfh9CekxorozVpCR9KMkahozcPGcWo7tYN7/CRfJt4PrN4AO1mAhhTC90V1awBaVI3fjK3fOk0xVkkyCWBsFI1Dks8ImnAuX2eFgG0IFboN0fa9yKDPx2cOPVlrV3Coq49q0ivu0aipNd+M80ayntlKFCr4A/t2lTBeKHeiqSP6sP7puN3iWZgztDFkMxNJezMnu0dTq1/5rhSdzcoX1SYkOMS32J+njcXjC8tWsFlhRSPsx7OqFdFVX1NPgc5IArw3Tj+mQGrKdnYsc= Example user key

1
testdata/example.ed25519.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJwDMb/OJUwQ958RVp9xJcvKBvJnqaBtSq+fYZCEn5rD Example user key

7
testdata/keys/test_id_ed25519_a vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBaNZbepRJPtVVKzG4ZWlaZCNKdUxVBrox2iip+/laBmwAAAIi3gDMRt4Az
EQAAAAtzc2gtZWQyNTUxOQAAACBaNZbepRJPtVVKzG4ZWlaZCNKdUxVBrox2iip+/laBmw
AAAEBk1ZexnhZdDjMBYGVc1MfVIJPYxLsqxuVT/7QrAPJexlo1lt6lEk+1VUrMbhlaVpkI
0p1TFUGujHaKKn7+VoGbAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_a.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFo1lt6lEk+1VUrMbhlaVpkI0p1TFUGujHaKKn7+VoGb

7
testdata/keys/test_id_ed25519_b vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAgPXUIzXl93gb5DlyTfpyZtQtlvVLUYi2yvTvOzI5IqwAAAIj0LYbK9C2G
ygAAAAtzc2gtZWQyNTUxOQAAACAgPXUIzXl93gb5DlyTfpyZtQtlvVLUYi2yvTvOzI5Iqw
AAAECLb8tIZGTc0AocsQx7FxKHoFyQ5fR8aYYitu2NiujlgyA9dQjNeX3eBvkOXJN+nJm1
C2W9UtRiLbK9O87MjkirAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_b.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICA9dQjNeX3eBvkOXJN+nJm1C2W9UtRiLbK9O87Mjkir

7
testdata/keys/test_id_ed25519_c vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDm3ZNg2OwpyOgLse4weKA0AoVDc9rY5XEHw/y8wzBhLgAAAIhJAmhKSQJo
SgAAAAtzc2gtZWQyNTUxOQAAACDm3ZNg2OwpyOgLse4weKA0AoVDc9rY5XEHw/y8wzBhLg
AAAEDjbxojnzIkz9CuH3QANAkk7Kox3Oz85Nq2uVlhf6kmJObdk2DY7CnI6Aux7jB4oDQC
hUNz2tjlcQfD/LzDMGEuAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_c.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIObdk2DY7CnI6Aux7jB4oDQChUNz2tjlcQfD/LzDMGEu

7
testdata/keys/test_id_ed25519_d vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDBQODCk5lDPxFVJnX3xc/yVM8Xq3MVt0RDkjWdd9pDkwAAAIgFh+AQBYfg
EAAAAAtzc2gtZWQyNTUxOQAAACDBQODCk5lDPxFVJnX3xc/yVM8Xq3MVt0RDkjWdd9pDkw
AAAEAMdFkVlAmVL0ecGpOYK+o/uJvunuJIiScQbke8xRLfS8FA4MKTmUM/EVUmdffFz/JU
zxercxW3REOSNZ132kOTAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_d.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMFA4MKTmUM/EVUmdffFz/JUzxercxW3REOSNZ132kOT

7
testdata/keys/test_id_ed25519_e vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCSAEB5cNSSrzWoPCCq53E2AvYzV76T3QjWGj7TFZaNYwAAAIhAyiXGQMol
xgAAAAtzc2gtZWQyNTUxOQAAACCSAEB5cNSSrzWoPCCq53E2AvYzV76T3QjWGj7TFZaNYw
AAAED2o3NXUBsaKL1aKda3pxLimBGTcPuzkRhYZ5Dys7ERw5IAQHlw1JKvNag8IKrncTYC
9jNXvpPdCNYaPtMVlo1jAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_e.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJIAQHlw1JKvNag8IKrncTYC9jNXvpPdCNYaPtMVlo1j

7
testdata/keys/test_id_ed25519_f vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCanTkfwlCIaVcacPGL3jeNVwoXbWmK+akknwunyq6uWgAAAIg4gKjhOICo
4QAAAAtzc2gtZWQyNTUxOQAAACCanTkfwlCIaVcacPGL3jeNVwoXbWmK+akknwunyq6uWg
AAAEBYShYJSTJICVjAX7WyLePUtRfF8ThMl6BUHsgGUcLLS5qdOR/CUIhpVxpw8YveN41X
ChdtaYr5qSSfC6fKrq5aAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_f.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJqdOR/CUIhpVxpw8YveN41XChdtaYr5qSSfC6fKrq5a

7
testdata/keys/test_id_ed25519_g vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCmgF9b/O4j4Mp/gD3akcmOQg3uLiooXqTXGy0IzQCreQAAAIhOHBGGThwR
hgAAAAtzc2gtZWQyNTUxOQAAACCmgF9b/O4j4Mp/gD3akcmOQg3uLiooXqTXGy0IzQCreQ
AAAEDEkL3EQCRMpsA2WCw9ItOv+BOaJAj3ukvbr3p/9VsXmaaAX1v87iPgyn+APdqRyY5C
De4uKihepNcbLQjNAKt5AAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_g.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKaAX1v87iPgyn+APdqRyY5CDe4uKihepNcbLQjNAKt5

7
testdata/keys/test_id_ed25519_h vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCuBduE8JMIOG256dOIHsmL+9746xpONczBzGGwUv9DzgAAAIgVDPWqFQz1
qgAAAAtzc2gtZWQyNTUxOQAAACCuBduE8JMIOG256dOIHsmL+9746xpONczBzGGwUv9Dzg
AAAECOgmmc+eoWrBB0hY1yTB7NTS9oIzDSnYYUPSX/p3E4Cq4F24Twkwg4bbnp04geyYv7
3vjrGk41zMHMYbBS/0POAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_h.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK4F24Twkwg4bbnp04geyYv73vjrGk41zMHMYbBS/0PO

7
testdata/keys/test_id_ed25519_i vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAP2hN6igLqAA4zBhse8CSStWYPO31HUPukb7ixYNG85AAAAIie1TyintU8
ogAAAAtzc2gtZWQyNTUxOQAAACAP2hN6igLqAA4zBhse8CSStWYPO31HUPukb7ixYNG85A
AAAEDgLZocjGTRre5cBGYPiQhrgW1SS+hyCz1G86Z2Pgg9Tw/aE3qKAuoADjMGGx7wJJK1
Zg87fUdQ+6RvuLFg0bzkAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_i.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA/aE3qKAuoADjMGGx7wJJK1Zg87fUdQ+6RvuLFg0bzk

7
testdata/keys/test_id_ed25519_j vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDB4NfG2rYL07aZjhY2QWpHqcosZ0XDIYv1VsU0AwYUAwAAAIgAMqKwADKi
sAAAAAtzc2gtZWQyNTUxOQAAACDB4NfG2rYL07aZjhY2QWpHqcosZ0XDIYv1VsU0AwYUAw
AAAEBE92CUagxRtrqwPnOk3NxWJ38lMoI1jCrXF3iZYDjDysHg18batgvTtpmOFjZBakep
yixnRcMhi/VWxTQDBhQDAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_j.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMHg18batgvTtpmOFjZBakepyixnRcMhi/VWxTQDBhQD

7
testdata/keys/test_id_ed25519_k vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDlA9NjGTu0MfR5t/6Qbw+X9YhAxY/e4STc8l5l5oiMzwAAAIixZyEHsWch
BwAAAAtzc2gtZWQyNTUxOQAAACDlA9NjGTu0MfR5t/6Qbw+X9YhAxY/e4STc8l5l5oiMzw
AAAECJz/+yGGs6Tv3DAb/Bmodgj4nsHShB+qn4dPIIlWE+BuUD02MZO7Qx9Hm3/pBvD5f1
iEDFj97hJNzyXmXmiIzPAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_k.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOUD02MZO7Qx9Hm3/pBvD5f1iEDFj97hJNzyXmXmiIzP

7
testdata/keys/test_id_ed25519_l vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACA9c0oRAlg1bQV4aASHj4fsBCtsuvWHG70M7I270P/DHgAAAIh1NgxzdTYM
cwAAAAtzc2gtZWQyNTUxOQAAACA9c0oRAlg1bQV4aASHj4fsBCtsuvWHG70M7I270P/DHg
AAAEAKRU+DiCxLCMuuN89DphFJ9pWshOGymhWBFyD37VgkoD1zShECWDVtBXhoBIePh+wE
K2y69YcbvQzsjbvQ/8MeAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_l.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID1zShECWDVtBXhoBIePh+wEK2y69YcbvQzsjbvQ/8Me

7
testdata/keys/test_id_ed25519_m vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDwr8Ap993ioXt7nVs16V9Q8K8rem1ebmT6DL0fncKqyQAAAIignn3boJ59
2wAAAAtzc2gtZWQyNTUxOQAAACDwr8Ap993ioXt7nVs16V9Q8K8rem1ebmT6DL0fncKqyQ
AAAECUi0UUw2RmFxKAlwcHZxqkT3cCbcNqNRk/rZnkZaFIl/CvwCn33eKhe3udWzXpX1Dw
ryt6bV5uZPoMvR+dwqrJAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_m.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPCvwCn33eKhe3udWzXpX1Dwryt6bV5uZPoMvR+dwqrJ

7
testdata/keys/test_id_ed25519_n vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAL+rCmYdr90w9sQGTc4q2jrXC0T/HiXV7hAC/rGSbAEgAAAIj8310O/N9d
DgAAAAtzc2gtZWQyNTUxOQAAACAL+rCmYdr90w9sQGTc4q2jrXC0T/HiXV7hAC/rGSbAEg
AAAECEqFXGeY12fcYUBwBn9ltRldW9IeP6/cI084TuLUBmdwv6sKZh2v3TD2xAZNziraOt
cLRP8eJdXuEAL+sZJsASAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_n.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAv6sKZh2v3TD2xAZNziraOtcLRP8eJdXuEAL+sZJsAS

7
testdata/keys/test_id_ed25519_o vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDrlvw5/7W09kgkralSUFN6SU/INhuo1lxMGgm9sF3jiQAAAIgJq4FqCauB
agAAAAtzc2gtZWQyNTUxOQAAACDrlvw5/7W09kgkralSUFN6SU/INhuo1lxMGgm9sF3jiQ
AAAECZKFVeNm3E4LFqGUZjE2KRUE6DtqSRAoUspke+mQYGPuuW/Dn/tbT2SCStqVJQU3pJ
T8g2G6jWXEwaCb2wXeOJAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_o.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOuW/Dn/tbT2SCStqVJQU3pJT8g2G6jWXEwaCb2wXeOJ

7
testdata/keys/test_id_ed25519_p vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDBVOlRzQJsfAtS8RHRfe8/RUe53TXgeKpJui4+bQ4cXAAAAIiWs4rJlrOK
yQAAAAtzc2gtZWQyNTUxOQAAACDBVOlRzQJsfAtS8RHRfe8/RUe53TXgeKpJui4+bQ4cXA
AAAEC5vxPtZLKeN8A3jAyK5O9SLYbp3LWwbDKZLgQWwmOwA8FU6VHNAmx8C1LxEdF97z9F
R7ndNeB4qkm6Lj5tDhxcAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_p.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMFU6VHNAmx8C1LxEdF97z9FR7ndNeB4qkm6Lj5tDhxc

7
testdata/keys/test_id_ed25519_q vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACANC9xH5Wixxwpy0IVJNfKHdWEqIz5bpGDX8H21fhmSKAAAAIjzbmzD825s
wwAAAAtzc2gtZWQyNTUxOQAAACANC9xH5Wixxwpy0IVJNfKHdWEqIz5bpGDX8H21fhmSKA
AAAEDi1lOpgb7xVeKVcn6NtL+9YuxDKyW8VTqYnB/DTvfrNQ0L3EflaLHHCnLQhUk18od1
YSojPlukYNfwfbV+GZIoAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_q.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA0L3EflaLHHCnLQhUk18od1YSojPlukYNfwfbV+GZIo

7
testdata/keys/test_id_ed25519_r vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACADzkeAxKvKL5pKUlTt1SezzmpZS73AgaFA+/oOa6AFfAAAAIgXQ569F0Oe
vQAAAAtzc2gtZWQyNTUxOQAAACADzkeAxKvKL5pKUlTt1SezzmpZS73AgaFA+/oOa6AFfA
AAAECg8l+0RHTtTR4KaHGoSDQUwjf1diHrl4X3VksZ2JytwgPOR4DEq8ovmkpSVO3VJ7PO
allLvcCBoUD7+g5roAV8AAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_r.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAPOR4DEq8ovmkpSVO3VJ7POallLvcCBoUD7+g5roAV8

7
testdata/keys/test_id_ed25519_s vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCFd9rETu9NZWCGUIszhtv+HIxpq3SABTvu0ixK2eDHygAAAIjirHXj4qx1
4wAAAAtzc2gtZWQyNTUxOQAAACCFd9rETu9NZWCGUIszhtv+HIxpq3SABTvu0ixK2eDHyg
AAAECJzn3k3eM9694KCZiOBZPPFFNXqng2vV5niPVDMT3aQIV32sRO701lYIZQizOG2/4c
jGmrdIAFO+7SLErZ4MfKAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_s.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIV32sRO701lYIZQizOG2/4cjGmrdIAFO+7SLErZ4MfK

7
testdata/keys/test_id_ed25519_t vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCbKLr5W3EoAEEC/y9KFr/nAm7mRcMv246pOw6ZBiSeJwAAAIjEXWV/xF1l
fwAAAAtzc2gtZWQyNTUxOQAAACCbKLr5W3EoAEEC/y9KFr/nAm7mRcMv246pOw6ZBiSeJw
AAAED3mCjxtEhxY5/Jm0ByKs2+/8PsNdWJPjdQ6QZFIBbczZsouvlbcSgAQQL/L0oWv+cC
buZFwy/bjqk7DpkGJJ4nAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_t.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJsouvlbcSgAQQL/L0oWv+cCbuZFwy/bjqk7DpkGJJ4n

7
testdata/keys/test_id_ed25519_u vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDAQJBCBUJ55Aqy7c0eqvlM8FdrTynepQr72uaWofnVrAAAAIiWeKP9lnij
/QAAAAtzc2gtZWQyNTUxOQAAACDAQJBCBUJ55Aqy7c0eqvlM8FdrTynepQr72uaWofnVrA
AAAEAoLltBG4iUsZDBTaWDv3mie5zJn6tMIQz9l5jCB1phlcBAkEIFQnnkCrLtzR6q+Uzw
V2tPKd6lCvva5pah+dWsAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_u.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMBAkEIFQnnkCrLtzR6q+UzwV2tPKd6lCvva5pah+dWs

7
testdata/keys/test_id_ed25519_v vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDK3NCwJUuiCYMoAh3WcP0uDgSMJxdOTjNVkAZFMjbjOwAAAIj2Cvar9gr2
qwAAAAtzc2gtZWQyNTUxOQAAACDK3NCwJUuiCYMoAh3WcP0uDgSMJxdOTjNVkAZFMjbjOw
AAAEDw5HzNtISf8jObCiX1TIfMG4riUafWdmaMnbHylD9Pdsrc0LAlS6IJgygCHdZw/S4O
BIwnF05OM1WQBkUyNuM7AAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_v.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMrc0LAlS6IJgygCHdZw/S4OBIwnF05OM1WQBkUyNuM7

7
testdata/keys/test_id_ed25519_w vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACA2M9JkSMBC875ww1f5tYpFGc0sHbvTVW0ZbiOO/6mGBgAAAIi+yvZUvsr2
VAAAAAtzc2gtZWQyNTUxOQAAACA2M9JkSMBC875ww1f5tYpFGc0sHbvTVW0ZbiOO/6mGBg
AAAECTIiMBCKvnKDMSTN/cSJh0vTRlQQ5274pROcyxYXYOIzYz0mRIwELzvnDDV/m1ikUZ
zSwdu9NVbRluI47/qYYGAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

1
testdata/keys/test_id_ed25519_w.pub vendored Normal file
View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDYz0mRIwELzvnDDV/m1ikUZzSwdu9NVbRluI47/qYYG

7
testdata/keys/test_id_ed25519_x vendored Normal file
View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBsnmWDuYTUNvauU7ZBdrvOyHo5eNqkk80OcuOSMj9tcgAAAIirtwdgq7cH
YAAAAAtzc2gtZWQyNTUxOQAAACBsnmWDuYTUNvauU7ZBdrvOyHo5eNqkk80OcuOSMj9tcg
AAAED42nmxcvm2cvfubKitLujExxqkPWrOftVpFaHf/AzghGyeZYO5hNQ29q5TtkF2u87I
ejl42qSTzQ5y45IyP21yAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----

Some files were not shown because too many files have changed in this diff Show More