Skip to content

Commit

Permalink
Port to Darwin
Browse files Browse the repository at this point in the history
Use raw syscalls to retrieve the command line under Darwin, since macOS
does not provide a `/proc` filesystem.

The code to do this is from https://github.com/elastic/go-sysinfo which
can be sanity checked against:

https://github.com/apple-oss-distributions/adv_cmds/blob/adv_cmds-205/ps/print.c#L115

I've verified with these changes that `pam_sshca.so` works as expected
under macOS 13 (Ventura) on an arm64 host.

Issues:

1. The Linux `pam.d/sudo` configuration line:

       "auth   [success=done default=die]   pam_sshca.so"

   Does not work on Darwin. Instead use one of the following:

       "auth   requisite   /path/to/pam_sshca.so"

   Or:

       "auth   required   /path/to/pam_sshca.so"

    Neither is identical to "[success=done default=die]" whose
    semantics are impossible under Darwin. See the `pam.conf` man pages
    on Linux and macOS for details.

2. The "log/syslog" module does not work under macOS >= 12 (Monterey).
   Log messages are silently dropped:

   golang/go#59229

3. The `kern.procargs2` syscall returns incorrect data under macOS
   10.15 (Catalina) due to a bug in that OS version. The code won't
   panic under that OS version but it won't return a command line:

   - elastic/go-sysinfo#172
   - elastic/go-sysinfo#173
  • Loading branch information
Jay Soffian committed Aug 31, 2023
1 parent 79d000d commit 3109d20
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 14 deletions.
64 changes: 64 additions & 0 deletions package/build_darwin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/bash
# Copyright 2023 Yahoo Inc.
# Licensed under the terms of the Apache License 2.0. Please see LICENSE file in project root for terms.
#
# Run this script to build pam_sshca.so for darwin (macOS) to
# _build/darwin/{arch}/pam_sshca.so
#

set -euo pipefail

SCRIPT_NAME=$(basename "$0")
SOURCE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")"/.. &>/dev/null && pwd)
BUILD_DIR="$SOURCE_DIR/_build"

usage() {
cat >&2 <<__USAGE__
$SCRIPT_NAME: compile pam_sshca.so for darwin (macOS)."
Prerequisites:
- Go (e.g. brew install go)
Usage: $SCRIPT_NAME [--os-arch {arm64 | amd64 | all}]"
--os-arch Architecture name. Default: all"
__USAGE__
exit 1
}

build() {
local arch=$1
local output_dir="$BUILD_DIR/darwin/$arch"
mkdir -p "$output_dir"
local -ra build_args=(
-v
-o "$output_dir/pam_sshca.so"
-buildmode=c-shared
"$SOURCE_DIR/cmd/pam_sshca"
)
echo "GOARCH=$arch GOOS=darwin CGO_ENABLED=1 go build ${build_args[*]}"
GOARCH="$arch" GOOS=darwin CGO_ENABLED=1 go build "${build_args[@]}"
}

ARCHS=()
while [[ $# -gt 0 ]]; do
case $1 in
--os-arch)
if [[ $2 == all ]]; then
ARCHS+=(amd64 arm64)
else
ARCHS+=("$2")
fi
shift 2
;;
*)
usage
;;
esac
done

[[ "${#ARCHS[@]}" == 0 ]] && ARCHS=(amd64 arm64)

for arch in "${ARCHS[@]}"; do
build "$arch"
done
67 changes: 67 additions & 0 deletions pam/cmdline_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2023 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
}
29 changes: 29 additions & 0 deletions pam/cmdline_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2023 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
}
19 changes: 5 additions & 14 deletions pam/pam_sshca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -59,6 +59,8 @@ func newAuthenticator(user, home string) *authenticator {
config := parser.ParseConfigFile(configPath)

// Initialize system logger.
// FIXME(darwin): sysLogger output is lost on macOS due to
// https://github.com/golang/go/issues/59229
sysLogger, err := syslog.New(syslog.LOG_AUTHPRIV, "PAM_SSHCA")
if err != nil {
msg.Printlf(msg.WARN, "Failed to access syslogd, please fix your system logs.")
Expand All @@ -74,19 +76,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()
Expand Down Expand Up @@ -160,6 +150,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.
Expand Down

0 comments on commit 3109d20

Please sign in to comment.