Remove github.com/msteinert/pam/v2

This commit is contained in:
Owen
2026-05-29 15:57:39 -07:00
parent 57575490e1
commit b33a12a528
3 changed files with 257 additions and 21 deletions

1
go.mod
View File

@@ -9,7 +9,6 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/moby/moby/api v1.54.2
github.com/moby/moby/client v0.4.1
github.com/msteinert/pam/v2 v2.1.0
github.com/prometheus/client_golang v1.23.2
github.com/vishvananda/netlink v1.3.1
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0

2
go.sum
View File

@@ -57,8 +57,6 @@ github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
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/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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=

View File

@@ -3,32 +3,271 @@
package nativessh
import (
"bufio"
"bytes"
"crypto/sha512"
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/msteinert/pam/v2"
"golang.org/x/crypto/bcrypt"
)
// VerifySystemPassword authenticates username/password via PAM using the
// "sshd" service stack. It returns nil on success and an error on failure.
// The caller must not reveal the error detail to the client.
// VerifySystemPassword authenticates username/password by reading /etc/shadow
// and verifying the stored hash using pure-Go cryptography (no CGo required).
// Supported hash schemes: bcrypt ($2a$/$2b$/$2y$) and SHA-512 crypt ($6$).
func VerifySystemPassword(username, password string) error {
tx, err := pam.StartFunc("sshd", username, func(s pam.Style, msg string) (string, error) {
switch s {
case pam.PromptEchoOff, pam.PromptEchoOn:
return password, nil
default:
return "", nil
}
})
hash, err := readShadowHash(username)
if err != nil {
return fmt.Errorf("PAM start: %w", err)
return fmt.Errorf("shadow: %w", err)
}
return cryptVerify(password, hash)
}
// readShadowHash reads /etc/shadow and returns the password hash for username.
func readShadowHash(username string) (string, error) {
f, err := os.Open("/etc/shadow")
if err != nil {
return "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fields := strings.SplitN(scanner.Text(), ":", 3)
if len(fields) < 2 || fields[0] != username {
continue
}
h := fields[1]
if h == "" || h == "*" || strings.HasPrefix(h, "!") || h == "x" {
return "", errors.New("account locked or has no password")
}
return h, nil
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", errors.New("user not found in shadow database")
}
// cryptVerify verifies password against a crypt(3) hash string.
func cryptVerify(password, hash string) error {
switch {
case strings.HasPrefix(hash, "$2a$"), strings.HasPrefix(hash, "$2b$"), strings.HasPrefix(hash, "$2y$"):
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
case strings.HasPrefix(hash, "$6$"):
computed, err := sha512CryptHash([]byte(password), hash)
if err != nil {
return err
}
if computed != hash {
return errors.New("authentication failed")
}
return nil
default:
return fmt.Errorf("unsupported password hash scheme")
}
}
// --- SHA-512 crypt ($6$) ---
// Specification: https://www.akkadia.org/docs/sha-crypt.txt
const (
sha512CryptMagic = "$6$"
sha512CryptRoundsDefault = 5000
sha512CryptRoundsMin = 1000
sha512CryptRoundsMax = 999999999
sha512CryptSaltLenMax = 16
sha512CryptAlphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
)
var sha512CryptRoundsPrefix = []byte("rounds=")
// sha512CryptHash computes a SHA-512 crypt hash for key. saltStr may be a
// full stored hash string (for verification) or just the salt parameters.
func sha512CryptHash(key []byte, saltStr string) (string, error) {
salt := []byte(saltStr)
if !bytes.HasPrefix(salt, []byte(sha512CryptMagic)) {
return "", errors.New("sha512crypt: invalid prefix")
}
salt = salt[len(sha512CryptMagic):]
rounds := sha512CryptRoundsDefault
isRoundsDef := false
if bytes.HasPrefix(salt, sha512CryptRoundsPrefix) {
salt = salt[len(sha512CryptRoundsPrefix):]
i := bytes.IndexByte(salt, '$')
if i < 0 {
return "", errors.New("sha512crypt: malformed rounds field")
}
r, err := strconv.Atoi(string(salt[:i]))
if err != nil {
return "", fmt.Errorf("sha512crypt: invalid rounds: %w", err)
}
salt = salt[i+1:]
isRoundsDef = true
rounds = r
if rounds < sha512CryptRoundsMin {
rounds = sha512CryptRoundsMin
} else if rounds > sha512CryptRoundsMax {
rounds = sha512CryptRoundsMax
}
}
if err := tx.Authenticate(0); err != nil {
return fmt.Errorf("PAM authenticate: %w", err)
// When saltStr is a full hash, strip the stored hash after the last '$'.
if i := bytes.IndexByte(salt, '$'); i >= 0 {
salt = salt[:i]
}
if err := tx.AcctMgmt(0); err != nil {
return fmt.Errorf("PAM acct_mgmt: %w", err)
if len(salt) > sha512CryptSaltLenMax {
salt = salt[:sha512CryptSaltLenMax]
}
return nil
// Alternate = SHA512(key + salt + key)
altH := sha512.New()
altH.Write(key)
altH.Write(salt)
altH.Write(key)
altSum := altH.Sum(nil)
// Digest A = SHA512(key + salt + altSum-cycling-to-len(key) + bit-pattern)
aH := sha512.New()
aH.Write(key)
aH.Write(salt)
for i := len(key); i > 0; i -= 64 {
if i > 64 {
aH.Write(altSum)
} else {
aH.Write(altSum[:i])
}
}
for i := len(key); i > 0; i >>= 1 {
if i&1 != 0 {
aH.Write(altSum)
} else {
aH.Write(key)
}
}
aSum := aH.Sum(nil)
// P-sequence: SHA512(key×len(key)) cycled to len(key) bytes
pH := sha512.New()
for i := 0; i < len(key); i++ {
pH.Write(key)
}
pSeq := sha512CryptCycle(pH.Sum(nil), len(key))
// S-sequence: SHA512(salt×(16+aSum[0])) cycled to len(salt) bytes
sH := sha512.New()
for i := 0; i < 16+int(aSum[0]); i++ {
sH.Write(salt)
}
sSeq := sha512CryptCycle(sH.Sum(nil), len(salt))
// Iterative hashing rounds
cSum := aSum
for i := 0; i < rounds; i++ {
c := sha512.New()
if i&1 != 0 {
c.Write(pSeq)
} else {
c.Write(cSum)
}
if i%3 != 0 {
c.Write(sSeq)
}
if i%7 != 0 {
c.Write(pSeq)
}
if i&1 != 0 {
c.Write(cSum)
} else {
c.Write(pSeq)
}
cSum = c.Sum(nil)
}
// Build the output string
out := []byte(sha512CryptMagic)
if isRoundsDef {
out = append(out, fmt.Sprintf("rounds=%d$", rounds)...)
}
out = append(out, salt...)
out = append(out, '$')
out = append(out, sha512CryptEncode(cSum)...)
return string(out), nil
}
// sha512CryptCycle returns exactly n bytes by cycling the 64-byte src slice.
func sha512CryptCycle(src []byte, n int) []byte {
dst := make([]byte, 0, n)
for i := n; i > 64; i -= 64 {
dst = append(dst, src...)
}
if rem := n % 64; rem == 0 && n > 0 {
dst = append(dst, src...)
} else if rem > 0 {
dst = append(dst, src[:rem]...)
}
return dst
}
// sha512CryptEncode applies the sha512crypt byte permutation and encodes the
// result with the crypt(3) base-64 alphabet (86 output characters for 64 input bytes).
func sha512CryptEncode(sum []byte) []byte {
perm := []byte{
sum[42], sum[21], sum[0],
sum[1], sum[43], sum[22],
sum[23], sum[2], sum[44],
sum[45], sum[24], sum[3],
sum[4], sum[46], sum[25],
sum[26], sum[5], sum[47],
sum[48], sum[27], sum[6],
sum[7], sum[49], sum[28],
sum[29], sum[8], sum[50],
sum[51], sum[30], sum[9],
sum[10], sum[52], sum[31],
sum[32], sum[11], sum[53],
sum[54], sum[33], sum[12],
sum[13], sum[55], sum[34],
sum[35], sum[14], sum[56],
sum[57], sum[36], sum[15],
sum[16], sum[58], sum[37],
sum[38], sum[17], sum[59],
sum[60], sum[39], sum[18],
sum[19], sum[61], sum[40],
sum[41], sum[20], sum[62],
sum[63],
}
src := perm
out := make([]byte, 0, 86)
for len(src) > 0 {
switch len(src) {
default:
out = append(out,
sha512CryptAlphabet[src[0]&0x3f],
sha512CryptAlphabet[((src[0]>>6)|(src[1]<<2))&0x3f],
sha512CryptAlphabet[((src[1]>>4)|(src[2]<<4))&0x3f],
sha512CryptAlphabet[(src[2]>>2)&0x3f],
)
src = src[3:]
case 2:
out = append(out,
sha512CryptAlphabet[src[0]&0x3f],
sha512CryptAlphabet[((src[0]>>6)|(src[1]<<2))&0x3f],
sha512CryptAlphabet[(src[1]>>4)&0x3f],
)
src = src[2:]
case 1:
out = append(out,
sha512CryptAlphabet[src[0]&0x3f],
sha512CryptAlphabet[(src[0]>>6)&0x3f],
)
src = src[1:]
}
}
return out
}