mirror of
https://github.com/fosrl/newt.git
synced 2026-07-17 11:29:52 +00:00
159 lines
5.3 KiB
Go
159 lines
5.3 KiB
Go
package nativessh
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/fosrl/newt/logger"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
type staticConnMeta struct {
|
|
user string
|
|
}
|
|
|
|
func (m staticConnMeta) User() string { return m.user }
|
|
func (m staticConnMeta) SessionID() []byte { return nil }
|
|
func (m staticConnMeta) ClientVersion() []byte { return nil }
|
|
func (m staticConnMeta) ServerVersion() []byte { return nil }
|
|
func (m staticConnMeta) RemoteAddr() net.Addr { return nil }
|
|
func (m staticConnMeta) LocalAddr() net.Addr { return nil }
|
|
|
|
// CheckAuthorizedKeys reports whether key matches any entry in the system
|
|
// user's ~/.ssh/authorized_keys file. Returns false (not an error) when the
|
|
// user or file does not exist.
|
|
func CheckAuthorizedKeys(username string, key ssh.PublicKey) bool {
|
|
u, err := user.Lookup(username)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
f, err := os.Open(filepath.Join(u.HomeDir, ".ssh", "authorized_keys"))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer f.Close()
|
|
|
|
want := ssh.FingerprintSHA256(key)
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if ssh.FingerprintSHA256(parsed) == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SystemUserExists reports whether a user account with the given name exists
|
|
// on the host OS.
|
|
func SystemUserExists(username string) bool {
|
|
_, err := user.Lookup(username)
|
|
return err == nil
|
|
}
|
|
|
|
// Authenticate authenticates a user for a browser-based native SSH session.
|
|
// It tries, in order:
|
|
// 1. Private key — parses privateKeyPEM and checks it against the user's
|
|
// ~/.ssh/authorized_keys.
|
|
// 2. Password — verifies password via the host OS PAM stack (Linux only).
|
|
//
|
|
// Returns nil on the first method that succeeds, or an error if all fail.
|
|
func Authenticate(username, password, privateKeyPEM string) error {
|
|
return AuthenticateWithCertificate(nil, username, password, privateKeyPEM, "")
|
|
}
|
|
|
|
// AuthenticateWithCertificate authenticates a user for a browser-based native
|
|
// SSH session using the same method ordering as the native SSH server:
|
|
// 1. Private key in host ~/.ssh/authorized_keys.
|
|
// 2. SSH certificate signed by the configured CA (when provided).
|
|
// 3. Password via host PAM stack.
|
|
func AuthenticateWithCertificate(store *CredentialStore, username, password, privateKeyPEM, certificate string) error {
|
|
logger.Debug("nativessh: authenticating user %q (hasPassword=%v, hasPrivateKey=%v)", username, password != "", privateKeyPEM != "")
|
|
if !SystemUserExists(username) {
|
|
logger.Debug("nativessh: user %q not found on system", username)
|
|
return fmt.Errorf("user %q does not exist", username)
|
|
}
|
|
|
|
var signer ssh.Signer
|
|
if privateKeyPEM != "" {
|
|
parsedSigner, err := ssh.ParsePrivateKey([]byte(privateKeyPEM))
|
|
if err != nil {
|
|
logger.Debug("nativessh: failed to parse private key for %q: %v", username, err)
|
|
} else if CheckAuthorizedKeys(username, parsedSigner.PublicKey()) {
|
|
logger.Debug("nativessh: private key auth succeeded for %q", username)
|
|
return nil
|
|
} else {
|
|
signer = parsedSigner
|
|
logger.Debug("nativessh: private key not in authorized_keys for %q", username)
|
|
}
|
|
}
|
|
|
|
if store != nil && certificate != "" {
|
|
if signer == nil {
|
|
logger.Debug("nativessh: certificate provided for %q but no matching private key was provided", username)
|
|
} else {
|
|
pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certificate))
|
|
if err != nil {
|
|
logger.Debug("nativessh: failed to parse certificate for %q: %v", username, err)
|
|
} else {
|
|
cert, ok := pub.(*ssh.Certificate)
|
|
if !ok {
|
|
logger.Debug("nativessh: provided cert data for %q is not an SSH certificate", username)
|
|
} else if ssh.FingerprintSHA256(cert.Key) != ssh.FingerprintSHA256(signer.PublicKey()) {
|
|
logger.Debug("nativessh: certificate key mismatch for %q", username)
|
|
} else {
|
|
caKey, userPrincipals := store.get(username)
|
|
if caKey == nil {
|
|
logger.Debug("nativessh: CA key is not set for certificate auth user %q", username)
|
|
} else if len(userPrincipals) == 0 {
|
|
logger.Debug("nativessh: no allowed principals found for certificate auth user %q", username)
|
|
} else {
|
|
checker := &ssh.CertChecker{
|
|
IsUserAuthority: func(auth ssh.PublicKey) bool {
|
|
return ssh.FingerprintSHA256(auth) == ssh.FingerprintSHA256(caKey)
|
|
},
|
|
}
|
|
|
|
var lastErr error
|
|
for principal := range userPrincipals {
|
|
_, authErr := checker.Authenticate(staticConnMeta{user: principal}, cert)
|
|
if authErr == nil {
|
|
logger.Debug("nativessh: certificate auth succeeded for %q (principal=%q)", username, principal)
|
|
return nil
|
|
}
|
|
lastErr = authErr
|
|
}
|
|
if lastErr != nil {
|
|
logger.Debug("nativessh: certificate auth failed for %q: %v", username, lastErr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if password != "" {
|
|
if err := VerifySystemPassword(username, password); err != nil {
|
|
logger.Debug("nativessh: password auth failed for %q: %v", username, err)
|
|
} else {
|
|
logger.Debug("nativessh: password auth succeeded for %q", username)
|
|
return nil
|
|
}
|
|
} else {
|
|
logger.Debug("nativessh: no password provided for %q", username)
|
|
}
|
|
return fmt.Errorf("authentication failed for user %q", username)
|
|
}
|