diff --git a/go.mod b/go.mod index f99dc7d..bb74798 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module shell2 go 1.25.4 require ( + github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 github.com/lib/pq v1.10.9 - github.com/msteinert/pam/v2 v2.1.0 + github.com/openwall/yescrypt-go v1.0.0 github.com/xtaci/kcp-go/v5 v5.6.61 github.com/xtaci/smux v1.5.50 golang.org/x/crypto v0.45.0 diff --git a/go.sum b/go.sum index 5f05854..2d5ac6b 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,12 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -36,15 +38,15 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/msteinert/pam/v2 v2.1.0 h1:er5F9TKV5nGFuTt12ubtqPHEUdeBwReP7vd3wovidGY= -github.com/msteinert/pam/v2 v2.1.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc= +github.com/openwall/yescrypt-go v1.0.0 h1:jsGk48zkFvtUjGVOhYPGh+CS595JmTRcKnpggK2AON4= +github.com/openwall/yescrypt-go v1.0.0/go.mod h1:e6CWtFizUEOUttaOjeVMiv1lJaJie3mfOtLJ9CCD6sA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/xtaci/kcp-go/v5 v5.6.61 h1:ajm12pGuWO+GWQNusPyPESC7Rq0yTC2rEXVYkM8ExOg= diff --git a/install.sh b/install.sh index 6fae9cf..dabfac9 100644 --- a/install.sh +++ b/install.sh @@ -61,24 +61,23 @@ detect_pkg_manager() { set_package_deps() { case "$PKG_MANAGER" in apt) - # libpam0g-dev: PAM headers for the optional PAM auth build (cgo). - PKG_DEPS=(curl wget git rsync build-essential libpam0g-dev postgresql ca-certificates unzip openssh-client openssl python3 tar gzip) + PKG_DEPS=(curl wget git rsync build-essential postgresql ca-certificates unzip openssh-client openssl python3 tar gzip) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) ;; dnf|yum) - PKG_DEPS=(curl wget git rsync gcc make pam-devel postgresql-server ca-certificates unzip openssh-clients openssl python3 tar gzip) + PKG_DEPS=(curl wget git rsync gcc make postgresql-server ca-certificates unzip openssh-clients openssl python3 tar gzip) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) ;; zypper) - PKG_DEPS=(curl wget git rsync gcc make pam-devel postgresql-server ca-certificates unzip openssh openssl python3 tar gzip) + PKG_DEPS=(curl wget git rsync gcc make postgresql-server ca-certificates unzip openssh openssl python3 tar gzip) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) ;; pacman) - PKG_DEPS=(curl wget git rsync base-devel pam postgresql ca-certificates unzip openssh openssl python tar gzip) + PKG_DEPS=(curl wget git rsync base-devel postgresql ca-certificates unzip openssh openssl python tar gzip) PKG_OPTIONAL_DEPS=(iptables-nft nftables) ;; apk) - PKG_DEPS=(curl wget git rsync build-base linux-pam-dev postgresql ca-certificates unzip openssh-client openssl python3 tar gzip) + PKG_DEPS=(curl wget git rsync build-base postgresql ca-certificates unzip openssh-client openssl python3 tar gzip) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) ;; esac diff --git a/pam_auth.go b/pam_auth.go new file mode 100644 index 0000000..dc57468 --- /dev/null +++ b/pam_auth.go @@ -0,0 +1,95 @@ +package main + +import ( + "crypto/subtle" + "errors" + "fmt" + "os" + "strings" + + "github.com/GehirnInc/crypt" + _ "github.com/GehirnInc/crypt/apr1_crypt" + _ "github.com/GehirnInc/crypt/md5_crypt" + _ "github.com/GehirnInc/crypt/sha256_crypt" + _ "github.com/GehirnInc/crypt/sha512_crypt" + "github.com/openwall/yescrypt-go" + "golang.org/x/crypto/bcrypt" +) + +// pamAuthAvailable reports that system-password ("PAM") auth is compiled in. +// The implementation is pure Go (no cgo, no libpam), so it is always available; +// on non-Linux builds /etc/shadow simply does not exist and auth fails closed. +const pamAuthAvailable = true + +const shadowFile = "/etc/shadow" + +var errNoSystemPassword = errors.New("account has no usable password") + +// authenticatePAM verifies password against the Linux system account matching +// username. It reads the account's hash from /etc/shadow (the panel runs as +// root) and recomputes it with the same algorithm — this is the "just the auth" +// behaviour: the supplied password is checked exactly as the system would, +// with no account/session management and nothing to do with the SSH daemon. +// +// It is called "PAM" for continuity with the user-facing flag, but it does not +// link libpam; it verifies the crypt(3) hash directly. Supported hash formats: +// yescrypt ($y$), sha512-crypt ($6$), sha256-crypt ($5$), md5-crypt ($1$), +// apr1 ($apr1$) and bcrypt ($2a$/$2b$/$2y$). Returns nil on success. +func authenticatePAM(username, password string) error { + if username == "" { + return errors.New("shadow: empty username") + } + hash, err := lookupShadowHash(username) + if err != nil { + return err + } + return verifyCryptHash(hash, password) +} + +// lookupShadowHash returns the password hash field for username from /etc/shadow. +func lookupShadowHash(username string) (string, error) { + data, err := os.ReadFile(shadowFile) + if err != nil { + return "", fmt.Errorf("read %s: %w", shadowFile, err) + } + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimRight(line, "\r") + if line == "" { + continue + } + fields := strings.Split(line, ":") + if len(fields) < 2 || fields[0] != username { + continue + } + hash := fields[1] + // Empty, or locked/disabled accounts (! or * in the hash field) have no + // password that any input can match — reject rather than risk a match. + if hash == "" || strings.HasPrefix(hash, "!") || strings.HasPrefix(hash, "*") { + return "", errNoSystemPassword + } + return hash, nil + } + return "", fmt.Errorf("shadow: user %q not found", username) +} + +// verifyCryptHash checks password against a crypt(3)-style hash string, +// dispatching on the hash prefix. Returns nil only on an exact match. +func verifyCryptHash(hash, password string) error { + switch { + case strings.HasPrefix(hash, "$y$"): + computed, err := yescrypt.Hash([]byte(password), []byte(hash)) + if err != nil { + return fmt.Errorf("yescrypt: %w", err) + } + if subtle.ConstantTimeCompare(computed, []byte(hash)) == 1 { + return nil + } + return errors.New("password mismatch") + case strings.HasPrefix(hash, "$2a$"), strings.HasPrefix(hash, "$2b$"), strings.HasPrefix(hash, "$2y$"): + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + case crypt.IsHashSupported(hash): + return crypt.NewFromHash(hash).Verify(hash, []byte(password)) + default: + return fmt.Errorf("shadow: unsupported hash format") + } +} diff --git a/pam_auth_linux.go b/pam_auth_linux.go deleted file mode 100644 index 69258c7..0000000 --- a/pam_auth_linux.go +++ /dev/null @@ -1,63 +0,0 @@ -//go:build linux && cgo - -package main - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/msteinert/pam/v2" -) - -// pamAuthAvailable reports that PAM authentication is compiled into this build. -const pamAuthAvailable = true - -// pamServiceName returns the PAM service to use for the auth-only check. This -// is the name of a file under /etc/pam.d. Override with SSHPANEL_PAM_SERVICE; -// it defaults to "login", which exists on every mainstream distro and runs the -// standard unix auth stack (common-auth / system-auth). -func pamServiceName() string { - if s := strings.TrimSpace(os.Getenv("SSHPANEL_PAM_SERVICE")); s != "" { - return s - } - return "login" -} - -// authenticatePAM verifies password against the Linux PAM stack for username. -// -// It runs ONLY the auth phase (pam_authenticate) — no account management -// (pam_acct_mgmt), no session, no credential setup, and nothing to do with the -// system SSH daemon. This is the "just the auth" behaviour: the supplied -// password is checked against the system account exactly as PAM's auth modules -// would, and nothing else. Returns nil on success, an error on failure. -// -// The panel runs as root, so pam_unix can read /etc/shadow to verify the hash -// (including yescrypt/sha512crypt) for any local account. -func authenticatePAM(username, password string) error { - if username == "" { - return errors.New("pam: empty username") - } - t, err := pam.StartFunc(pamServiceName(), username, func(s pam.Style, msg string) (string, error) { - switch s { - case pam.PromptEchoOff, pam.PromptEchoOn: - return password, nil - case pam.ErrorMsg, pam.TextInfo: - // Informational messages from modules; nothing to return. - return "", nil - default: - return "", fmt.Errorf("pam: unsupported conversation style %v", s) - } - }) - if err != nil { - return fmt.Errorf("pam start: %w", err) - } - defer func() { _ = t.End() }() - - // Silent keeps modules from writing to stdout/syslog noise; auth phase only. - if err := t.Authenticate(pam.Silent); err != nil { - return fmt.Errorf("pam auth: %w", err) - } - return nil -} diff --git a/pam_auth_stub.go b/pam_auth_stub.go deleted file mode 100644 index 6da0566..0000000 --- a/pam_auth_stub.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !linux || !cgo - -package main - -import "errors" - -// pamAuthAvailable is false when the binary is built without cgo/PAM support -// (for example the Windows development build, or any CGO_ENABLED=0 build). -// In that case a user flagged for PAM auth can never authenticate. -const pamAuthAvailable = false - -func pamServiceName() string { return "" } - -func authenticatePAM(username, password string) error { - return errors.New("pam authentication is not supported in this build") -} diff --git a/update.sh b/update.sh index 99a8ce2..22132fc 100644 --- a/update.sh +++ b/update.sh @@ -79,29 +79,21 @@ detect_pkg_manager() { } set_update_deps() { - # PAM_DEV_PKG provides , needed to build the optional - # PAM auth support (cgo). Tracked separately because it is a dev header, not - # a command, so the command-based dependency check cannot detect it. case "$PKG_MANAGER" in apt) - PAM_DEV_PKG="libpam0g-dev" - UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip "$PAM_DEV_PKG") + UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip) ;; dnf|yum) - PAM_DEV_PKG="pam-devel" - UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip "$PAM_DEV_PKG") + UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip) ;; zypper) - PAM_DEV_PKG="pam-devel" - UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip "$PAM_DEV_PKG") + UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip) ;; pacman) - PAM_DEV_PKG="pam" - UPDATE_DEPS=(git rsync wget ca-certificates python gcc make tar gzip "$PAM_DEV_PKG") + UPDATE_DEPS=(git rsync wget ca-certificates python gcc make tar gzip) ;; apk) - PAM_DEV_PKG="linux-pam-dev" - UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip "$PAM_DEV_PKG") + UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip) ;; esac } @@ -138,11 +130,6 @@ ensure_update_dependencies() { if ! command -v python3 >/dev/null 2>&1; then missing=true fi - # The PAM auth build needs ; a dev header install, not a - # command, so check for the header file directly. - if [ ! -e /usr/include/security/pam_appl.h ]; then - missing=true - fi if $missing; then warn "One or more updater dependencies are missing. Installing them with $PKG_MANAGER..." pkg_update