Files
newt/nativessh/auth.go
Owen 17c4fe9b2b Use logger not printf
Former-commit-id: 3ef2550687
2026-06-04 16:57:51 -07:00

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)
}