Skip to content

Commit

Permalink
Port to Darwin
Browse files Browse the repository at this point in the history
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:

elastic/go-sysinfo#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.
  • Loading branch information
Jay Soffian committed Aug 31, 2023
1 parent 79d000d commit 6fdf615
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 14 deletions.
17 changes: 17 additions & 0 deletions build-darwin.sh
Original file line number Diff line number Diff line change
@@ -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[@]}"
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 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
}
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 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
}
17 changes: 3 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 @@ -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()
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 6fdf615

Please sign in to comment.