diff --git a/package/build_darwin.sh b/package/build_darwin.sh new file mode 100755 index 0000000..301f9c7 --- /dev/null +++ b/package/build_darwin.sh @@ -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 diff --git a/pam/cmdline_darwin.go b/pam/cmdline_darwin.go new file mode 100644 index 0000000..da2517a --- /dev/null +++ b/pam/cmdline_darwin.go @@ -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 +} diff --git a/pam/cmdline_linux.go b/pam/cmdline_linux.go new file mode 100644 index 0000000..4f15068 --- /dev/null +++ b/pam/cmdline_linux.go @@ -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 +} diff --git a/pam/pam_sshca.go b/pam/pam_sshca.go index aafe609..469d824 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" @@ -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.") @@ -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() @@ -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.