From 6fdf615516be81dd7823210e19851f5c83628123 Mon Sep 17 00:00:00 2001 From: Jay Soffian Date: Thu, 31 Aug 2023 00:56:48 -0400 Subject: [PATCH] Port to Darwin Mostly everything worked. I just needed a replacement for reading `/proc` to get the command line since macOS doesn't provide `/proc`. I cobbled together this solution from code I found in https://github.com/elastic/go-sysinfo I've verified it works as expected under macOS 13 (Ventura). It probably panics under macOS 10.15 (Catalina) due to this issue: https://github.com/elastic/go-sysinfo/issues/173 Note also that using: "auth [success=done default=die] pam_sshca.so" does not work on Darwin to configure the module. The closest equivalent is likely to be: "auth requisite] /path/to/pam_sshca.so" I have not tested on Linux and this commit should be considered a proof-of-concept. --- build-darwin.sh | 17 +++++++++++ pam/cmdline_darwin.go | 67 +++++++++++++++++++++++++++++++++++++++++++ pam/cmdline_linux.go | 29 +++++++++++++++++++ pam/pam_sshca.go | 17 ++--------- 4 files changed, 116 insertions(+), 14 deletions(-) create mode 100755 build-darwin.sh create mode 100644 pam/cmdline_darwin.go create mode 100644 pam/cmdline_linux.go diff --git a/build-darwin.sh b/build-darwin.sh new file mode 100755 index 0000000..dbde692 --- /dev/null +++ b/build-darwin.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -euo pipefail + +readonly SOURCE_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +readonly BUILD_DIR="$SOURCE_DIR/_build" +readonly PAM_SSHCA_DIR="$SOURCE_DIR" + +declare -ra build_cmd=( + go build -v + -o "$BUILD_DIR/pam_sshca.so" + -buildmode=c-shared + "$PAM_SSHCA_DIR/cmd/pam_sshca" +) + +set -x +mkdir -p "$BUILD_DIR" +GOARCH=arm64 GOOS=darwin "${build_cmd[@]}" diff --git a/pam/cmdline_darwin.go b/pam/cmdline_darwin.go new file mode 100644 index 0000000..a358c86 --- /dev/null +++ b/pam/cmdline_darwin.go @@ -0,0 +1,67 @@ +// Copyright 2022 Yahoo Inc. +// Licensed under the terms of the Apache License 2.0. Please see LICENSE file in project root for terms. + +package pam + +import ( + "encoding/binary" + "errors" + "github.com/theparanoids/pam-ysshca/msg" + "golang.org/x/sys/unix" + "os" + "strings" + "syscall" +) + +var unknownCommand = []byte("unknown command") + +func getCmdLine() []byte { + data, err := unix.SysctlRaw("kern.procargs2", os.Getpid()) + if err != nil { + if errors.Is(err, syscall.EINVAL) { + // sysctl returns "invalid argument" for both "no such process" + // and "operation not permitted" errors. + msg.Printlf(msg.WARN, "No such process or operation not permitted: %w", err) + } + return unknownCommand + } + return parseKernProcargs2(data) +} + +func parseKernProcargs2(data []byte) []byte { + // argc + if len(data) < 4 { + msg.Printlf(msg.WARN, "Invalid kern.procargs2 data") + return unknownCommand + } + argc := binary.LittleEndian.Uint32(data) + data = data[4:] + + // exe + lines := strings.Split(string(data), "\x00") + exe := lines[0] + lines = lines[1:] + + // Skip nulls that may be appended after the exe. + for len(lines) > 0 { + if lines[0] != "" { + break + } + lines = lines[1:] + } + + // argv + if c := min(argc, uint32(len(lines))); c > 0 { + exe += " " + exe += strings.Join(lines[:c], " ") + } + + return []byte(exe) +} + +func min(a, b uint32) uint32 { + if a < b { + return a + } + return b +} diff --git a/pam/cmdline_linux.go b/pam/cmdline_linux.go new file mode 100644 index 0000000..c0f8d54 --- /dev/null +++ b/pam/cmdline_linux.go @@ -0,0 +1,29 @@ +// Copyright 2022 Yahoo Inc. +// Licensed under the terms of the Apache License 2.0. Please see LICENSE file in project root for terms. + +package pam + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" +) + +func getCmdLine() []byte { + cmd, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", os.Getpid())) + if err != nil { + cmd = []byte("unknown command") + msg.Printlf(msg.WARN, "Failed to read /proc/%d/cmdline: %v", os.Getpid(), err) + } else if len(cmd) == 0 { + cmd = []byte("empty command") + msg.Printlf(msg.WARN, "/proc/%d/cmdline is empty", os.Getpid()) + } + + // Remove '\0' at the end. + cmd = cmd[:len(cmd)-1] + // Replace '\0' with ' '. + cmd = bytes.Replace(cmd, []byte{0}, []byte{' '}, -1) + + return cmd +} diff --git a/pam/pam_sshca.go b/pam/pam_sshca.go index aafe609..3170091 100644 --- a/pam/pam_sshca.go +++ b/pam/pam_sshca.go @@ -18,12 +18,12 @@ package pam // uid_t GetCurrentUserUID(pam_handle_t *pamh); // const char *GetCurrentUserName(pam_handle_t *pamh); // const char *GetCurrentUserHome(pam_handle_t *pamh); +// import "C" import ( "bytes" "fmt" - "io/ioutil" "log/syslog" "net" "os" @@ -74,19 +74,7 @@ func newAuthenticator(user, home string) *authenticator { } func (a *authenticator) authenticate() C.int { - cmd, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", os.Getpid())) - if err != nil { - cmd = []byte("unknown command") - msg.Printlf(msg.WARN, "Failed to read /proc/%d/cmdline: %v", os.Getpid(), err) - } else if len(cmd) == 0 { - cmd = []byte("empty command") - msg.Printlf(msg.WARN, "/proc/%d/cmdline is empty", os.Getpid()) - } - - // Remove '\0' at the end. - cmd = cmd[:len(cmd)-1] - // Replace '\0' with ' '. - cmd = bytes.Replace(cmd, []byte{0}, []byte{' '}, -1) + cmd := getCmdLine() // Initialize ssh-agent. sshAuthSock, err := sshagent.CheckSSHAuthSock() @@ -160,6 +148,7 @@ func (a *authenticator) sysLogWarning(m string) { // Authenticate is the entry of Go language part. // It is invoked by pam_sm_authenticate in C language part. +// //export Authenticate func Authenticate(pamh *C.pam_handle_t) C.int { // Initialize login variables.