Merge branch 'pam-moduler'
Some checks failed
Test / test (1.20.x, ubuntu-latest) (push) Has been cancelled
Test / test (1.21.x, ubuntu-latest) (push) Has been cancelled
Lint / lint (push) Has been cancelled

This commit is contained in:
Lars Scheibling
2024-01-12 09:14:01 +00:00
36 changed files with 6615 additions and 331 deletions

111
.clang-format Normal file
View File

@@ -0,0 +1,111 @@
# clang-format configuration file. For more information, see:
#
# https://clang.llvm.org/docs/ClangFormat.html
# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
#
---
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Custom
BreakBeforeInheritanceComma: false
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeComma
BreakStringLiterals: false
ColumnLimit: 120
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 8
ContinuationIndentWidth: 8
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^"(allez)/'
Priority: 2
SortPriority: 2
CaseSensitive: true
- Regex: '.*'
Priority: 1
SortPriority: 0
IndentCaseLabels: false
IndentGotoLabels: false
IndentPPDirectives: None
IndentWidth: 8
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 8
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
# Taken from git's rules
PenaltyBreakAssignment: 10
PenaltyBreakBeforeFirstCallParameter: 30
PenaltyBreakComment: 10
PenaltyBreakFirstLessLess: 0
PenaltyBreakString: 10
PenaltyExcessCharacter: 2
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
ReflowComments: false
SortIncludes: true
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatementsExceptForEachMacros
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
TabWidth: 8
UseTab: Always
...

3
.codecov.yml Normal file
View File

@@ -0,0 +1,3 @@
ignore:
# Ignore pam-moduler generated files
- "**/pam_module.go"

22
.github/workflows/lint.yaml vendored Normal file
View File

@@ -0,0 +1,22 @@
on: [push, pull_request]
name: Lint
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
cache: false
- name: Install PAM
run: sudo apt install -y libpam-dev
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.54

View File

@@ -4,19 +4,48 @@ jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.19.x, 1.20.x] go-version: [1.20.x, 1.21.x]
os: [ubuntu-latest] os: [ubuntu-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Install PAM - name: Install PAM
run: sudo apt install -y libpam-dev run: |
sudo apt update -y
sudo apt install -y libpam-dev
- name: Install Debug symbols
run: |
sudo apt install -y ubuntu-dev-tools
(cd /tmp && pull-lp-ddebs libpam0g $(lsb_release -c -s))
(cd /tmp && pull-lp-ddebs libpam-modules $(lsb_release -c -s))
sudo dpkg -i /tmp/libpam*-dbgsym_*.ddeb
- name: Add a test user - name: Add a test user
run: sudo useradd -d /tmp/test -p '$1$Qd8H95T5$RYSZQeoFbEB.gS19zS99A0' -s /bin/false test run: sudo useradd -d /tmp/test -p '$1$Qd8H95T5$RYSZQeoFbEB.gS19zS99A0' -s /bin/false test
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Test - name: Test
run: sudo go test -v ./... run: sudo go test -v -cover -coverprofile=coverage.out -coverpkg=./... ./...
- name: Test with Address Sanitizer
env:
GO_PAM_TEST_WITH_ASAN: true
CGO_CFLAGS: "-O0 -g3 -fno-omit-frame-pointer"
run: |
# Do not run sudo-requiring go tests because as PAM has some leaks in 22.04
go test -v -asan -cover -coverprofile=coverage-asan-tx.out -coverpkg=./... -gcflags=all="-N -l"
# Run the rest of tests normally
sudo go test -v -cover -coverprofile=coverage-asan-module.out -coverpkg=./... -asan -gcflags=all="-N -l" -run Module
sudo go test -C cmd -coverprofile=coverage-asan.out -v -coverpkg=./... -asan -gcflags=all="-N -l" ./...
- name: Generate example module
run: |
rm -f example-module/pam_go.so
go generate -C example-module -v
test -e example-module/pam_go.so
git diff --exit-code example-module
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
coverage*.out
example-module/*.so
example-module/*.h
cmd/pam-moduler/tests/*/*.so
cmd/pam-moduler/tests/*/*.h

61
.golangci.yaml Normal file
View File

@@ -0,0 +1,61 @@
# This is for linting. To run it, please use:
# golangci-lint run ${MODULE}/... [--fix]
linters:
# linters to run in addition to default ones
enable:
- dupl
- durationcheck
- errname
- errorlint
- exportloopref
- forbidigo
- forcetypeassert
- gci
- godot
- gofmt
- gosec
- misspell
- nakedret
- nolintlint
- revive
- thelper
- tparallel
- unconvert
- unparam
- whitespace
run:
timeout: 5m
# Get all linter issues, even if duplicated
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
fix: false # we dont want this in CI
exclude:
# EXC0001 errcheck: most errors are in defer calls, which are safe to ignore and idiomatic Go (would be good to only ignore defer ones though)
- 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv|w\.Stop). is not checked'
# EXC0008 gosec: duplicated of errcheck
- (G104|G307)
# EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)'
- Potential file inclusion via variable
# We want named parameters even if unused, as they help better document the function
- unused-parameter
# Sometimes it is more readable it do a `if err:=a(); err != nil` tha simpy `return a()`
- if-return
nolintlint:
require-explanation: true
require-specific: true
linters-settings:
# Forbid the usage of deprecated ioutil and debug prints
forbidigo:
forbid:
- ioutil\.
- ^print.*$
# Never have naked return ever
nakedret:
max-func-lines: 1

127
README.md
View File

@@ -1,10 +1,128 @@
[![GoDoc](https://godoc.org/github.com/msteinert/pam?status.svg)](http://godoc.org/github.com/msteinert/pam) [![GoDoc](https://godoc.org/github.com/msteinert/pam/v2?status.svg)](http://godoc.org/github.com/msteinert/pam/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/msteinert/pam)](https://goreportcard.com/report/github.com/msteinert/pam) [![codecov](https://codecov.io/gh/msteinert/pam/graph/badge.svg?token=L1K3UTB065)](https://codecov.io/gh/msteinert/pam)
[![Go Report Card](https://goreportcard.com/badge/github.com/msteinert/pam/v2)](https://goreportcard.com/report/github.com/msteinert/pam/v2)
# Go PAM # Go PAM
This is a Go wrapper for the PAM application API. This is a Go wrapper for the PAM application API.
## Module support
Go PAM can also used to create PAM modules in a simple way, using the go.
The code can be generated using [pam-moduler](cmd/pam-moduler/moduler.go) and
an example how to use it using `go generate` create them is available as an
[example module](example-module/module.go).
### Modules and PAM applications
The modules generated with go can be used by any PAM application, however there
are some caveats, in fact a Go shared library could misbehave when loaded
improperly. In particular if a Go shared library is loaded and then the program
`fork`s, the library will have an undefined behavior.
This is the case of SSHd that loads a pam library before forking, making any
go PAM library to make it hang.
To solve this case, we can use a little workaround: to ensure that the go
library is loaded only after the program has forked, we can just `dload` it once
a PAM library is called, in this way go code will be loaded only after that the
PAM application has `fork`'ed.
To do this, we can use a very simple wrapper written in C:
```c
#include <dlfcn.h>
#include <limits.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
typedef int (*PamHandler)(pam_handle_t *,
int flags,
int argc,
const char **argv);
static void
on_go_module_removed (pam_handle_t *pamh,
void *go_module,
int error_status)
{
dlclose (go_module);
}
static void *
load_module (pam_handle_t *pamh,
const char *module_path)
{
void *go_module;
if (pam_get_data (pamh, "go-module", (const void **) &go_module) == PAM_SUCCESS)
return go_module;
go_module = dlopen (module_path, RTLD_LAZY);
if (!go_module)
return NULL;
pam_set_data (pamh, "go-module", go_module, on_go_module_removed);
return go_module;
}
static inline int
call_pam_function (pam_handle_t *pamh,
const char *function,
int flags,
int argc,
const char **argv)
{
char module_path[PATH_MAX] = {0};
const char *sub_module;
PamHandler func;
void *go_module;
if (argc < 1)
{
pam_error (pamh, "%s: no module provided", function);
return PAM_MODULE_UNKNOWN;
}
sub_module = argv[0];
argc -= 1;
argv = (argc == 0) ? NULL : &argv[1];
strncpy (module_path, sub_module, PATH_MAX - 1);
go_module = load_module (pamh, module_path);
if (!go_module)
{
pam_error (pamh, "Impossible to load module %s", module_path);
return PAM_OPEN_ERR;
}
*(void **) (&func) = dlsym (go_module, function);
if (!func)
{
pam_error (pamh, "Symbol %s not found in %s", function, module_path);
return PAM_OPEN_ERR;
}
return func (pamh, flags, argc, argv);
}
#define DEFINE_PAM_WRAPPER(name) \
PAM_EXTERN int \
(pam_sm_ ## name) (pam_handle_t * pamh, int flags, int argc, const char **argv) \
{ \
return call_pam_function (pamh, "pam_sm_" #name, flags, argc, argv); \
}
DEFINE_PAM_WRAPPER (authenticate)
DEFINE_PAM_WRAPPER (chauthtok)
DEFINE_PAM_WRAPPER (close_session)
DEFINE_PAM_WRAPPER (open_session)
DEFINE_PAM_WRAPPER (setcred)
```
## Testing ## Testing
To run the full suite, the tests must be run as the root user. To setup your To run the full suite, the tests must be run as the root user. To setup your
@@ -24,5 +142,8 @@ Then execute the tests:
$ sudo GOPATH=$GOPATH $(which go) test -v $ sudo GOPATH=$GOPATH $(which go) test -v
``` ```
[1]: http://godoc.org/github.com/msteinert/pam Other tests can instead run as user without any setup with
normal `go test ./...`
[1]: http://godoc.org/github.com/msteinert/pam/v2
[2]: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_ADG.html [2]: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_ADG.html

292
app-transaction.go Normal file
View File

@@ -0,0 +1,292 @@
//go:build !go_pam_module
package pam
/*
#include "transaction.h"
*/
import "C"
import (
"fmt"
"runtime/cgo"
"sync/atomic"
"unsafe"
)
// ConversationHandler is an interface for objects that can be used as
// conversation callbacks during PAM authentication.
type ConversationHandler interface {
// RespondPAM receives a message style and a message string. If the
// message Style is PromptEchoOff or PromptEchoOn then the function
// should return a response string.
RespondPAM(Style, string) (string, error)
}
// BinaryConversationHandler is an interface for objects that can be used as
// conversation callbacks during PAM authentication if binary protocol is going
// to be supported.
type BinaryConversationHandler interface {
ConversationHandler
// RespondPAMBinary receives a pointer to the binary message. It's up to
// the receiver to parse it according to the protocol specifications.
// The function can return a byte array that will passed as pointer back
// to the module.
RespondPAMBinary(BinaryPointer) ([]byte, error)
}
// BinaryPointerConversationHandler is an interface for objects that can be used as
// conversation callbacks during PAM authentication if binary protocol is going
// to be supported.
type BinaryPointerConversationHandler interface {
ConversationHandler
// RespondPAMBinary receives a pointer to the binary message. It's up to
// the receiver to parse it according to the protocol specifications.
// The function must return a pointer that is allocated via malloc or
// similar, as it's expected to be free'd by the conversation handler.
RespondPAMBinary(BinaryPointer) (BinaryPointer, error)
}
// ConversationFunc is an adapter to allow the use of ordinary functions as
// conversation callbacks.
type ConversationFunc func(Style, string) (string, error)
// RespondPAM is a conversation callback adapter.
func (f ConversationFunc) RespondPAM(s Style, msg string) (string, error) {
return f(s, msg)
}
// BinaryConversationFunc is an adapter to allow the use of ordinary functions
// as binary (only) conversation callbacks.
type BinaryConversationFunc func(BinaryPointer) ([]byte, error)
// RespondPAMBinary is a conversation callback adapter.
func (f BinaryConversationFunc) RespondPAMBinary(ptr BinaryPointer) ([]byte, error) {
return f(ptr)
}
// RespondPAM is a dummy conversation callback adapter.
func (f BinaryConversationFunc) RespondPAM(Style, string) (string, error) {
return "", ErrConv
}
// BinaryPointerConversationFunc is an adapter to allow the use of ordinary
// functions as binary pointer (only) conversation callbacks.
type BinaryPointerConversationFunc func(BinaryPointer) (BinaryPointer, error)
// RespondPAMBinary is a conversation callback adapter.
func (f BinaryPointerConversationFunc) RespondPAMBinary(ptr BinaryPointer) (BinaryPointer, error) {
return f(ptr)
}
// RespondPAM is a dummy conversation callback adapter.
func (f BinaryPointerConversationFunc) RespondPAM(Style, string) (string, error) {
return "", ErrConv
}
// _go_pam_conv_handler is a C wrapper for the conversation callback function.
//
//export _go_pam_conv_handler
func _go_pam_conv_handler(msg *C.struct_pam_message, c C.uintptr_t, outMsg **C.char) C.int {
convHandler, ok := cgo.Handle(c).Value().(ConversationHandler)
if !ok || convHandler == nil {
return C.int(ErrConv)
}
replyMsg, r := pamConvHandler(Style(msg.msg_style), msg.msg, convHandler)
*outMsg = replyMsg
return r
}
// pamConvHandler is a Go wrapper for the conversation callback function.
func pamConvHandler(style Style, msg *C.char, handler ConversationHandler) (*C.char, C.int) {
var r string
var err error
switch cb := handler.(type) {
case BinaryConversationHandler:
if style == BinaryPrompt {
bytes, err := cb.RespondPAMBinary(BinaryPointer(msg))
if err != nil {
return nil, C.int(ErrConv)
}
if bytes == nil {
return nil, success
}
return (*C.char)(C.CBytes(bytes)), success
}
handler = cb
case BinaryPointerConversationHandler:
if style == BinaryPrompt {
ptr, err := cb.RespondPAMBinary(BinaryPointer(msg))
if err != nil {
defer C.free(unsafe.Pointer(ptr))
return nil, C.int(ErrConv)
}
return (*C.char)(ptr), success
}
handler = cb
case ConversationHandler:
if style == BinaryPrompt {
return nil, C.int(ErrConv)
}
handler = cb
default:
return nil, C.int(ErrConv)
}
r, err = handler.RespondPAM(style, C.GoString(msg))
if err != nil {
return nil, C.int(ErrConv)
}
return C.CString(r), success
}
// Transaction is the application's handle for a PAM transaction.
type Transaction struct {
transactionBase
conv *C.struct_pam_conv
c cgo.Handle
}
// Start initiates a new PAM transaction. Service is treated identically to
// how pam_start treats it internally.
//
// All application calls to PAM begin with Start*. The returned
// transaction provides an interface to the remainder of the API.
//
// It's responsibility of the Transaction owner to release all the resources
// allocated underneath by PAM by calling End() once done.
//
// It's not advised to End the transaction using a runtime.SetFinalizer unless
// you're absolutely sure that your stack is multi-thread friendly (normally it
// is not!) and using a LockOSThread/UnlockOSThread pair.
func Start(service, user string, handler ConversationHandler) (*Transaction, error) {
return start(service, user, handler, "")
}
// StartFunc registers the handler func as a conversation handler and starts
// the transaction (see Start() documentation).
func StartFunc(service, user string, handler func(Style, string) (string, error)) (*Transaction, error) {
return start(service, user, ConversationFunc(handler), "")
}
// StartConfDir initiates a new PAM transaction. Service is treated identically to
// how pam_start treats it internally.
// confdir allows to define where all pam services are defined. This is used to provide
// custom paths for tests.
//
// All application calls to PAM begin with Start*. The returned
// transaction provides an interface to the remainder of the API.
//
// It's responsibility of the Transaction owner to release all the resources
// allocated underneath by PAM by calling End() once done.
//
// It's not advised to End the transaction using a runtime.SetFinalizer unless
// you're absolutely sure that your stack is multi-thread friendly (normally it
// is not!) and using a LockOSThread/UnlockOSThread pair.
func StartConfDir(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) {
if !CheckPamHasStartConfdir() {
return nil, fmt.Errorf(
"%w: StartConfDir was used, but the pam version on the system is not recent enough",
ErrSystem)
}
return start(service, user, handler, confDir)
}
func start(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) {
switch handler.(type) {
case BinaryConversationHandler:
if !CheckPamHasBinaryProtocol() {
return nil, fmt.Errorf("%w: BinaryConversationHandler was used, but it is not supported by this platform",
ErrSystem)
}
case BinaryPointerConversationHandler:
if !CheckPamHasBinaryProtocol() {
return nil, fmt.Errorf(
"%w: BinaryPointerConversationHandler was used, but it is not supported by this platform",
ErrSystem)
}
}
t := &Transaction{
conv: &C.struct_pam_conv{},
c: cgo.NewHandle(handler),
}
C.init_pam_conv(t.conv, C.uintptr_t(t.c))
s := C.CString(service)
defer C.free(unsafe.Pointer(s))
var u *C.char
if len(user) != 0 {
u = C.CString(user)
defer C.free(unsafe.Pointer(u))
}
var err error
if confDir == "" {
err = t.handlePamStatus(C.pam_start(s, u, t.conv, &t.handle))
} else {
c := C.CString(confDir)
defer C.free(unsafe.Pointer(c))
err = t.handlePamStatus(C.pam_start_confdir(s, u, t.conv, c, &t.handle))
}
if err != nil {
var _ = t.End()
return nil, err
}
return t, nil
}
// Authenticate is used to authenticate the user.
//
// Valid flags: Silent, DisallowNullAuthtok
func (t *Transaction) Authenticate(f Flags) error {
return t.handlePamStatus(C.pam_authenticate(t.handle, C.int(f)))
}
// SetCred is used to establish, maintain and delete the credentials of a
// user.
//
// Valid flags: EstablishCred, DeleteCred, ReinitializeCred, RefreshCred
func (t *Transaction) SetCred(f Flags) error {
return t.handlePamStatus(C.pam_setcred(t.handle, C.int(f)))
}
// AcctMgmt is used to determine if the user's account is valid.
//
// Valid flags: Silent, DisallowNullAuthtok
func (t *Transaction) AcctMgmt(f Flags) error {
return t.handlePamStatus(C.pam_acct_mgmt(t.handle, C.int(f)))
}
// ChangeAuthTok is used to change the authentication token.
//
// Valid flags: Silent, ChangeExpiredAuthtok
func (t *Transaction) ChangeAuthTok(f Flags) error {
return t.handlePamStatus(C.pam_chauthtok(t.handle, C.int(f)))
}
// OpenSession sets up a user session for an authenticated user.
//
// Valid flags: Slient
func (t *Transaction) OpenSession(f Flags) error {
return t.handlePamStatus(C.pam_open_session(t.handle, C.int(f)))
}
// CloseSession closes a previously opened session.
//
// Valid flags: Silent
func (t *Transaction) CloseSession(f Flags) error {
return t.handlePamStatus(C.pam_close_session(t.handle, C.int(f)))
}
// End cleans up the PAM handle and deletes the callback function.
// It must be called when done with the transaction.
func (t *Transaction) End() error {
handle := atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&t.handle)), nil)
if handle == nil {
return nil
}
defer t.c.Delete()
return t.handlePamStatus(C.pam_end((*C.pam_handle_t)(handle),
C.int(t.lastStatus.Load())))
}

313
cmd/pam-moduler/moduler.go Normal file
View File

@@ -0,0 +1,313 @@
// pam-moduler is a tool to automate the creation of PAM Modules in go
//
// The file is created in the same package and directory as the package that
// creates the module
//
// The module implementation should define a pamModuleHandler object that
// implements the pam.ModuleHandler type and that will be used for each callback
//
// Otherwise it's possible to provide a typename from command line that will
// be used for this purpose
//
// For example:
//
// //go:generate go run github.com/msteinert/pam/v2/pam-moduler
// //go:generate go generate --skip="pam_module"
// package main
//
// import "github.com/msteinert/pam/v2"
//
// type ExampleHandler struct{}
// var pamModuleHandler pam.ModuleHandler = &ExampleHandler{}
//
// func (h *ExampleHandler) AcctMgmt(pam.ModuleTransaction, pam.Flags, []string) error {
// return nil
// }
//
// func (h *ExampleHandler) Authenticate(pam.ModuleTransaction, pam.Flags, []string) error {
// return nil
// }
//
// func (h *ExampleHandler) ChangeAuthTok(pam.ModuleTransaction, pam.Flags, []string) error {
// return nil
// }
//
// func (h *ExampleHandler) OpenSession(pam.ModuleTransaction, pam.Flags, []string) error {
// return nil
// }
//
// func (h *ExampleHandler) CloseSession(pam.ModuleTransaction, pam.Flags, []string) error {
// return nil
// }
//
// func (h *ExampleHandler) SetCred(pam.ModuleTransaction, pam.Flags, []string) error {
// return nil
// }
// Package main provides the module shared library.
package main
import (
"bytes"
"flag"
"fmt"
"go/format"
"log"
"os"
"path/filepath"
"strings"
)
const toolName = "pam-moduler"
var (
output = flag.String("output", "", "output file name; default srcdir/pam_module.go")
libName = flag.String("libname", "", "output library name; default pam_go.so")
typeName = flag.String("type", "", "type name to be used as pam.ModuleHandler")
buildTags = flag.String("tags", "", "build tags expression to append to use in the go:build directive")
skipGenerator = flag.Bool("no-generator", false, "whether to add go:generator directives to the generated source")
moduleBuildFlags = flag.String("build-flags", "", "comma-separated list of go build flags to use when generating the module")
moduleBuildTags = flag.String("build-tags", "", "comma-separated list of build tags to use when generating the module")
noMain = flag.Bool("no-main", false, "whether to add an empty main to generated file")
parallelConv = flag.Bool("parallel-conv", false, "whether to support performing PAM conversations in parallel")
)
// Usage is a replacement usage function for the flags package.
func Usage() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", toolName)
fmt.Fprintf(os.Stderr, "\t%s [flags] [-output O] [-libname pam_go] [-type N]\n", toolName)
flag.PrintDefaults()
}
func main() {
log.SetFlags(0)
log.SetPrefix(toolName + ": ")
flag.Usage = Usage
flag.Parse()
if *skipGenerator {
if *libName != "" {
fmt.Fprintf(os.Stderr,
"Generator directives disabled, libname will have no effect\n")
}
if *moduleBuildTags != "" {
fmt.Fprintf(os.Stderr,
"Generator directives disabled, build-tags will have no effect\n")
}
if *moduleBuildFlags != "" {
fmt.Fprintf(os.Stderr,
"Generator directives disabled, build-flags will have no effect\n")
}
}
lib := *libName
if lib == "" {
lib = "pam_go"
} else {
lib, _ = strings.CutSuffix(lib, ".so")
lib, _ = strings.CutPrefix(lib, "lib")
}
outputName, _ := strings.CutSuffix(*output, ".go")
if outputName == "" {
baseName := "pam_module"
outputName = filepath.Join(".", strings.ToLower(baseName))
}
outputName = outputName + ".go"
var tags string
if *buildTags != "" {
tags = *buildTags
}
generateTags := []string{"go_pam_module"}
if len(*moduleBuildTags) > 0 {
generateTags = append(generateTags, strings.Split(*moduleBuildTags, ",")...)
}
var buildFlags []string
if *moduleBuildFlags != "" {
buildFlags = strings.Split(*moduleBuildFlags, ",")
}
g := Generator{
outputName: outputName,
libName: lib,
tags: tags,
buildFlags: buildFlags,
generateTags: generateTags,
noMain: *noMain,
typeName: *typeName,
parallelConv: *parallelConv,
}
// Print the header and package clause.
g.printf("// Code generated by \"%s %s\"; DO NOT EDIT.\n",
toolName, strings.Join(os.Args[1:], " "))
g.printf("\n")
// Generate the code
g.generate()
// Format the output.
src := g.format()
// Write to file.
err := os.WriteFile(outputName, src, 0600)
if err != nil {
log.Fatalf("writing output: %s", err)
}
}
// Generator holds the state of the analysis. Primarily used to buffer
// the output for format.Source.
type Generator struct {
buf bytes.Buffer // Accumulated output.
libName string
outputName string
typeName string
tags string
generateTags []string
buildFlags []string
noMain bool
parallelConv bool
}
func (g *Generator) printf(format string, args ...interface{}) {
fmt.Fprintf(&g.buf, format, args...)
}
// generate produces the String method for the named type.
func (g *Generator) generate() {
if g.tags != "" {
g.printf("//go:build %s\n", g.tags)
}
var buildTagsArg string
if len(g.generateTags) > 0 {
buildTagsArg = fmt.Sprintf("-tags %s", strings.Join(g.generateTags, ","))
}
var transactionCreator = "NewModuleTransactionInvoker"
if g.parallelConv {
transactionCreator = "NewModuleTransactionInvokerParallelConv"
}
// We use a slice since we want to keep order, for reproducible builds.
vFuncs := []struct {
cName string
goName string
}{
{"authenticate", "Authenticate"},
{"setcred", "SetCred"},
{"acct_mgmt", "AcctMgmt"},
{"open_session", "OpenSession"},
{"close_session", "CloseSession"},
{"chauthtok", "ChangeAuthTok"},
}
g.printf(`//go:generate go build "-ldflags=-extldflags -Wl,-soname,%[2]s.so" `+
`-buildmode=c-shared -o %[2]s.so %[3]s %[4]s
`,
g.outputName, g.libName, buildTagsArg, strings.Join(g.buildFlags, " "))
g.printf(`
// Package main is the package for the PAM module library.
package main
/*
#cgo LDFLAGS: -lpam -fPIC
#include <security/pam_modules.h>
typedef const char _const_char_t;
*/
import "C"
import (
"errors"
"fmt"
"os"
"unsafe"
"github.com/msteinert/pam/v2"
)
`)
if g.typeName != "" {
g.printf(`
var pamModuleHandler pam.ModuleHandler = &%[1]s{}
`, g.typeName)
} else {
g.printf(`
// Do a typecheck at compile time
var _ pam.ModuleHandler = pamModuleHandler;
`)
}
g.printf(`
// sliceFromArgv returns a slice of strings given to the PAM module.
func sliceFromArgv(argc C.int, argv **C._const_char_t) []string {
r := make([]string, 0, argc)
for _, s := range unsafe.Slice(argv, argc) {
r = append(r, C.GoString(s))
}
return r
}
// handlePamCall is the function that translates C pam requests to Go.
func handlePamCall(pamh *C.pam_handle_t, flags C.int, argc C.int,
argv **C._const_char_t, moduleFunc pam.ModuleHandlerFunc) C.int {
if pamModuleHandler == nil {
return C.int(pam.ErrNoModuleData)
}
if moduleFunc == nil {
return C.int(pam.ErrIgnore)
}
mt := pam.%s(pam.NativeHandle(pamh))
err := mt.InvokeHandler(moduleFunc, pam.Flags(flags),
sliceFromArgv(argc, argv))
if err == nil {
return 0
}
if (pam.Flags(flags) & pam.Silent) == 0 && !errors.Is(err, pam.ErrIgnore) {
fmt.Fprintf(os.Stderr, "module returned error: %%v\n", err)
}
var pamErr pam.Error
if errors.As(err, &pamErr) {
return C.int(pamErr)
}
return C.int(pam.ErrSystem)
}
`, transactionCreator)
for _, f := range vFuncs {
g.printf(`
//export pam_sm_%[1]s
func pam_sm_%[1]s(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.%[2]s)
}
`, f.cName, f.goName)
}
if !g.noMain {
g.printf("\nfunc main() {}\n")
}
}
// format returns the gofmt-ed contents of the Generator's buffer.
func (g *Generator) format() []byte {
src, err := format.Source(g.buf.Bytes())
if err != nil {
// Should never happen, but can arise when developing this code.
// The user can compile the output to see the error.
log.Printf("warning: internal error: invalid Go generated: %s", err)
log.Printf("warning: compile the package to analyze the error")
return g.buf.Bytes()
}
return src
}

View File

@@ -0,0 +1,119 @@
//go:generate go run github.com/msteinert/pam/v2/cmd/pam-moduler -libname "pam_godebug.so"
//go:generate go generate --skip="pam_module.go"
// This is a similar implementation of pam_debug.so
// Package main is the package for the debug PAM module library
package main
import (
"fmt"
"strings"
"github.com/msteinert/pam/v2"
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
)
var pamModuleHandler pam.ModuleHandler = &DebugModule{}
var _ = pamModuleHandler
var moduleArgsRetTypes = map[string]error{
"success": nil,
"open_err": pam.ErrOpen,
"symbol_err": pam.ErrSymbol,
"service_err": pam.ErrService,
"system_err": pam.ErrSystem,
"buf_err": pam.ErrBuf,
"perm_denied": pam.ErrPermDenied,
"auth_err": pam.ErrAuth,
"cred_insufficient": pam.ErrCredInsufficient,
"authinfo_unavail": pam.ErrAuthinfoUnavail,
"user_unknown": pam.ErrUserUnknown,
"maxtries": pam.ErrMaxtries,
"new_authtok_reqd": pam.ErrNewAuthtokReqd,
"acct_expired": pam.ErrAcctExpired,
"session_err": pam.ErrSession,
"cred_unavail": pam.ErrCredUnavail,
"cred_expired": pam.ErrCredExpired,
"cred_err": pam.ErrCred,
"no_module_data": pam.ErrNoModuleData,
"conv_err": pam.ErrConv,
"authtok_err": pam.ErrAuthtok,
"authtok_recover_err": pam.ErrAuthtokRecovery,
"authtok_lock_busy": pam.ErrAuthtokLockBusy,
"authtok_disable_aging": pam.ErrAuthtokDisableAging,
"try_again": pam.ErrTryAgain,
"ignore": pam.ErrIgnore,
"abort": pam.ErrAbort,
"authtok_expired": pam.ErrAuthtokExpired,
"module_unknown": pam.ErrModuleUnknown,
"bad_item": pam.ErrBadItem,
"conv_again": pam.ErrConvAgain,
"incomplete": pam.ErrIncomplete,
}
var debugModuleArgs = []string{"auth", "cred", "acct", "prechauthtok",
"chauthtok", "open_session", "close_session"}
// DebugModule is the PAM module structure.
type DebugModule struct {
utils.BaseModule
}
func (dm *DebugModule) getReturnType(args []string, key string) error {
var value string
for _, a := range args {
v, found := strings.CutPrefix(a, key+"=")
if found {
value = v
}
}
if value == "" {
return fmt.Errorf("Value not found")
}
if ret, found := moduleArgsRetTypes[value]; found {
return ret
}
return fmt.Errorf("Parameter %s not known", value)
}
func (dm *DebugModule) handleCall(args []string, action string) error {
err := dm.getReturnType(args, action)
if err == nil {
return nil
}
return fmt.Errorf("error %w", err)
}
// AcctMgmt is a PAM handler.
func (dm *DebugModule) AcctMgmt(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return dm.handleCall(args, "acct")
}
// Authenticate is a PAM handler.
func (dm *DebugModule) Authenticate(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return dm.handleCall(args, "auth")
}
// ChangeAuthTok is a PAM handler.
func (dm *DebugModule) ChangeAuthTok(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return dm.handleCall(args, "chauthtok")
}
// OpenSession is a PAM handler.
func (dm *DebugModule) OpenSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return dm.handleCall(args, "open_session")
}
// CloseSession is a PAM handler.
func (dm *DebugModule) CloseSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return dm.handleCall(args, "close_session")
}
// SetCred is a PAM handler.
func (dm *DebugModule) SetCred(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return dm.handleCall(args, "cred")
}

View File

@@ -0,0 +1,120 @@
package main
import (
"errors"
"fmt"
"testing"
"github.com/msteinert/pam/v2"
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
)
func Test_DebugModule_ActionStatus(t *testing.T) {
t.Parallel()
module := DebugModule{}
for ret, expected := range moduleArgsRetTypes {
ret := ret
expected := expected
for actionName, action := range utils.Actions {
actionName := actionName
action := action
t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) {
t.Parallel()
moduleArgs := make([]string, 0)
for _, a := range debugModuleArgs {
moduleArgs = append(moduleArgs, fmt.Sprintf("%s=%s", a, ret))
}
mt := pam.ModuleTransactionInvoker(nil)
var err error
switch action {
case utils.Account:
err = module.AcctMgmt(mt, 0, moduleArgs)
case utils.Auth:
err = module.Authenticate(mt, 0, moduleArgs)
case utils.Password:
err = module.ChangeAuthTok(mt, 0, moduleArgs)
case utils.Session:
err = module.OpenSession(mt, 0, moduleArgs)
}
if !errors.Is(err, expected) {
t.Fatalf("error #unexpected %#v vs %#v", expected, err)
}
})
}
}
}
func Test_DebugModuleTransaction_ActionStatus(t *testing.T) {
t.Parallel()
if !pam.CheckPamHasStartConfdir() {
t.Skip("this requires PAM with Conf dir support")
}
ts := utils.NewTestSetup(t, utils.WithWorkDir())
modulePath := ts.GenerateModule(".", "pam_godebug.so")
for ret, expected := range moduleArgsRetTypes {
ret := ret
expected := expected
for actionName, action := range utils.Actions {
ret := ret
expected := expected
actionName := actionName
action := action
t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) {
t.Parallel()
serviceName := ret + "-" + actionName
moduleArgs := make([]string, 0)
for _, a := range debugModuleArgs {
moduleArgs = append(moduleArgs, fmt.Sprintf("%s=%s", a, ret))
}
control := utils.Requisite
fallbackModule := utils.Permit
if ret == "success" {
fallbackModule = utils.Deny
control = utils.Sufficient
}
ts.CreateService(serviceName, []utils.ServiceLine{
{Action: action, Control: control, Module: modulePath, Args: moduleArgs},
{Action: action, Control: control, Module: fallbackModule.String(), Args: []string{}},
})
tx, err := pam.StartConfDir(serviceName, "user", nil, ts.WorkDir())
if err != nil {
t.Fatalf("start #error: %v", err)
}
defer func() {
err := tx.End()
if err != nil {
t.Fatalf("end #error: %v", err)
}
}()
switch action {
case utils.Account:
err = tx.AcctMgmt(pam.Silent)
case utils.Auth:
err = tx.Authenticate(pam.Silent)
case utils.Password:
err = tx.ChangeAuthTok(pam.Silent)
case utils.Session:
err = tx.OpenSession(pam.Silent)
}
if errors.Is(expected, pam.ErrIgnore) {
// Ignore can't be returned
expected = nil
}
if !errors.Is(err, expected) {
t.Fatalf("error #unexpected %#v vs %#v", expected, err)
}
})
}
}
}

View File

@@ -0,0 +1,96 @@
// Code generated by "pam-moduler -libname pam_godebug.so"; DO NOT EDIT.
//go:generate go build "-ldflags=-extldflags -Wl,-soname,pam_godebug.so" -buildmode=c-shared -o pam_godebug.so -tags go_pam_module
// Package main is the package for the PAM module library.
package main
/*
#cgo LDFLAGS: -lpam -fPIC
#include <security/pam_modules.h>
typedef const char _const_char_t;
*/
import "C"
import (
"errors"
"fmt"
"github.com/msteinert/pam/v2"
"os"
"unsafe"
)
// Do a typecheck at compile time
var _ pam.ModuleHandler = pamModuleHandler
// sliceFromArgv returns a slice of strings given to the PAM module.
func sliceFromArgv(argc C.int, argv **C._const_char_t) []string {
r := make([]string, 0, argc)
for _, s := range unsafe.Slice(argv, argc) {
r = append(r, C.GoString(s))
}
return r
}
// handlePamCall is the function that translates C pam requests to Go.
func handlePamCall(pamh *C.pam_handle_t, flags C.int, argc C.int,
argv **C._const_char_t, moduleFunc pam.ModuleHandlerFunc) C.int {
if pamModuleHandler == nil {
return C.int(pam.ErrNoModuleData)
}
if moduleFunc == nil {
return C.int(pam.ErrIgnore)
}
mt := pam.NewModuleTransactionInvoker(pam.NativeHandle(pamh))
err := mt.InvokeHandler(moduleFunc, pam.Flags(flags),
sliceFromArgv(argc, argv))
if err == nil {
return 0
}
if (pam.Flags(flags)&pam.Silent) == 0 && !errors.Is(err, pam.ErrIgnore) {
fmt.Fprintf(os.Stderr, "module returned error: %v\n", err)
}
var pamErr pam.Error
if errors.As(err, &pamErr) {
return C.int(pamErr)
}
return C.int(pam.ErrSystem)
}
//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.Authenticate)
}
//export pam_sm_setcred
func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.SetCred)
}
//export pam_sm_acct_mgmt
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.AcctMgmt)
}
//export pam_sm_open_session
func pam_sm_open_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.OpenSession)
}
//export pam_sm_close_session
func pam_sm_close_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.CloseSession)
}
//export pam_sm_chauthtok
func pam_sm_chauthtok(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.ChangeAuthTok)
}
func main() {}

View File

@@ -0,0 +1,230 @@
// Package main is the package for the integration tester module PAM shared library.
package main
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"io"
"net"
"runtime"
)
// Request is a serializable integration module tester structure request.
type Request struct {
Action string
ActionArgs []interface{}
}
// Result is a serializable integration module tester structure result.
type Result = Request
// NewRequest returns a new Request.
func NewRequest(action string, actionArgs ...interface{}) Request {
return Request{action, actionArgs}
}
// GOB serializes the request in binary format.
func (r *Request) GOB() ([]byte, error) {
b := bytes.Buffer{}
e := gob.NewEncoder(&b)
if err := e.Encode(r); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// NewRequestFromGOB gets a Request from a serialized binary.
func NewRequestFromGOB(data []byte) (*Request, error) {
b := bytes.Buffer{}
b.Write(data)
d := gob.NewDecoder(&b)
var req Request
if err := d.Decode(&req); err != nil {
return nil, err
}
return &req, nil
}
const bufSize = 1024
type connectionHandler struct {
inOutData chan []byte
outErr chan error
SocketPath string
}
// Listener is a socket listener.
type Listener struct {
connectionHandler
listener net.Listener
}
// NewListener creates a new Listener.
func NewListener(socketPath string) *Listener {
if len(socketPath) > 90 {
// See https://manpages.ubuntu.com/manpages/jammy/man7/sys_un.h.7posix.html#application%20usage
panic(fmt.Sprintf("Socket path %s too long", socketPath))
}
return &Listener{connectionHandler{SocketPath: socketPath}, nil}
}
// WaitForData waits for result data (or an error) on connection to be returned.
func (c *connectionHandler) WaitForData() (*Result, error) {
data, err := <-c.inOutData, <-c.outErr
if err != nil {
if errors.Is(err, io.EOF) {
return nil, nil
}
return nil, err
}
req, err := NewRequestFromGOB(data)
if err != nil {
return nil, err
}
return req, nil
}
// SendRequest sends a request to the connection.
func (c *connectionHandler) SendRequest(req *Request) error {
bytes, err := req.GOB()
if err != nil {
return err
}
c.inOutData <- bytes
return nil
}
// SendResult sends the Result to the connection.
func (c *connectionHandler) SendResult(res *Result) error {
return c.SendRequest(res)
}
// DoRequest performs a Request on the connection, waiting for data.
func (c *connectionHandler) DoRequest(req *Request) (*Result, error) {
if err := c.SendRequest(req); err != nil {
return nil, err
}
return c.WaitForData()
}
// Send performs a request.
func (r *Request) Send(c *connectionHandler) error {
return c.SendRequest(r)
}
// ErrAlreadyListening is the error if a listener is already set.
var ErrAlreadyListening = errors.New("listener already set")
// StartListening initiates the unix listener.
func (l *Listener) StartListening() error {
if l.listener != nil {
return ErrAlreadyListening
}
listener, err := net.Listen("unix", l.SocketPath)
if err != nil {
return err
}
l.listener = listener
l.inOutData, l.outErr = make(chan []byte), make(chan error)
go func() {
bytes, err := func() ([]byte, error) {
for {
c, err := l.listener.Accept()
if err != nil {
return nil, err
}
for {
buf := make([]byte, bufSize)
nr, err := c.Read(buf)
if err != nil {
return buf, err
}
data := buf[0:nr]
l.inOutData <- data
l.outErr <- nil
_, err = c.Write(<-l.inOutData)
if err != nil {
return nil, err
}
}
}
}()
l.inOutData <- bytes
l.outErr <- err
}()
return nil
}
// Connector is a connection type.
type Connector struct {
connectionHandler
connection net.Conn
}
// NewConnector creates a new connection.
func NewConnector(socketPath string) *Connector {
return &Connector{connectionHandler{SocketPath: socketPath}, nil}
}
// ErrAlreadyConnected is the error if a connection is already set.
var ErrAlreadyConnected = errors.New("connection already set")
// Connect connects to a listening unix socket.
func (c *Connector) Connect() error {
if c.connection != nil {
return ErrAlreadyConnected
}
connection, err := net.Dial("unix", c.SocketPath)
if err != nil {
return err
}
runtime.SetFinalizer(c, func(c *Connector) {
c.connection.Close()
})
c.connection = connection
c.inOutData, c.outErr = make(chan []byte), make(chan error)
go func() {
buf := make([]byte, bufSize)
writeAndRead := func() ([]byte, error) {
data := <-c.inOutData
_, err := c.connection.Write(data)
if err != nil {
return nil, err
}
n, err := c.connection.Read(buf[:])
if err != nil {
return nil, err
}
return buf[0:n], nil
}
for {
bytes, err := writeAndRead()
c.inOutData <- bytes
c.outErr <- err
}
}()
return nil
}

View File

@@ -0,0 +1,107 @@
package main
import (
"errors"
"path/filepath"
"reflect"
"testing"
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
)
func ensureNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("unexpected error %v", err)
}
}
func ensureError(t *testing.T, err error, expected error) {
t.Helper()
if err == nil {
t.Fatalf("error was expected, got none")
}
if !errors.Is(err, expected) {
t.Fatalf("error %v was expected, got %v", err, expected)
}
}
func ensureEqual(t *testing.T, a any, b any) {
t.Helper()
if !reflect.DeepEqual(a, b) {
t.Fatalf("values mismatch %#v vs %#v", a, b)
}
}
func Test_Communication(t *testing.T) {
t.Parallel()
ts := utils.NewTestSetup(t, utils.WithWorkDir())
for _, name := range []string{"test-1", "test-2"} {
name := name
t.Run(name, func(t *testing.T) {
t.Parallel()
socketPath := filepath.Join(ts.WorkDir(), name+".socket")
listener := NewListener(socketPath)
connector := NewConnector(socketPath)
ensureNoError(t, listener.StartListening())
ensureNoError(t, connector.Connect())
ensureError(t, listener.StartListening(), ErrAlreadyListening)
ensureError(t, connector.Connect(), ErrAlreadyConnected)
resChan, errChan := make(chan *Result), make(chan error)
go func() {
res, err := listener.WaitForData()
resChan <- res
errChan <- err
}()
req := NewRequest("A Request")
ensureNoError(t, connector.SendRequest(&req))
res, err := <-resChan, <-errChan
ensureNoError(t, err)
ensureEqual(t, *res, req)
go func() {
res := NewRequest("Listener result")
ensureNoError(t, listener.SendResult(&res))
}()
res, err = connector.WaitForData()
ensureNoError(t, err)
ensureEqual(t, *res, NewRequest("Listener result"))
go func() {
req, err := listener.WaitForData()
res := NewRequest("Response", *req)
defer func() {
resChan <- &res
errChan <- err
}()
ensureNoError(t, listener.SendResult(&res))
}()
done := make(chan bool)
req = NewRequest("Requesting...")
go func() {
defer func() {
done <- true
}()
res, err := connector.DoRequest(&req)
ensureNoError(t, err)
ensureEqual(t, *res, NewRequest("Response", req))
}()
res, err = <-resChan, <-errChan
ensureNoError(t, err)
ensureEqual(t, *res, NewRequest("Response", req))
<-done
})
}
}

View File

@@ -0,0 +1,159 @@
//go:generate go run github.com/msteinert/pam/v2/cmd/pam-moduler -type integrationTesterModule -parallel-conv
//go:generate go generate --skip="pam_module.go"
// Package main is the package for the integration tester module PAM shared library.
package main
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/msteinert/pam/v2"
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
)
type integrationTesterModule struct {
utils.BaseModule
}
type authRequest struct {
mt pam.ModuleTransaction
lastError error
}
func (m *integrationTesterModule) handleRequest(authReq *authRequest, r *Request) (res *Result, err error) {
switch r.Action {
case "bye":
return nil, authReq.lastError
}
defer func() {
if p := recover(); p != nil {
if s, ok := p.(string); ok {
if strings.HasPrefix(s, "reflect:") {
res = nil
err = &utils.SerializableError{Msg: fmt.Sprintf(
"error on request %v: %v", *r, p)}
authReq.lastError = err
return
}
}
panic(p)
}
if err != nil {
authReq.lastError = err
}
}()
method := reflect.ValueOf(authReq.mt).MethodByName(r.Action)
if method == (reflect.Value{}) {
return nil, &utils.SerializableError{Msg: fmt.Sprintf(
"no method %s found", r.Action)}
}
var args []reflect.Value
for i, arg := range r.ActionArgs {
switch v := arg.(type) {
case SerializableStringConvRequest:
args = append(args, reflect.ValueOf(
pam.NewStringConvRequest(v.Style, v.Request)))
case SerializableBinaryConvRequest:
args = append(args, reflect.ValueOf(
pam.NewBinaryConvRequestFromBytes(v.Request)))
default:
if arg == nil {
args = append(args, reflect.Zero(method.Type().In(i)))
} else {
args = append(args, reflect.ValueOf(arg))
}
}
}
res = &Result{Action: "return"}
for _, ret := range method.Call(args) {
iface := ret.Interface()
switch value := iface.(type) {
case pam.StringConvResponse:
res.ActionArgs = append(res.ActionArgs,
SerializableStringConvResponse{value.Style(), value.Response()})
case pam.BinaryConvResponse:
data, err := value.Decode(utils.TestBinaryDataDecoder)
if err != nil {
return nil, err
}
res.ActionArgs = append(res.ActionArgs, SerializableBinaryConvResponse{data})
case pam.Error:
authReq.lastError = value
res.ActionArgs = append(res.ActionArgs, value)
case error:
var pamError pam.Error
if errors.As(value, &pamError) {
retErr := &SerializablePamError{Msg: value.Error(),
RetStatus: pamError}
authReq.lastError = retErr
res.ActionArgs = append(res.ActionArgs, retErr)
return res, err
}
authReq.lastError = value
res.ActionArgs = append(res.ActionArgs,
&utils.SerializableError{Msg: value.Error()})
default:
res.ActionArgs = append(res.ActionArgs, iface)
}
}
return res, err
}
func (m *integrationTesterModule) handleError(err error) *Result {
return &Result{
Action: "error",
ActionArgs: []interface{}{&utils.SerializableError{Msg: err.Error()}},
}
}
func (m *integrationTesterModule) Authenticate(mt pam.ModuleTransaction, _ pam.Flags, args []string) error {
if len(args) != 1 {
return errors.New("Invalid arguments")
}
authRequest := authRequest{mt, nil}
connection := NewConnector(args[0])
if err := connection.Connect(); err != nil {
return err
}
connectionHandler := func() error {
if err := connection.SendRequest(&Request{Action: "hello"}); err != nil {
return err
}
for {
req, err := connection.WaitForData()
if err != nil {
return err
}
res, err := m.handleRequest(&authRequest, req)
if err != nil {
_ = connection.SendResult(m.handleError(err))
return err
}
if res == nil {
return nil
}
if err := connection.SendResult(res); err != nil {
_ = connection.SendResult(m.handleError(err))
return err
}
}
}
if err := connectionHandler(); err != nil {
return err
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
// Code generated by "pam-moduler -type integrationTesterModule -parallel-conv"; DO NOT EDIT.
//go:generate go build "-ldflags=-extldflags -Wl,-soname,pam_go.so" -buildmode=c-shared -o pam_go.so -tags go_pam_module
// Package main is the package for the PAM module library.
package main
/*
#cgo LDFLAGS: -lpam -fPIC
#include <security/pam_modules.h>
typedef const char _const_char_t;
*/
import "C"
import (
"errors"
"fmt"
"github.com/msteinert/pam/v2"
"os"
"unsafe"
)
var pamModuleHandler pam.ModuleHandler = &integrationTesterModule{}
// sliceFromArgv returns a slice of strings given to the PAM module.
func sliceFromArgv(argc C.int, argv **C._const_char_t) []string {
r := make([]string, 0, argc)
for _, s := range unsafe.Slice(argv, argc) {
r = append(r, C.GoString(s))
}
return r
}
// handlePamCall is the function that translates C pam requests to Go.
func handlePamCall(pamh *C.pam_handle_t, flags C.int, argc C.int,
argv **C._const_char_t, moduleFunc pam.ModuleHandlerFunc) C.int {
if pamModuleHandler == nil {
return C.int(pam.ErrNoModuleData)
}
if moduleFunc == nil {
return C.int(pam.ErrIgnore)
}
mt := pam.NewModuleTransactionInvokerParallelConv(pam.NativeHandle(pamh))
err := mt.InvokeHandler(moduleFunc, pam.Flags(flags),
sliceFromArgv(argc, argv))
if err == nil {
return 0
}
if (pam.Flags(flags)&pam.Silent) == 0 && !errors.Is(err, pam.ErrIgnore) {
fmt.Fprintf(os.Stderr, "module returned error: %v\n", err)
}
var pamErr pam.Error
if errors.As(err, &pamErr) {
return C.int(pamErr)
}
return C.int(pam.ErrSystem)
}
//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.Authenticate)
}
//export pam_sm_setcred
func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.SetCred)
}
//export pam_sm_acct_mgmt
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.AcctMgmt)
}
//export pam_sm_open_session
func pam_sm_open_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.OpenSession)
}
//export pam_sm_close_session
func pam_sm_close_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.CloseSession)
}
//export pam_sm_chauthtok
func pam_sm_chauthtok(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.ChangeAuthTok)
}
func main() {}

View File

@@ -0,0 +1,67 @@
package main
import (
"encoding/gob"
"github.com/msteinert/pam/v2"
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
)
// SerializablePamError represents a [pam.Error] in a
// serializable way that splits message and return code.
type SerializablePamError struct {
Msg string
RetStatus pam.Error
}
// NewSerializablePamError initializes a SerializablePamError from
// the default status error message.
func NewSerializablePamError(status pam.Error) SerializablePamError {
return SerializablePamError{Msg: status.Error(), RetStatus: status}
}
func (e *SerializablePamError) Error() string {
return e.RetStatus.Error()
}
// SerializableStringConvRequest is a serializable string request.
type SerializableStringConvRequest struct {
Style pam.Style
Request string
}
// SerializableStringConvResponse is a serializable string response.
type SerializableStringConvResponse struct {
Style pam.Style
Response string
}
// SerializableBinaryConvRequest is a serializable binary request.
type SerializableBinaryConvRequest struct {
Request []byte
}
// SerializableBinaryConvResponse is a serializable binary response.
type SerializableBinaryConvResponse struct {
Response []byte
}
func init() {
gob.Register(map[string]string{})
gob.Register(Request{})
gob.Register(pam.Item(0))
gob.Register(pam.Error(0))
gob.Register(pam.Style(0))
gob.Register([]pam.ConvResponse{})
gob.RegisterName("main.SerializablePamError",
SerializablePamError{})
gob.RegisterName("main.SerializableStringConvRequest",
SerializableStringConvRequest{})
gob.RegisterName("main.SerializableStringConvResponse",
SerializableStringConvResponse{})
gob.RegisterName("main.SerializableBinaryConvRequest",
SerializableBinaryConvRequest{})
gob.RegisterName("main.SerializableBinaryConvResponse",
SerializableBinaryConvResponse{})
gob.Register(utils.SerializableError{})
}

View File

@@ -0,0 +1,38 @@
package utils
import "github.com/msteinert/pam/v2"
// BaseModule is the type for a base PAM module.
type BaseModule struct{}
// AcctMgmt is the handler function for PAM AcctMgmt.
func (h *BaseModule) AcctMgmt(pam.ModuleTransaction, pam.Flags, []string) error {
return nil
}
// Authenticate is the handler function for PAM Authenticate.
func (h *BaseModule) Authenticate(pam.ModuleTransaction, pam.Flags, []string) error {
return nil
}
// ChangeAuthTok is the handler function for PAM ChangeAuthTok.
func (h *BaseModule) ChangeAuthTok(pam.ModuleTransaction, pam.Flags, []string) error {
return nil
}
// OpenSession is the handler function for PAM OpenSession.
func (h *BaseModule) OpenSession(pam.ModuleTransaction, pam.Flags, []string) error {
return nil
}
// CloseSession is the handler function for PAM CloseSession.
func (h *BaseModule) CloseSession(pam.ModuleTransaction, pam.Flags, []string) error {
return nil
}
// SetCred is the handler function for PAM SetCred.
func (h *BaseModule) SetCred(pam.ModuleTransaction, pam.Flags, []string) error {
return nil
}
var _ pam.ModuleHandler = &BaseModule{}

View File

@@ -0,0 +1,35 @@
package utils
import (
"testing"
"github.com/msteinert/pam/v2"
)
func TestMain(t *testing.T) {
bm := BaseModule{}
if bm.AcctMgmt(nil, pam.Flags(0), nil) != nil {
t.Fatalf("Unexpected non-nil value")
}
if bm.Authenticate(nil, pam.Flags(0), nil) != nil {
t.Fatalf("Unexpected non-nil value")
}
if bm.ChangeAuthTok(nil, pam.Flags(0), nil) != nil {
t.Fatalf("Unexpected non-nil value")
}
if bm.OpenSession(nil, pam.Flags(0), nil) != nil {
t.Fatalf("Unexpected non-nil value")
}
if bm.CloseSession(nil, pam.Flags(0), nil) != nil {
t.Fatalf("Unexpected non-nil value")
}
if bm.SetCred(nil, pam.Flags(0), nil) != nil {
t.Fatalf("Unexpected non-nil value")
}
}

View File

@@ -0,0 +1,135 @@
// Package utils contains the internal test utils
package utils
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/msteinert/pam/v2"
)
// TestSetup is an utility type for having a playground for test PAM modules.
type TestSetup struct {
t *testing.T
workDir string
}
type withWorkDir struct{}
//nolint:revive
func WithWorkDir() withWorkDir {
return withWorkDir{}
}
// NewTestSetup creates a new TestSetup.
func NewTestSetup(t *testing.T, args ...interface{}) *TestSetup {
t.Helper()
ts := &TestSetup{t: t}
for _, arg := range args {
switch argType := arg.(type) {
case withWorkDir:
ts.ensureWorkDir()
default:
t.Fatalf("Unknown parameter of type %v", argType)
}
}
return ts
}
// CreateTemporaryDir creates a temporary directory with provided basename.
func (ts *TestSetup) CreateTemporaryDir(basename string) string {
tmpDir, err := os.MkdirTemp(os.TempDir(), basename)
if err != nil {
ts.t.Fatalf("can't create service path %v", err)
}
ts.t.Cleanup(func() { os.RemoveAll(tmpDir) })
return tmpDir
}
func (ts *TestSetup) ensureWorkDir() string {
if ts.workDir != "" {
return ts.workDir
}
ts.workDir = ts.CreateTemporaryDir("go-pam-*")
return ts.workDir
}
// WorkDir returns the test setup work directory.
func (ts TestSetup) WorkDir() string {
return ts.workDir
}
// GenerateModule generates a PAM module for the provided path and name.
func (ts *TestSetup) GenerateModule(testModulePath string, moduleName string) string {
cmd := exec.Command("go", "generate", "-C", testModulePath)
out, err := cmd.CombinedOutput()
if err != nil {
ts.t.Fatalf("can't build pam module %v: %s", err, out)
}
builtFile := filepath.Join(cmd.Dir, testModulePath, moduleName)
modulePath := filepath.Join(ts.ensureWorkDir(), filepath.Base(builtFile))
if err = os.Rename(builtFile, modulePath); err != nil {
ts.t.Fatalf("can't move module: %v", err)
os.Remove(builtFile)
}
return modulePath
}
func (ts TestSetup) currentFile(skip int) string {
_, currentFile, _, ok := runtime.Caller(skip)
if !ok {
ts.t.Fatalf("can't get current binary path")
}
return currentFile
}
// GetCurrentFile returns the current file path.
func (ts TestSetup) GetCurrentFile() string {
// This is a library so we care about the caller location
return ts.currentFile(2)
}
// GetCurrentFileDir returns the current file directory.
func (ts TestSetup) GetCurrentFileDir() string {
return filepath.Dir(ts.currentFile(2))
}
// GenerateModuleDefault generates a default module.
func (ts *TestSetup) GenerateModuleDefault(testModulePath string) string {
return ts.GenerateModule(testModulePath, "pam_go.so")
}
// CreateService creates a service file.
func (ts *TestSetup) CreateService(serviceName string, services []ServiceLine) string {
if !pam.CheckPamHasStartConfdir() {
ts.t.Skip("PAM has no support for custom service paths")
return ""
}
serviceName = strings.ToLower(serviceName)
serviceFile := filepath.Join(ts.ensureWorkDir(), serviceName)
var contents = []string{}
for _, s := range services {
contents = append(contents, strings.TrimRight(strings.Join([]string{
s.Action.String(), s.Control.String(), s.Module, strings.Join(s.Args, " "),
}, "\t"), "\t"))
}
if err := os.WriteFile(serviceFile,
[]byte(strings.Join(contents, "\n")), 0600); err != nil {
ts.t.Fatalf("can't create service file %v: %v", serviceFile, err)
}
return serviceFile
}

View File

@@ -0,0 +1,180 @@
package utils
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"strings"
"testing"
)
func isDir(t *testing.T, path string) bool {
t.Helper()
if file, err := os.Open(path); err == nil {
if fileInfo, err := file.Stat(); err == nil {
return fileInfo.IsDir()
}
t.Fatalf("error: %v", err)
} else {
t.Fatalf("error: %v", err)
}
return false
}
func Test_CreateTemporaryDir(t *testing.T) {
t.Parallel()
ts := NewTestSetup(t)
dir := ts.CreateTemporaryDir("")
if !isDir(t, dir) {
t.Fatalf("%s not a directory", dir)
}
dir = ts.CreateTemporaryDir("foo-prefix-*")
if !isDir(t, dir) {
t.Fatalf("%s not a directory", dir)
}
}
func Test_TestSetupWithWorkDir(t *testing.T) {
t.Parallel()
ts := NewTestSetup(t, WithWorkDir())
if !isDir(t, ts.WorkDir()) {
t.Fatalf("%s not a directory", ts.WorkDir())
}
}
func Test_CreateService(t *testing.T) {
t.Parallel()
ts := NewTestSetup(t)
tests := map[string]struct {
services []ServiceLine
expectedContent string
}{
"empty": {},
"CApital-Empty": {},
"auth-sufficient-permit": {
services: []ServiceLine{
{Auth, Sufficient, Permit.String(), []string{}},
},
expectedContent: "auth sufficient pam_permit.so",
},
"auth-sufficient-permit-args": {
services: []ServiceLine{
{Auth, Required, Deny.String(), []string{"a b c [d e]"}},
},
expectedContent: "auth required pam_deny.so a b c [d e]",
},
"complete-custom": {
services: []ServiceLine{
{Account, Required, "pam_account_module.so", []string{"a", "b", "c", "[d e]"}},
{Account, Required, Deny.String(), []string{}},
{Auth, Requisite, "pam_auth_module.so", []string{}},
{Auth, Requisite, Deny.String(), []string{}},
{Password, Sufficient, "pam_password_module.so", []string{"arg"}},
{Password, Sufficient, Deny.String(), []string{}},
{Session, Optional, "pam_session_module.so", []string{""}},
{Session, Optional, Deny.String(), []string{}},
},
expectedContent: `account required pam_account_module.so a b c [d e]
account required pam_deny.so
auth requisite pam_auth_module.so
auth requisite pam_deny.so
password sufficient pam_password_module.so arg
password sufficient pam_deny.so
session optional pam_session_module.so
session optional pam_deny.so`,
},
}
for name, tc := range tests {
tc := tc
name := name
t.Run(name, func(t *testing.T) {
t.Parallel()
service := ts.CreateService(name, tc.services)
if filepath.Base(service) != strings.ToLower(name) {
t.Fatalf("Invalid service name %s", service)
}
if bytes, err := os.ReadFile(service); err != nil {
t.Fatalf("Failed reading %s: %v", service, err)
} else {
if string(bytes) != tc.expectedContent {
t.Fatalf("Unexpected file content:\n%s\n---\n%s",
tc.expectedContent, string(bytes))
}
}
})
}
}
func Test_GenerateModule(t *testing.T) {
ts := NewTestSetup(t)
dir := ts.CreateTemporaryDir("")
if !isDir(t, dir) {
t.Fatalf("%s not a directory", dir)
}
f, err := os.Create(filepath.Join(dir, "test-generate.go"))
if err != nil {
t.Fatalf("can't create file %v", err)
}
defer f.Close()
randomName := ""
for i := 0; i < 10; i++ {
// #nosec:G404 - it's a test, we don't care.
randomName += string(byte('a' + rand.Intn('z'-'a')))
}
wantFile := randomName + ".so"
fmt.Fprintf(f, `//go:generate touch %s
package generate_file
`, wantFile)
mod, err := os.Create(filepath.Join(dir, "go.mod"))
if err != nil {
t.Fatalf("can't create file %v", err)
}
defer mod.Close()
fmt.Fprintf(mod, `module example.com/greetings
go 1.20
`)
fakeModule := ts.GenerateModule(dir, wantFile)
if _, err := os.Stat(fakeModule); err != nil {
t.Fatalf("module not generated %v", err)
}
fmt.Fprint(f, `//go:generate touch pam_go.so
package generate_file
`, wantFile)
fakeModule = ts.GenerateModuleDefault(dir)
if _, err := os.Stat(fakeModule); err != nil {
t.Fatalf("module not generated %v", err)
}
}
func Test_GetCurrentFileDir(t *testing.T) {
t.Parallel()
ts := NewTestSetup(t)
if !strings.HasSuffix(ts.GetCurrentFileDir(), filepath.Join("internal", "utils")) {
t.Fatalf("unexpected file %v", ts.GetCurrentFileDir())
}
}
func Test_GetCurrentFile(t *testing.T) {
t.Parallel()
ts := NewTestSetup(t)
if !strings.HasSuffix(ts.GetCurrentFile(), filepath.Join("utils", "test-setup_test.go")) {
t.Fatalf("unexpected file %v", ts.GetCurrentFile())
}
}

View File

@@ -0,0 +1,263 @@
// Package utils contains the internal test utils
package utils
//#include <stdint.h>
import "C"
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"reflect"
"unsafe"
"github.com/msteinert/pam/v2"
)
// Action represents a PAM action to perform.
type Action int
const (
// Account is the account.
Account Action = iota + 1
// Auth is the auth.
Auth
// Password is the password.
Password
// Session is the session.
Session
)
func (a Action) String() string {
switch a {
case Account:
return "account"
case Auth:
return "auth"
case Password:
return "password"
case Session:
return "session"
default:
return ""
}
}
// Actions is a map with all the available Actions by their name.
var Actions = map[string]Action{
Account.String(): Account,
Auth.String(): Auth,
Password.String(): Password,
Session.String(): Session,
}
// Control represents how a PAM module should controlled in PAM service file.
type Control int
const (
// Required implies that the module is required.
Required Control = iota + 1
// Requisite implies that the module is requisite.
Requisite
// Sufficient implies that the module is sufficient.
Sufficient
// Optional implies that the module is optional.
Optional
)
func (c Control) String() string {
switch c {
case Required:
return "required"
case Requisite:
return "requisite"
case Sufficient:
return "sufficient"
case Optional:
return "optional"
default:
return ""
}
}
// ServiceLine is the representation of a PAM module service file line.
type ServiceLine struct {
Action Action
Control Control
Module string
Args []string
}
// FallBackModule is a type to represent the module that should be used as fallback.
type FallBackModule int
const (
// NoFallback add no fallback module.
NoFallback FallBackModule = iota + 1
// Permit uses a module that always permits.
Permit
// Deny uses a module that always denys.
Deny
)
func (a FallBackModule) String() string {
switch a {
case Permit:
return "pam_permit.so"
case Deny:
return "pam_deny.so"
default:
return ""
}
}
// SerializableError is a representation of an error in a way can be serialized.
type SerializableError struct {
Msg string
}
func (e *SerializableError) Error() string {
return e.Msg
}
// Credentials is a test [pam.ConversationHandler] implementation.
type Credentials struct {
User string
Password string
EchoOn string
EchoOff string
TextInfo string
ErrorMsg string
ExpectedMessage string
CheckEmptyMessage bool
ExpectedStyle pam.Style
CheckZeroStyle bool
Context interface{}
}
// RespondPAM handles PAM string conversations.
func (c Credentials) RespondPAM(s pam.Style, msg string) (string, error) {
if (c.ExpectedMessage != "" || c.CheckEmptyMessage) &&
msg != c.ExpectedMessage {
return "", errors.Join(pam.ErrConv,
&SerializableError{
fmt.Sprintf("unexpected prompt: %s vs %s", msg, c.ExpectedMessage),
})
}
if (c.ExpectedStyle != 0 || c.CheckZeroStyle) &&
s != c.ExpectedStyle {
return "", errors.Join(pam.ErrConv,
&SerializableError{
fmt.Sprintf("unexpected style: %#v vs %#v", s, c.ExpectedStyle),
})
}
switch s {
case pam.PromptEchoOn:
if c.User != "" {
return c.User, nil
}
return c.EchoOn, nil
case pam.PromptEchoOff:
if c.Password != "" {
return c.Password, nil
}
return c.EchoOff, nil
case pam.TextInfo:
return c.TextInfo, nil
case pam.ErrorMsg:
return c.ErrorMsg, nil
}
return "", errors.Join(pam.ErrConv,
&SerializableError{fmt.Sprintf("unhandled style: %v", s)})
}
// BinaryTransaction represents a binary PAM transaction handler struct.
type BinaryTransaction struct {
data []byte
ExpectedNull bool
ReturnedData []byte
}
// TestBinaryDataEncoder encodes a test binary data.
func TestBinaryDataEncoder(bytes []byte) []byte {
if len(bytes) > 0xff {
panic("Binary transaction size not supported")
}
if bytes == nil {
return bytes
}
data := make([]byte, 0, len(bytes)+1)
data = append(data, byte(len(bytes)))
data = append(data, bytes...)
return data
}
// TestBinaryDataDecoder decodes a test binary data.
func TestBinaryDataDecoder(ptr pam.BinaryPointer) ([]byte, error) {
if ptr == nil {
return nil, nil
}
length := uint8(*((*C.uint8_t)(ptr)))
if length == 0 {
return []byte{}, nil
}
return C.GoBytes(unsafe.Pointer(ptr), C.int(length+1))[1:], nil
}
// NewBinaryTransactionWithData creates a new [pam.BinaryTransaction] from bytes.
func NewBinaryTransactionWithData(data []byte, retData []byte) BinaryTransaction {
t := BinaryTransaction{ReturnedData: retData}
t.data = TestBinaryDataEncoder(data)
t.ExpectedNull = data == nil
return t
}
// NewBinaryTransactionWithRandomData creates a new [pam.BinaryTransaction] with random data.
func NewBinaryTransactionWithRandomData(size uint8, retData []byte) BinaryTransaction {
t := BinaryTransaction{ReturnedData: retData}
randomData := make([]byte, size)
if err := binary.Read(rand.Reader, binary.LittleEndian, &randomData); err != nil {
panic(err)
}
t.data = TestBinaryDataEncoder(randomData)
return t
}
// Data returns the bytes of the transaction.
func (b BinaryTransaction) Data() []byte {
return b.data
}
// RespondPAM (not) handles the PAM string conversations.
func (b BinaryTransaction) RespondPAM(s pam.Style, msg string) (string, error) {
return "", errors.Join(pam.ErrConv,
&SerializableError{"unexpected non-binary request"})
}
// RespondPAMBinary handles the PAM binary conversations.
func (b BinaryTransaction) RespondPAMBinary(ptr pam.BinaryPointer) ([]byte, error) {
if ptr == nil && !b.ExpectedNull {
return nil, errors.Join(pam.ErrConv,
&SerializableError{"unexpected null binary data"})
} else if ptr == nil {
return TestBinaryDataEncoder(b.ReturnedData), nil
}
bytes, _ := TestBinaryDataDecoder(ptr)
if !reflect.DeepEqual(bytes, b.data[1:]) {
return nil, errors.Join(pam.ErrConv,
&SerializableError{
fmt.Sprintf("data mismatch %#v vs %#v", bytes, b.data[1:]),
})
}
return TestBinaryDataEncoder(b.ReturnedData), nil
}

94
errors.go Normal file
View File

@@ -0,0 +1,94 @@
package pam
/*
#include <security/pam_appl.h>
*/
import "C"
// Error is the Type for PAM Return types
type Error int
// Pam Return types
const (
// OpenErr indicates a dlopen() failure when dynamically loading a
// service module.
ErrOpen Error = C.PAM_OPEN_ERR
// ErrSymbol indicates a symbol not found.
ErrSymbol Error = C.PAM_SYMBOL_ERR
// ErrService indicates a error in service module.
ErrService Error = C.PAM_SERVICE_ERR
// ErrSystem indicates a system error.
ErrSystem Error = C.PAM_SYSTEM_ERR
// ErrBuf indicates a memory buffer error.
ErrBuf Error = C.PAM_BUF_ERR
// ErrPermDenied indicates a permission denied.
ErrPermDenied Error = C.PAM_PERM_DENIED
// ErrAuth indicates a authentication failure.
ErrAuth Error = C.PAM_AUTH_ERR
// ErrCredInsufficient indicates a can not access authentication data due to
// insufficient credentials.
ErrCredInsufficient Error = C.PAM_CRED_INSUFFICIENT
// ErrAuthinfoUnavail indicates that the underlying authentication service
// can not retrieve authentication information.
ErrAuthinfoUnavail Error = C.PAM_AUTHINFO_UNAVAIL
// ErrUserUnknown indicates a user not known to the underlying authentication
// module.
ErrUserUnknown Error = C.PAM_USER_UNKNOWN
// ErrMaxtries indicates that an authentication service has maintained a retry
// count which has been reached. No further retries should be attempted.
ErrMaxtries Error = C.PAM_MAXTRIES
// ErrNewAuthtokReqd indicates a new authentication token required. This is
// normally returned if the machine security policies require that the
// password should be changed because the password is nil or it has aged.
ErrNewAuthtokReqd Error = C.PAM_NEW_AUTHTOK_REQD
// ErrAcctExpired indicates that an user account has expired.
ErrAcctExpired Error = C.PAM_ACCT_EXPIRED
// ErrSession indicates a can not make/remove an entry for the
// specified session.
ErrSession Error = C.PAM_SESSION_ERR
// ErrCredUnavail indicates that an underlying authentication service can not
// retrieve user credentials.
ErrCredUnavail Error = C.PAM_CRED_UNAVAIL
// ErrCredExpired indicates that an user credentials expired.
ErrCredExpired Error = C.PAM_CRED_EXPIRED
// ErrCred indicates a failure setting user credentials.
ErrCred Error = C.PAM_CRED_ERR
// ErrNoModuleData indicates a no module specific data is present.
ErrNoModuleData Error = C.PAM_NO_MODULE_DATA
// ErrConv indicates a conversation error.
ErrConv Error = C.PAM_CONV_ERR
// ErrAuthtokErr indicates an authentication token manipulation error.
ErrAuthtok Error = C.PAM_AUTHTOK_ERR
// ErrAuthtokRecoveryErr indicates an authentication information cannot
// be recovered.
ErrAuthtokRecovery Error = C.PAM_AUTHTOK_RECOVERY_ERR
// ErrAuthtokLockBusy indicates am authentication token lock busy.
ErrAuthtokLockBusy Error = C.PAM_AUTHTOK_LOCK_BUSY
// ErrAuthtokDisableAging indicates an authentication token aging disabled.
ErrAuthtokDisableAging Error = C.PAM_AUTHTOK_DISABLE_AGING
// ErrTryAgain indicates a preliminary check by password service.
ErrTryAgain Error = C.PAM_TRY_AGAIN
// ErrIgnore indicates to ignore underlying account module regardless of
// whether the control flag is required, optional, or sufficient.
ErrIgnore Error = C.PAM_IGNORE
// ErrAbort indicates a critical error (module fail now request).
ErrAbort Error = C.PAM_ABORT
// ErrAuthtokExpired indicates an user's authentication token has expired.
ErrAuthtokExpired Error = C.PAM_AUTHTOK_EXPIRED
// ErrModuleUnknown indicates a module is not known.
ErrModuleUnknown Error = C.PAM_MODULE_UNKNOWN
// ErrBadItem indicates a bad item passed to pam_*_item().
ErrBadItem Error = C.PAM_BAD_ITEM
// ErrConvAgain indicates a conversation function is event driven and data
// is not available yet.
ErrConvAgain Error = C.PAM_CONV_AGAIN
// ErrIncomplete indicates to please call this function again to complete
// authentication stack. Before calling again, verify that conversation
// is completed.
ErrIncomplete Error = C.PAM_INCOMPLETE
)
// Error returns the error message for the given status.
func (status Error) Error() string {
return C.GoString(C.pam_strerror(nil, C.int(status)))
}

50
example-module/module.go Normal file
View File

@@ -0,0 +1,50 @@
// These go:generate directive allow to generate the module by just using
// `go generate` once in the module directory.
// This is not strictly needed
//go:generate go run github.com/msteinert/pam/v2/cmd/pam-moduler
//go:generate go generate --skip="pam_module.go"
// Package main provides the module shared library.
package main
import (
"fmt"
"github.com/msteinert/pam/v2"
)
type exampleHandler struct{}
var pamModuleHandler pam.ModuleHandler = &exampleHandler{}
var _ = pamModuleHandler
// AcctMgmt is the module handle function for account management.
func (h *exampleHandler) AcctMgmt(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return fmt.Errorf("AcctMgmt not implemented: %w", pam.ErrIgnore)
}
// Authenticate is the module handle function for authentication.
func (h *exampleHandler) Authenticate(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return pam.ErrAuthinfoUnavail
}
// ChangeAuthTok is the module handle function for changing authentication token.
func (h *exampleHandler) ChangeAuthTok(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return fmt.Errorf("ChangeAuthTok not implemented: %w", pam.ErrIgnore)
}
// OpenSession is the module handle function for open session.
func (h *exampleHandler) OpenSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return fmt.Errorf("OpenSession not implemented: %w", pam.ErrIgnore)
}
// CloseSession is the module handle function for close session.
func (h *exampleHandler) CloseSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return fmt.Errorf("CloseSession not implemented: %w", pam.ErrIgnore)
}
// SetCred is the module handle function for set credentials.
func (h *exampleHandler) SetCred(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
return fmt.Errorf("SetCred not implemented: %w", pam.ErrIgnore)
}

View File

@@ -0,0 +1,96 @@
// Code generated by "pam-moduler "; DO NOT EDIT.
//go:generate go build "-ldflags=-extldflags -Wl,-soname,pam_go.so" -buildmode=c-shared -o pam_go.so -tags go_pam_module
// Package main is the package for the PAM module library.
package main
/*
#cgo LDFLAGS: -lpam -fPIC
#include <security/pam_modules.h>
typedef const char _const_char_t;
*/
import "C"
import (
"errors"
"fmt"
"github.com/msteinert/pam/v2"
"os"
"unsafe"
)
// Do a typecheck at compile time
var _ pam.ModuleHandler = pamModuleHandler
// sliceFromArgv returns a slice of strings given to the PAM module.
func sliceFromArgv(argc C.int, argv **C._const_char_t) []string {
r := make([]string, 0, argc)
for _, s := range unsafe.Slice(argv, argc) {
r = append(r, C.GoString(s))
}
return r
}
// handlePamCall is the function that translates C pam requests to Go.
func handlePamCall(pamh *C.pam_handle_t, flags C.int, argc C.int,
argv **C._const_char_t, moduleFunc pam.ModuleHandlerFunc) C.int {
if pamModuleHandler == nil {
return C.int(pam.ErrNoModuleData)
}
if moduleFunc == nil {
return C.int(pam.ErrIgnore)
}
mt := pam.NewModuleTransactionInvoker(pam.NativeHandle(pamh))
err := mt.InvokeHandler(moduleFunc, pam.Flags(flags),
sliceFromArgv(argc, argv))
if err == nil {
return 0
}
if (pam.Flags(flags)&pam.Silent) == 0 && !errors.Is(err, pam.ErrIgnore) {
fmt.Fprintf(os.Stderr, "module returned error: %v\n", err)
}
var pamErr pam.Error
if errors.As(err, &pamErr) {
return C.int(pamErr)
}
return C.int(pam.ErrSystem)
}
//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.Authenticate)
}
//export pam_sm_setcred
func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.SetCred)
}
//export pam_sm_acct_mgmt
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.AcctMgmt)
}
//export pam_sm_open_session
func pam_sm_open_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.OpenSession)
}
//export pam_sm_close_session
func pam_sm_close_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.CloseSession)
}
//export pam_sm_chauthtok
func pam_sm_chauthtok(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.ChangeAuthTok)
}
func main() {}

View File

@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/msteinert/pam" "github.com/msteinert/pam/v2"
"golang.org/x/term" "golang.org/x/term"
) )
@@ -14,7 +14,7 @@ import (
// should cause PAM to ask its conversation handler for a username and password // should cause PAM to ask its conversation handler for a username and password
// in sequence. // in sequence.
func Example() { func Example() {
t, err := pam.StartFunc("", "", func(s pam.Style, msg string) (string, error) { t, err := pam.StartFunc("passwd", "", func(s pam.Style, msg string) (string, error) {
switch s { switch s {
case pam.PromptEchoOff: case pam.PromptEchoOff:
fmt.Print(msg) fmt.Print(msg)
@@ -40,12 +40,19 @@ func Example() {
} }
}) })
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "start: %s\n", err.Error()) fmt.Fprintf(os.Stderr, "start: %v\n", err)
os.Exit(1) os.Exit(1)
} }
defer func() {
err := t.End()
if err != nil {
fmt.Fprintf(os.Stderr, "end: %v\n", err)
os.Exit(1)
}
}()
err = t.Authenticate(0) err = t.Authenticate(0)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "authenticate: %s\n", err.Error()) fmt.Fprintf(os.Stderr, "authenticate: %v\n", err)
os.Exit(1) os.Exit(1)
} }
fmt.Println("authentication succeeded!") fmt.Println("authentication succeeded!")

2
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/msteinert/pam module github.com/msteinert/pam/v2
go 1.20 go 1.20

233
module-transaction-mock.go Normal file
View File

@@ -0,0 +1,233 @@
//go:build !go_pam_module
package pam
/*
#cgo CFLAGS: -Wall -std=c99
#include <stdint.h>
#include <stdlib.h>
#include <security/pam_modules.h>
void init_pam_conv(struct pam_conv *conv, uintptr_t appdata);
*/
import "C"
import (
"errors"
"fmt"
"reflect"
"runtime"
"runtime/cgo"
"testing"
"unsafe"
)
type mockModuleTransactionExpectations struct {
UserPrompt string
DataKey string
}
type mockModuleTransactionReturnedData struct {
User string
InteractiveUser bool
Status Error
}
type mockModuleTransaction struct {
moduleTransaction
T *testing.T
Expectations mockModuleTransactionExpectations
RetData mockModuleTransactionReturnedData
ConversationHandler ConversationHandler
moduleData map[string]uintptr
allocatedData []unsafe.Pointer
binaryProtocol bool
}
func newMockModuleTransaction(m *mockModuleTransaction) *mockModuleTransaction {
m.moduleData = make(map[string]uintptr)
m.binaryProtocol = true
runtime.SetFinalizer(m, func(m *mockModuleTransaction) {
for _, ptr := range m.allocatedData {
C.free(ptr)
}
for _, handle := range m.moduleData {
_go_pam_data_cleanup(nil, C.uintptr_t(handle), C.PAM_DATA_SILENT)
}
})
return m
}
func (m *mockModuleTransaction) getUser(outUser **C.char, prompt *C.char) C.int {
goPrompt := C.GoString(prompt)
if goPrompt != m.Expectations.UserPrompt {
m.T.Fatalf("unexpected prompt: %s vs %s", goPrompt, m.Expectations.UserPrompt)
return C.int(ErrAbort)
}
user := m.RetData.User
if m.RetData.InteractiveUser || (m.RetData.User == "" && m.ConversationHandler != nil) {
if m.ConversationHandler == nil {
m.T.Fatalf("no conversation handler provided")
}
u, err := m.ConversationHandler.RespondPAM(PromptEchoOn, goPrompt)
user = u
if err != nil {
var pamErr Error
if errors.As(err, &pamErr) {
return C.int(pamErr)
}
return C.int(ErrAbort)
}
}
cUser := C.CString(user)
m.allocatedData = append(m.allocatedData, unsafe.Pointer(cUser))
*outUser = cUser
return C.int(m.RetData.Status)
}
func (m *mockModuleTransaction) getData(key *C.char, outHandle *C.uintptr_t) C.int {
goKey := C.GoString(key)
if m.Expectations.DataKey != "" && goKey != m.Expectations.DataKey {
m.T.Fatalf("data key mismatch: %#v vs %#v", goKey, m.Expectations.DataKey)
}
if handle, ok := m.moduleData[goKey]; ok {
*outHandle = C.uintptr_t(handle)
} else {
*outHandle = 0
}
return C.int(m.RetData.Status)
}
func (m *mockModuleTransaction) setData(key *C.char, handle C.uintptr_t) C.int {
goKey := C.GoString(key)
if m.Expectations.DataKey != "" && goKey != m.Expectations.DataKey {
m.T.Fatalf("data key mismatch: %#v vs %#v", goKey, m.Expectations.DataKey)
}
if oldHandle, ok := m.moduleData[goKey]; ok {
_go_pam_data_cleanup(nil, C.uintptr_t(oldHandle), C.PAM_DATA_REPLACE)
}
if handle != 0 {
m.moduleData[goKey] = uintptr(handle)
}
return C.int(m.RetData.Status)
}
func (m *mockModuleTransaction) getConv() (*C.struct_pam_conv, error) {
if m.ConversationHandler != nil {
conv := C.struct_pam_conv{}
handler := cgo.NewHandle(m.ConversationHandler)
C.init_pam_conv(&conv, C.uintptr_t(handler))
return &conv, nil
}
if C.int(m.RetData.Status) != success {
return nil, m.RetData.Status
}
return nil, nil
}
func (m *mockModuleTransaction) hasBinaryProtocol() bool {
return m.binaryProtocol
}
type mockConversationHandler struct {
User string
PromptEchoOn string
PromptEchoOff string
TextInfo string
ErrorMsg string
Binary []byte
ExpectedMessage string
ExpectedMessagesByStyle map[Style]string
ExpectedNil bool
ExpectedBinary []byte
CheckEmptyMessage bool
ExpectedStyle Style
CheckZeroStyle bool
IgnoreUnknownStyle bool
}
func (c mockConversationHandler) RespondPAM(s Style, msg string) (string, error) {
var expectedMsg = c.ExpectedMessage
if msg, ok := c.ExpectedMessagesByStyle[s]; ok {
expectedMsg = msg
}
if (expectedMsg != "" || c.CheckEmptyMessage) &&
msg != expectedMsg {
return "", fmt.Errorf("%w: unexpected prompt: %s vs %s",
ErrConv, msg, c.ExpectedMessage)
}
if (c.ExpectedStyle != 0 || c.CheckZeroStyle) &&
s != c.ExpectedStyle {
return "", fmt.Errorf("%w: unexpected style: %#v vs %#v",
ErrConv, s, c.ExpectedStyle)
}
switch s {
case PromptEchoOn:
if c.User != "" {
return c.User, nil
}
return c.PromptEchoOn, nil
case PromptEchoOff:
return c.PromptEchoOff, nil
case TextInfo:
return c.TextInfo, nil
case ErrorMsg:
return c.ErrorMsg, nil
}
if c.IgnoreUnknownStyle {
return c.ExpectedMessage, nil
}
return "", fmt.Errorf("%w: unhandled style: %v", ErrConv, s)
}
func testBinaryDataEncoder(bytes []byte) []byte {
if len(bytes) > 0xff {
panic("Binary transaction size not supported")
}
if bytes == nil {
return bytes
}
data := make([]byte, 0, len(bytes)+1)
data = append(data, byte(len(bytes)))
data = append(data, bytes...)
return data
}
func testBinaryDataDecoder(ptr BinaryPointer) ([]byte, error) {
if ptr == nil {
return nil, nil
}
length := uint8(*((*C.uint8_t)(ptr)))
if length == 0 {
return []byte{}, nil
}
return C.GoBytes(unsafe.Pointer(ptr), C.int(length+1))[1:], nil
}
func (c mockConversationHandler) RespondPAMBinary(ptr BinaryPointer) ([]byte, error) {
if ptr == nil && !c.ExpectedNil {
return nil, fmt.Errorf("%w: unexpected null binary data", ErrConv)
} else if ptr == nil {
return testBinaryDataEncoder(c.Binary), nil
}
bytes, _ := testBinaryDataDecoder(ptr)
if !reflect.DeepEqual(bytes, c.ExpectedBinary) {
return nil, fmt.Errorf("%w: data mismatch %#v vs %#v",
ErrConv, bytes, c.ExpectedBinary)
}
return testBinaryDataEncoder(c.Binary), nil
}

627
module-transaction.go Normal file
View File

@@ -0,0 +1,627 @@
// Package pam provides a wrapper for the PAM application API.
package pam
/*
#include "transaction.h"
*/
import "C"
import (
"errors"
"fmt"
"runtime"
"runtime/cgo"
"sync"
"sync/atomic"
"unsafe"
)
const maxNumMsg = C.PAM_MAX_NUM_MSG
// ModuleTransaction is an interface that a pam module transaction
// should implement.
type ModuleTransaction interface {
SetItem(Item, string) error
GetItem(Item) (string, error)
PutEnv(nameVal string) error
GetEnv(name string) string
GetEnvList() (map[string]string, error)
GetUser(prompt string) (string, error)
SetData(key string, data any) error
GetData(key string) (any, error)
StartStringConv(style Style, prompt string) (StringConvResponse, error)
StartStringConvf(style Style, format string, args ...interface{}) (
StringConvResponse, error)
StartBinaryConv([]byte) (BinaryConvResponse, error)
StartConv(ConvRequest) (ConvResponse, error)
StartConvMulti([]ConvRequest) ([]ConvResponse, error)
}
// ModuleHandlerFunc is a function type used by the ModuleHandler.
type ModuleHandlerFunc func(ModuleTransaction, Flags, []string) error
// ModuleTransaction is the module-side handle for a PAM transaction.
type moduleTransaction struct {
transactionBase
convMutex *sync.Mutex
}
// ModuleHandler is an interface for objects that can be used to create
// PAM modules from go.
type ModuleHandler interface {
AcctMgmt(ModuleTransaction, Flags, []string) error
Authenticate(ModuleTransaction, Flags, []string) error
ChangeAuthTok(ModuleTransaction, Flags, []string) error
CloseSession(ModuleTransaction, Flags, []string) error
OpenSession(ModuleTransaction, Flags, []string) error
SetCred(ModuleTransaction, Flags, []string) error
}
// ModuleTransactionInvoker is an interface that a pam module transaction
// should implement to redirect requests from C handlers to go,
type ModuleTransactionInvoker interface {
ModuleTransaction
InvokeHandler(handler ModuleHandlerFunc, flags Flags, args []string) error
}
// NewModuleTransactionParallelConv allows initializing a transaction from the
// module side. Conversations using this transaction can be multi-thread, but
// this requires the application loading the module to support this, otherwise
// we may just break their assumptions.
func NewModuleTransactionParallelConv(handle NativeHandle) ModuleTransaction {
return &moduleTransaction{transactionBase{handle: handle}, nil}
}
// NewModuleTransactionInvoker allows initializing a transaction invoker from the
// module side.
func NewModuleTransactionInvoker(handle NativeHandle) ModuleTransactionInvoker {
return &moduleTransaction{transactionBase{handle: handle}, &sync.Mutex{}}
}
// NewModuleTransactionInvokerParallelConv allows initializing a transaction invoker
// from the module side.
// Conversations using this transaction can be multi-thread, but this requires
// the application loading the module to support this, otherwise we may just
// break their assumptions.
func NewModuleTransactionInvokerParallelConv(handle NativeHandle) ModuleTransactionInvoker {
return &moduleTransaction{transactionBase{handle: handle}, nil}
}
func (m *moduleTransaction) InvokeHandler(handler ModuleHandlerFunc,
flags Flags, args []string) error {
invoker := func() error {
if handler == nil {
return ErrIgnore
}
err := handler(m, flags, args)
if err != nil {
service, _ := m.GetItem(Service)
var pamErr Error
if !errors.As(err, &pamErr) {
err = ErrSystem
}
if pamErr == ErrIgnore || service == "" {
return err
}
return fmt.Errorf("%s failed: %w", service, err)
}
return nil
}
err := invoker()
if errors.Is(err, Error(0)) {
err = nil
}
var status int32
if err != nil {
status = int32(ErrSystem)
var pamErr Error
if errors.As(err, &pamErr) {
status = int32(pamErr)
}
}
m.lastStatus.Store(status)
return err
}
type moduleTransactionIface interface {
getUser(outUser **C.char, prompt *C.char) C.int
setData(key *C.char, handle C.uintptr_t) C.int
getData(key *C.char, outHandle *C.uintptr_t) C.int
getConv() (*C.struct_pam_conv, error)
hasBinaryProtocol() bool
startConv(conv *C.struct_pam_conv, nMsg C.int,
messages **C.struct_pam_message,
outResponses **C.struct_pam_response) C.int
}
func (m *moduleTransaction) getUser(outUser **C.char, prompt *C.char) C.int {
return C.pam_get_user(m.handle, outUser, prompt)
}
// getUserImpl is the default implementation for GetUser, but kept as private so
// that can be used to test the pam package
func (m *moduleTransaction) getUserImpl(iface moduleTransactionIface,
prompt string) (string, error) {
var user *C.char
var cPrompt = C.CString(prompt)
defer C.free(unsafe.Pointer(cPrompt))
err := m.handlePamStatus(iface.getUser(&user, cPrompt))
if err != nil {
return "", err
}
return C.GoString(user), nil
}
// GetUser is similar to GetItem(User), but it would start a conversation if
// no user is currently set in PAM.
func (m *moduleTransaction) GetUser(prompt string) (string, error) {
return m.getUserImpl(m, prompt)
}
// SetData allows to save any value in the module data that is preserved
// during the whole time the module is loaded.
func (m *moduleTransaction) SetData(key string, data any) error {
return m.setDataImpl(m, key, data)
}
func (m *moduleTransaction) setData(key *C.char, handle C.uintptr_t) C.int {
return C.set_data(m.handle, key, handle)
}
// setDataImpl is the implementation for SetData for testing purposes.
func (m *moduleTransaction) setDataImpl(iface moduleTransactionIface,
key string, data any) error {
var cKey = C.CString(key)
defer C.free(unsafe.Pointer(cKey))
var handle cgo.Handle
if data != nil {
handle = cgo.NewHandle(data)
}
return m.handlePamStatus(iface.setData(cKey, C.uintptr_t(handle)))
}
//export _go_pam_data_cleanup
func _go_pam_data_cleanup(h NativeHandle, handle C.uintptr_t, status C.int) {
cgo.Handle(handle).Delete()
}
// GetData allows to get any value from the module data saved using SetData
// that is preserved across the whole time the module is loaded.
func (m *moduleTransaction) GetData(key string) (any, error) {
return m.getDataImpl(m, key)
}
func (m *moduleTransaction) getData(key *C.char, outHandle *C.uintptr_t) C.int {
return C.get_data(m.handle, key, outHandle)
}
// getDataImpl is the implementation for GetData for testing purposes.
func (m *moduleTransaction) getDataImpl(iface moduleTransactionIface,
key string) (any, error) {
var cKey = C.CString(key)
defer C.free(unsafe.Pointer(cKey))
var handle C.uintptr_t
if err := m.handlePamStatus(iface.getData(cKey, &handle)); err != nil {
return nil, err
}
if goHandle := cgo.Handle(handle); goHandle != cgo.Handle(0) {
return goHandle.Value(), nil
}
return nil, m.handlePamStatus(C.int(ErrNoModuleData))
}
// getConv is a private function to get the conversation pointer to be used
// with C.do_conv() to initiate conversations.
func (m *moduleTransaction) getConv() (*C.struct_pam_conv, error) {
var convPtr unsafe.Pointer
if err := m.handlePamStatus(
C.pam_get_item(m.handle, C.PAM_CONV, &convPtr)); err != nil {
return nil, err
}
return (*C.struct_pam_conv)(convPtr), nil
}
// ConvRequest is an interface that all the Conversation requests should
// implement.
type ConvRequest interface {
Style() Style
}
// ConvResponse is an interface that all the Conversation responses should
// implement.
type ConvResponse interface {
Style() Style
}
// StringConvRequest is a ConvRequest for performing text-based conversations.
type StringConvRequest struct {
style Style
prompt string
}
// NewStringConvRequest creates a new StringConvRequest.
func NewStringConvRequest(style Style, prompt string) StringConvRequest {
return StringConvRequest{style, prompt}
}
// Style returns the conversation style of the StringConvRequest.
func (s StringConvRequest) Style() Style {
return s.style
}
// Prompt returns the conversation style of the StringConvRequest.
func (s StringConvRequest) Prompt() string {
return s.prompt
}
// StringConvResponse is an interface that string Conversation responses implements.
type StringConvResponse interface {
ConvResponse
Response() string
}
// stringConvResponse is a StringConvResponse implementation used for text-based
// conversation responses.
type stringConvResponse struct {
style Style
response string
}
// Style returns the conversation style of the StringConvResponse.
func (s stringConvResponse) Style() Style {
return s.style
}
// Response returns the string response of the conversation.
func (s stringConvResponse) Response() string {
return s.response
}
// BinaryFinalizer is a type of function that can be used to release
// the binary when it's not required anymore
type BinaryFinalizer func(BinaryPointer)
// BinaryConvRequester is the interface that binary ConvRequests should
// implement
type BinaryConvRequester interface {
ConvRequest
Pointer() BinaryPointer
CreateResponse(BinaryPointer) BinaryConvResponse
Release()
}
// BinaryConvRequest is a ConvRequest for performing binary conversations.
type BinaryConvRequest struct {
ptr atomic.Uintptr
finalizer BinaryFinalizer
responseFinalizer BinaryFinalizer
}
// NewBinaryConvRequestFull creates a new BinaryConvRequest with finalizer
// for response BinaryResponse.
func NewBinaryConvRequestFull(ptr BinaryPointer, finalizer BinaryFinalizer,
responseFinalizer BinaryFinalizer) *BinaryConvRequest {
b := &BinaryConvRequest{finalizer: finalizer, responseFinalizer: responseFinalizer}
b.ptr.Store(uintptr(ptr))
if ptr == nil || finalizer == nil {
return b
}
// The ownership of the data here is temporary
runtime.SetFinalizer(b, func(b *BinaryConvRequest) { b.Release() })
return b
}
// NewBinaryConvRequest creates a new BinaryConvRequest
func NewBinaryConvRequest(ptr BinaryPointer, finalizer BinaryFinalizer) *BinaryConvRequest {
return NewBinaryConvRequestFull(ptr, finalizer, finalizer)
}
// NewBinaryConvRequestFromBytes creates a new BinaryConvRequest from an array
// of bytes.
func NewBinaryConvRequestFromBytes(bytes []byte) *BinaryConvRequest {
if bytes == nil {
return &BinaryConvRequest{}
}
return NewBinaryConvRequest(BinaryPointer(C.CBytes(bytes)),
func(ptr BinaryPointer) { C.free(unsafe.Pointer(ptr)) })
}
// Style returns the response style for the request, so always BinaryPrompt.
func (b *BinaryConvRequest) Style() Style {
return BinaryPrompt
}
// Pointer returns the conversation style of the StringConvRequest.
func (b *BinaryConvRequest) Pointer() BinaryPointer {
ptr := b.ptr.Load()
return *(*BinaryPointer)(unsafe.Pointer(&ptr))
}
// CreateResponse creates a new BinaryConvResponse from the request
func (b *BinaryConvRequest) CreateResponse(ptr BinaryPointer) BinaryConvResponse {
bcr := &binaryConvResponse{ptr, b.responseFinalizer, &sync.Mutex{}}
runtime.SetFinalizer(bcr, func(bcr *binaryConvResponse) {
bcr.Release()
})
return bcr
}
// Release releases the resources allocated by the request
func (b *BinaryConvRequest) Release() {
ptr := b.ptr.Swap(0)
if b.finalizer != nil {
b.finalizer(*(*BinaryPointer)(unsafe.Pointer(&ptr)))
runtime.SetFinalizer(b, nil)
}
}
// BinaryDecoder is a function type for decode the a binary pointer data into
// bytes
type BinaryDecoder func(BinaryPointer) ([]byte, error)
// BinaryConvResponse is a subtype of ConvResponse used for binary
// conversation responses.
type BinaryConvResponse interface {
ConvResponse
Data() BinaryPointer
Decode(BinaryDecoder) ([]byte, error)
Release()
}
type binaryConvResponse struct {
ptr BinaryPointer
finalizer BinaryFinalizer
mutex *sync.Mutex
}
// Style returns the response style for the response, so always BinaryPrompt.
func (b binaryConvResponse) Style() Style {
return BinaryPrompt
}
// Data returns the response native pointer, it's up to the protocol to parse
// it accordingly.
func (b *binaryConvResponse) Data() BinaryPointer {
b.mutex.Lock()
defer b.mutex.Unlock()
return b.ptr
}
// Decode decodes the binary data using the provided decoder function.
func (b *binaryConvResponse) Decode(decoder BinaryDecoder) (
[]byte, error) {
if decoder == nil {
return nil, errors.New("nil decoder provided")
}
b.mutex.Lock()
defer b.mutex.Unlock()
return decoder(b.ptr)
}
// Release releases the binary conversation response data.
// This is also automatically via a finalizer, but applications may control
// this explicitly deferring execution of this.
func (b *binaryConvResponse) Release() {
b.mutex.Lock()
defer b.mutex.Unlock()
ptr := b.ptr
b.ptr = nil
if b.finalizer != nil {
b.finalizer(ptr)
} else {
C.free(unsafe.Pointer(ptr))
}
}
// StartStringConv starts a text-based conversation using the provided style
// and prompt.
func (m *moduleTransaction) StartStringConv(style Style, prompt string) (
StringConvResponse, error) {
return m.startStringConvImpl(m, style, prompt)
}
func (m *moduleTransaction) startStringConvImpl(iface moduleTransactionIface,
style Style, prompt string) (
StringConvResponse, error) {
switch style {
case BinaryPrompt:
return nil, fmt.Errorf("%w: binary style is not supported", ErrConv)
}
res, err := m.startConvImpl(iface, NewStringConvRequest(style, prompt))
if err != nil {
return nil, err
}
stringRes, _ := res.(stringConvResponse)
return stringRes, nil
}
// StartStringConvf allows to start string conversation with formatting support.
func (m *moduleTransaction) StartStringConvf(style Style, format string, args ...interface{}) (
StringConvResponse, error) {
return m.StartStringConv(style, fmt.Sprintf(format, args...))
}
// HasBinaryProtocol checks if binary protocol is supported.
func (m *moduleTransaction) hasBinaryProtocol() bool {
return CheckPamHasBinaryProtocol()
}
// StartBinaryConv starts a binary conversation using the provided bytes.
func (m *moduleTransaction) StartBinaryConv(bytes []byte) (
BinaryConvResponse, error) {
return m.startBinaryConvImpl(m, bytes)
}
func (m *moduleTransaction) startBinaryConvImpl(iface moduleTransactionIface,
bytes []byte) (
BinaryConvResponse, error) {
res, err := m.startConvImpl(iface, NewBinaryConvRequestFromBytes(bytes))
if err != nil {
return nil, err
}
binaryRes, _ := res.(BinaryConvResponse)
return binaryRes, nil
}
// StartConv initiates a PAM conversation using the provided ConvRequest.
func (m *moduleTransaction) StartConv(req ConvRequest) (
ConvResponse, error) {
return m.startConvImpl(m, req)
}
func (m *moduleTransaction) startConvImpl(iface moduleTransactionIface, req ConvRequest) (
ConvResponse, error) {
resp, err := m.startConvMultiImpl(iface, []ConvRequest{req})
if err != nil {
return nil, err
}
if len(resp) != 1 {
return nil, fmt.Errorf("%w: not enough values returned", ErrConv)
}
return resp[0], nil
}
func (m *moduleTransaction) startConv(conv *C.struct_pam_conv, nMsg C.int,
messages **C.struct_pam_message, outResponses **C.struct_pam_response) C.int {
return C.start_pam_conv(conv, nMsg, messages, outResponses)
}
// startConvMultiImpl is the implementation for GetData for testing purposes.
func (m *moduleTransaction) startConvMultiImpl(iface moduleTransactionIface,
requests []ConvRequest) (responses []ConvResponse, err error) {
defer func() {
if err == nil {
_ = m.handlePamStatus(success)
return
}
var pamErr Error
if !errors.As(err, &pamErr) {
err = errors.Join(ErrConv, err)
pamErr = ErrConv
}
_ = m.handlePamStatus(C.int(pamErr))
}()
if len(requests) == 0 {
return nil, errors.New("no requests defined")
}
if len(requests) > maxNumMsg {
return nil, errors.New("too many requests")
}
conv, err := iface.getConv()
if err != nil {
return nil, err
}
if conv == nil || conv.conv == nil {
return nil, errors.New("impossible to find conv handler")
}
// FIXME: Just use make([]C.struct_pam_message, 0, len(requests))
// and append, when it's possible to use runtime.Pinner
var cMessagePtr *C.struct_pam_message
cMessages := (**C.struct_pam_message)(C.calloc(C.size_t(len(requests)),
(C.size_t)(unsafe.Sizeof(cMessagePtr))))
defer C.free(unsafe.Pointer(cMessages))
goMsgs := unsafe.Slice(cMessages, len(requests))
for i, req := range requests {
var cBytes unsafe.Pointer
switch r := req.(type) {
case StringConvRequest:
cBytes = unsafe.Pointer(C.CString(r.Prompt()))
defer C.free(cBytes)
case BinaryConvRequester:
if !iface.hasBinaryProtocol() {
return nil, errors.New("%w: binary protocol is not supported")
}
cBytes = unsafe.Pointer(r.Pointer())
default:
return nil, fmt.Errorf("unsupported conversation type %#v", r)
}
cMessage := (*C.struct_pam_message)(C.calloc(1,
(C.size_t)(unsafe.Sizeof(*goMsgs[i]))))
defer C.free(unsafe.Pointer(cMessage))
cMessage.msg_style = C.int(req.Style())
cMessage.msg = (*C.char)(cBytes)
goMsgs[i] = cMessage
}
if m.convMutex != nil {
m.convMutex.Lock()
defer m.convMutex.Unlock()
}
var cResponses *C.struct_pam_response
ret := iface.startConv(conv, C.int(len(requests)), cMessages, &cResponses)
if ret != success {
return nil, Error(ret)
}
goResponses := unsafe.Slice(cResponses, len(requests))
defer func() {
for i, resp := range goResponses {
if resp.resp == nil {
continue
}
switch req := requests[i].(type) {
case BinaryConvRequester:
// In the binary prompt case, we need to rely on the provided
// finalizer to release the response, so let's create a new one.
req.CreateResponse(BinaryPointer(resp.resp)).Release()
default:
C.free(unsafe.Pointer(resp.resp))
}
}
C.free(unsafe.Pointer(cResponses))
}()
responses = make([]ConvResponse, 0, len(requests))
for i, resp := range goResponses {
request := requests[i]
msgStyle := request.Style()
switch msgStyle {
case PromptEchoOff:
fallthrough
case PromptEchoOn:
fallthrough
case ErrorMsg:
fallthrough
case TextInfo:
responses = append(responses, stringConvResponse{
style: msgStyle,
response: C.GoString(resp.resp),
})
case BinaryPrompt:
// Let's steal the resp ownership here, so that the request
// finalizer won't act on it.
bcr, _ := request.(BinaryConvRequester)
resp := bcr.CreateResponse(BinaryPointer(resp.resp))
goResponses[i].resp = nil
responses = append(responses, resp)
default:
return nil,
fmt.Errorf("unsupported conversation type %v", msgStyle)
}
}
return responses, nil
}
// StartConvMulti initiates a PAM conversation with multiple ConvRequest's.
func (m *moduleTransaction) StartConvMulti(requests []ConvRequest) (
[]ConvResponse, error) {
return m.startConvMultiImpl(m, requests)
}

1111
module-transaction_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +0,0 @@
#include "_cgo_export.h"
#include <security/pam_appl.h>
#include <stdint.h>
#include <string.h>
#ifdef __sun
#define PAM_CONST
#else
#define PAM_CONST const
#endif
int cb_pam_conv(
int num_msg,
PAM_CONST struct pam_message **msg,
struct pam_response **resp,
void *appdata_ptr)
{
*resp = calloc(num_msg, sizeof **resp);
if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) {
return PAM_CONV_ERR;
}
if (!*resp) {
return PAM_BUF_ERR;
}
for (size_t i = 0; i < num_msg; ++i) {
struct cbPAMConv_return result = cbPAMConv(
msg[i]->msg_style,
(char *)msg[i]->msg,
(uintptr_t)appdata_ptr);
if (result.r1 != PAM_SUCCESS) {
goto error;
}
(*resp)[i].resp = result.r0;
}
return PAM_SUCCESS;
error:
for (size_t i = 0; i < num_msg; ++i) {
if ((*resp)[i].resp) {
memset((*resp)[i].resp, 0, strlen((*resp)[i].resp));
free((*resp)[i].resp);
}
}
memset(*resp, 0, num_msg * sizeof *resp);
free(*resp);
*resp = NULL;
return PAM_CONV_ERR;
}
void init_pam_conv(struct pam_conv *conv, uintptr_t appdata)
{
conv->conv = cb_pam_conv;
conv->appdata_ptr = (void *)appdata;
}
// pam_start_confdir is a recent PAM api to declare a confdir (mostly for tests)
// weaken the linking dependency to detect if its present.
int pam_start_confdir(const char *service_name, const char *user, const struct pam_conv *pam_conversation, const char *confdir, pam_handle_t **pamh) __attribute__ ((weak));
int check_pam_start_confdir(void) {
if (pam_start_confdir == NULL)
return 1;
return 0;
}

View File

@@ -1,32 +1,21 @@
// Package pam provides a wrapper for the PAM application API. // Package pam provides a wrapper for the PAM application API.
package pam package pam
//#include <security/pam_appl.h>
//#include <stdlib.h>
//#include <stdint.h>
//#cgo CFLAGS: -Wall -std=c99 //#cgo CFLAGS: -Wall -std=c99
//#cgo LDFLAGS: -lpam //#cgo LDFLAGS: -lpam
//void init_pam_conv(struct pam_conv *conv, uintptr_t);
//int pam_start_confdir(const char *service_name, const char *user, const struct pam_conv *pam_conversation, const char *confdir, pam_handle_t **pamh) __attribute__ ((weak));
//int check_pam_start_confdir(void);
// //
//#ifdef PAM_BINARY_PROMPT //#include "transaction.h"
//#define BINARY_PROMPT_IS_SUPPORTED 1
//#else
//#include <limits.h>
//#define PAM_BINARY_PROMPT INT_MAX
//#define BINARY_PROMPT_IS_SUPPORTED 0
//#endif
import "C" import "C"
import ( import (
"errors"
"runtime"
"runtime/cgo"
"strings" "strings"
"sync/atomic"
"unsafe" "unsafe"
) )
// success indicates a successful function return.
const success = C.PAM_SUCCESS
// Style is the type of message that the conversation handler should display. // Style is the type of message that the conversation handler should display.
type Style int type Style int
@@ -37,155 +26,42 @@ const (
PromptEchoOff Style = C.PAM_PROMPT_ECHO_OFF PromptEchoOff Style = C.PAM_PROMPT_ECHO_OFF
// PromptEchoOn indicates the conversation handler should obtain a // PromptEchoOn indicates the conversation handler should obtain a
// string while echoing text. // string while echoing text.
PromptEchoOn = C.PAM_PROMPT_ECHO_ON PromptEchoOn Style = C.PAM_PROMPT_ECHO_ON
// ErrorMsg indicates the conversation handler should display an // ErrorMsg indicates the conversation handler should display an
// error message. // error message.
ErrorMsg = C.PAM_ERROR_MSG ErrorMsg Style = C.PAM_ERROR_MSG
// TextInfo indicates the conversation handler should display some // TextInfo indicates the conversation handler should display some
// text. // text.
TextInfo = C.PAM_TEXT_INFO TextInfo Style = C.PAM_TEXT_INFO
// BinaryPrompt indicates the conversation handler that should implement
// the private binary protocol
BinaryPrompt Style = C.PAM_BINARY_PROMPT
) )
// ConversationHandler is an interface for objects that can be used as
// conversation callbacks during PAM authentication.
type ConversationHandler interface {
// RespondPAM receives a message style and a message string. If the
// message Style is PromptEchoOff or PromptEchoOn then the function
// should return a response string.
RespondPAM(Style, string) (string, error)
}
// BinaryPointer exposes the type used for the data in a binary conversation // BinaryPointer exposes the type used for the data in a binary conversation
// it represents a pointer to data that is produced by the module and that // it represents a pointer to data that is produced by the module and that
// must be parsed depending on the protocol in use // must be parsed depending on the protocol in use
type BinaryPointer unsafe.Pointer type BinaryPointer unsafe.Pointer
type BinaryConversationHandler interface { // NativeHandle is the type of the native PAM handle for a transaction so that
ConversationHandler // it can be exported
// Respond receives a pointer to the binary message. It's up to the type NativeHandle = *C.pam_handle_t
// receiver to parse it according to the protocol specifications.
// The function can return a byte array that will passed as pointer back // transactionBase is a handler for a PAM transaction that can be used to
// to the module. // group the operations that can be performed both by the application and the
RespondPAMBinary(BinaryPointer) ([]byte, error) // module side
type transactionBase struct {
handle NativeHandle
lastStatus atomic.Int32
} }
// ConversationFunc is an adapter to allow the use of ordinary functions as // Allows to call pam functions managing return status
// conversation callbacks. func (t *transactionBase) handlePamStatus(cStatus C.int) error {
type ConversationFunc func(Style, string) (string, error) t.lastStatus.Store(int32(cStatus))
if status := Error(cStatus); status != success {
// RespondPAM is a conversation callback adapter. return status
func (f ConversationFunc) RespondPAM(s Style, msg string) (string, error) {
return f(s, msg)
}
// cbPAMConv is a wrapper for the conversation callback function.
//export cbPAMConv
func cbPAMConv(s C.int, msg *C.char, c C.uintptr_t) (*C.char, C.int) {
var r string
var err error
v := cgo.Handle(c).Value()
switch cb := v.(type) {
case ConversationHandler:
if s == C.PAM_BINARY_PROMPT {
return nil, C.PAM_AUTHINFO_UNAVAIL
}
r, err = cb.RespondPAM(Style(s), C.GoString(msg))
case BinaryConversationHandler:
if s == C.PAM_BINARY_PROMPT {
bytes, err := cb.RespondPAMBinary(BinaryPointer(msg))
if err != nil {
return nil, C.PAM_CONV_ERR
}
return (*C.char)(C.CBytes(bytes)), C.PAM_SUCCESS
} else {
r, err = cb.RespondPAM(Style(s), C.GoString(msg))
}
} }
if err != nil { return nil
return nil, C.PAM_CONV_ERR
}
return C.CString(r), C.PAM_SUCCESS
}
// Transaction is the application's handle for a PAM transaction.
type Transaction struct {
handle *C.pam_handle_t
conv *C.struct_pam_conv
status C.int
c cgo.Handle
}
// transactionFinalizer cleans up the PAM handle and deletes the callback
// function.
func transactionFinalizer(t *Transaction) {
C.pam_end(t.handle, t.status)
t.c.Delete()
}
// Start initiates a new PAM transaction. Service is treated identically to
// how pam_start treats it internally.
//
// All application calls to PAM begin with Start*. The returned
// transaction provides an interface to the remainder of the API.
func Start(service, user string, handler ConversationHandler) (*Transaction, error) {
return start(service, user, handler, "")
}
// StartFunc registers the handler func as a conversation handler.
func StartFunc(service, user string, handler func(Style, string) (string, error)) (*Transaction, error) {
return Start(service, user, ConversationFunc(handler))
}
// StartConfDir initiates a new PAM transaction. Service is treated identically to
// how pam_start treats it internally.
// confdir allows to define where all pam services are defined. This is used to provide
// custom paths for tests.
//
// All application calls to PAM begin with Start*. The returned
// transaction provides an interface to the remainder of the API.
func StartConfDir(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) {
if !CheckPamHasStartConfdir() {
return nil, errors.New("StartConfDir() was used, but the pam version on the system is not recent enough")
}
return start(service, user, handler, confDir)
}
func start(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) {
switch handler.(type) {
case BinaryConversationHandler:
if C.BINARY_PROMPT_IS_SUPPORTED == 0 {
return nil, errors.New("BinaryConversationHandler() was used, but it is not supported by this platform")
}
}
t := &Transaction{
conv: &C.struct_pam_conv{},
c: cgo.NewHandle(handler),
}
C.init_pam_conv(t.conv, C.uintptr_t(t.c))
runtime.SetFinalizer(t, transactionFinalizer)
s := C.CString(service)
defer C.free(unsafe.Pointer(s))
var u *C.char
if len(user) != 0 {
u = C.CString(user)
defer C.free(unsafe.Pointer(u))
}
if confDir == "" {
t.status = C.pam_start(s, u, t.conv, &t.handle)
} else {
c := C.CString(confDir)
defer C.free(unsafe.Pointer(c))
t.status = C.pam_start_confdir(s, u, t.conv, c, &t.handle)
}
if t.status != C.PAM_SUCCESS {
return nil, t
}
return t, nil
}
func (t *Transaction) Error() string {
return C.GoString(C.pam_strerror(t.handle, C.int(t.status)))
} }
// Item is a an PAM information type. // Item is a an PAM information type.
@@ -196,38 +72,42 @@ const (
// Service is the name which identifies the PAM stack. // Service is the name which identifies the PAM stack.
Service Item = C.PAM_SERVICE Service Item = C.PAM_SERVICE
// User identifies the username identity used by a service. // User identifies the username identity used by a service.
User = C.PAM_USER User Item = C.PAM_USER
// Tty is the terminal name. // Tty is the terminal name.
Tty = C.PAM_TTY Tty Item = C.PAM_TTY
// Rhost is the requesting host name. // Rhost is the requesting host name.
Rhost = C.PAM_RHOST Rhost Item = C.PAM_RHOST
// Authtok is the currently active authentication token. // Authtok is the currently active authentication token.
Authtok = C.PAM_AUTHTOK Authtok Item = C.PAM_AUTHTOK
// Oldauthtok is the old authentication token. // Oldauthtok is the old authentication token.
Oldauthtok = C.PAM_OLDAUTHTOK Oldauthtok Item = C.PAM_OLDAUTHTOK
// Ruser is the requesting user name. // Ruser is the requesting user name.
Ruser = C.PAM_RUSER Ruser Item = C.PAM_RUSER
// UserPrompt is the string use to prompt for a username. // UserPrompt is the string use to prompt for a username.
UserPrompt = C.PAM_USER_PROMPT UserPrompt Item = C.PAM_USER_PROMPT
// FailDelay is the app supplied function to override failure delays.
FailDelay Item = C.PAM_FAIL_DELAY
// Xdisplay is the X display name
Xdisplay Item = C.PAM_XDISPLAY
// Xauthdata is the X server authentication data.
Xauthdata Item = C.PAM_XAUTHDATA
// AuthtokType is the type for pam_get_authtok
AuthtokType Item = C.PAM_AUTHTOK_TYPE
) )
// SetItem sets a PAM information item. // SetItem sets a PAM information item.
func (t *Transaction) SetItem(i Item, item string) error { func (t *transactionBase) SetItem(i Item, item string) error {
cs := unsafe.Pointer(C.CString(item)) cs := unsafe.Pointer(C.CString(item))
defer C.free(cs) defer C.free(cs)
t.status = C.pam_set_item(t.handle, C.int(i), cs) return t.handlePamStatus(C.pam_set_item(t.handle, C.int(i), cs))
if t.status != C.PAM_SUCCESS {
return t
}
return nil
} }
// GetItem retrieves a PAM information item. // GetItem retrieves a PAM information item.
func (t *Transaction) GetItem(i Item) (string, error) { func (t *transactionBase) GetItem(i Item) (string, error) {
var s unsafe.Pointer var s unsafe.Pointer
t.status = C.pam_get_item(t.handle, C.int(i), &s) err := t.handlePamStatus(C.pam_get_item(t.handle, C.int(i), &s))
if t.status != C.PAM_SUCCESS { if err != nil {
return "", t return "", err
} }
return C.GoString((*C.char)(s)), nil return C.GoString((*C.char)(s)), nil
} }
@@ -243,107 +123,36 @@ const (
Silent Flags = C.PAM_SILENT Silent Flags = C.PAM_SILENT
// DisallowNullAuthtok indicates that authorization should fail // DisallowNullAuthtok indicates that authorization should fail
// if the user does not have a registered authentication token. // if the user does not have a registered authentication token.
DisallowNullAuthtok = C.PAM_DISALLOW_NULL_AUTHTOK DisallowNullAuthtok Flags = C.PAM_DISALLOW_NULL_AUTHTOK
// EstablishCred indicates that credentials should be established // EstablishCred indicates that credentials should be established
// for the user. // for the user.
EstablishCred = C.PAM_ESTABLISH_CRED EstablishCred Flags = C.PAM_ESTABLISH_CRED
// DeleteCred inidicates that credentials should be deleted. // DeleteCred indicates that credentials should be deleted.
DeleteCred = C.PAM_DELETE_CRED DeleteCred Flags = C.PAM_DELETE_CRED
// ReinitializeCred indicates that credentials should be fully // ReinitializeCred indicates that credentials should be fully
// reinitialized. // reinitialized.
ReinitializeCred = C.PAM_REINITIALIZE_CRED ReinitializeCred Flags = C.PAM_REINITIALIZE_CRED
// RefreshCred indicates that the lifetime of existing credentials // RefreshCred indicates that the lifetime of existing credentials
// should be extended. // should be extended.
RefreshCred = C.PAM_REFRESH_CRED RefreshCred Flags = C.PAM_REFRESH_CRED
// ChangeExpiredAuthtok indicates that the authentication token // ChangeExpiredAuthtok indicates that the authentication token
// should be changed if it has expired. // should be changed if it has expired.
ChangeExpiredAuthtok = C.PAM_CHANGE_EXPIRED_AUTHTOK ChangeExpiredAuthtok Flags = C.PAM_CHANGE_EXPIRED_AUTHTOK
) )
// Authenticate is used to authenticate the user.
//
// Valid flags: Silent, DisallowNullAuthtok
func (t *Transaction) Authenticate(f Flags) error {
t.status = C.pam_authenticate(t.handle, C.int(f))
if t.status != C.PAM_SUCCESS {
return t
}
return nil
}
// SetCred is used to establish, maintain and delete the credentials of a
// user.
//
// Valid flags: EstablishCred, DeleteCred, ReinitializeCred, RefreshCred
func (t *Transaction) SetCred(f Flags) error {
t.status = C.pam_setcred(t.handle, C.int(f))
if t.status != C.PAM_SUCCESS {
return t
}
return nil
}
// AcctMgmt is used to determine if the user's account is valid.
//
// Valid flags: Silent, DisallowNullAuthtok
func (t *Transaction) AcctMgmt(f Flags) error {
t.status = C.pam_acct_mgmt(t.handle, C.int(f))
if t.status != C.PAM_SUCCESS {
return t
}
return nil
}
// ChangeAuthTok is used to change the authentication token.
//
// Valid flags: Silent, ChangeExpiredAuthtok
func (t *Transaction) ChangeAuthTok(f Flags) error {
t.status = C.pam_chauthtok(t.handle, C.int(f))
if t.status != C.PAM_SUCCESS {
return t
}
return nil
}
// OpenSession sets up a user session for an authenticated user.
//
// Valid flags: Slient
func (t *Transaction) OpenSession(f Flags) error {
t.status = C.pam_open_session(t.handle, C.int(f))
if t.status != C.PAM_SUCCESS {
return t
}
return nil
}
// CloseSession closes a previously opened session.
//
// Valid flags: Silent
func (t *Transaction) CloseSession(f Flags) error {
t.status = C.pam_close_session(t.handle, C.int(f))
if t.status != C.PAM_SUCCESS {
return t
}
return nil
}
// PutEnv adds or changes the value of PAM environment variables. // PutEnv adds or changes the value of PAM environment variables.
// //
// NAME=value will set a variable to a value. // NAME=value will set a variable to a value.
// NAME= will set a variable to an empty value. // NAME= will set a variable to an empty value.
// NAME (without an "=") will delete a variable. // NAME (without an "=") will delete a variable.
func (t *Transaction) PutEnv(nameval string) error { func (t *transactionBase) PutEnv(nameval string) error {
cs := C.CString(nameval) cs := C.CString(nameval)
defer C.free(unsafe.Pointer(cs)) defer C.free(unsafe.Pointer(cs))
t.status = C.pam_putenv(t.handle, cs) return t.handlePamStatus(C.pam_putenv(t.handle, cs))
if t.status != C.PAM_SUCCESS {
return t
}
return nil
} }
// GetEnv is used to retrieve a PAM environment variable. // GetEnv is used to retrieve a PAM environment variable.
func (t *Transaction) GetEnv(name string) string { func (t *transactionBase) GetEnv(name string) string {
cs := C.CString(name) cs := C.CString(name)
defer C.free(unsafe.Pointer(cs)) defer C.free(unsafe.Pointer(cs))
value := C.pam_getenv(t.handle, cs) value := C.pam_getenv(t.handle, cs)
@@ -358,13 +167,14 @@ func next(p **C.char) **C.char {
} }
// GetEnvList returns a copy of the PAM environment as a map. // GetEnvList returns a copy of the PAM environment as a map.
func (t *Transaction) GetEnvList() (map[string]string, error) { func (t *transactionBase) GetEnvList() (map[string]string, error) {
env := make(map[string]string) env := make(map[string]string)
p := C.pam_getenvlist(t.handle) p := C.pam_getenvlist(t.handle)
if p == nil { if p == nil {
t.status = C.PAM_BUF_ERR t.lastStatus.Store(int32(ErrBuf))
return nil, t return nil, ErrBuf
} }
t.lastStatus.Store(success)
for q := p; *q != nil; q = next(q) { for q := p; *q != nil; q = next(q) {
chunks := strings.SplitN(C.GoString(*q), "=", 2) chunks := strings.SplitN(C.GoString(*q), "=", 2)
if len(chunks) == 2 { if len(chunks) == 2 {
@@ -380,3 +190,8 @@ func (t *Transaction) GetEnvList() (map[string]string, error) {
func CheckPamHasStartConfdir() bool { func CheckPamHasStartConfdir() bool {
return C.check_pam_start_confdir() == 0 return C.check_pam_start_confdir() == 0
} }
// CheckPamHasBinaryProtocol return if pam on system supports PAM_BINARY_PROMPT
func CheckPamHasBinaryProtocol() bool {
return C.BINARY_PROMPT_IS_SUPPORTED != 0
}

99
transaction.h Normal file
View File

@@ -0,0 +1,99 @@
#pragma once
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifdef PAM_BINARY_PROMPT
#define BINARY_PROMPT_IS_SUPPORTED 1
#else
#include <limits.h>
#define PAM_BINARY_PROMPT INT_MAX
#define BINARY_PROMPT_IS_SUPPORTED 0
#endif
#ifdef __sun
#define PAM_CONST
#else
#define PAM_CONST const
#endif
extern int _go_pam_conv_handler(struct pam_message *, uintptr_t, char **reply);
extern void _go_pam_data_cleanup(pam_handle_t *, uintptr_t, int status);
static inline int cb_pam_conv(int num_msg, PAM_CONST struct pam_message **msg, struct pam_response **resp, void *appdata_ptr)
{
if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG)
return PAM_CONV_ERR;
*resp = calloc(num_msg, sizeof **resp);
if (!*resp)
return PAM_BUF_ERR;
for (size_t i = 0; i < num_msg; ++i) {
int result = _go_pam_conv_handler((struct pam_message *)msg[i], (uintptr_t)appdata_ptr, &(*resp)[i].resp);
if (result != PAM_SUCCESS)
goto error;
}
return PAM_SUCCESS;
error:
for (size_t i = 0; i < num_msg; ++i) {
if ((*resp)[i].resp) {
#ifdef PAM_BINARY_PROMPT
if (msg[i]->msg_style != PAM_BINARY_PROMPT)
#endif
memset((*resp)[i].resp, 0, strlen((*resp)[i].resp));
free((*resp)[i].resp);
}
}
memset(*resp, 0, num_msg * sizeof *resp);
free(*resp);
*resp = NULL;
return PAM_CONV_ERR;
}
static inline void init_pam_conv(struct pam_conv *conv, uintptr_t appdata)
{
conv->conv = cb_pam_conv;
conv->appdata_ptr = (void *)appdata;
}
static inline int start_pam_conv(struct pam_conv *pc, int num_msgs, const struct pam_message **msgs, struct pam_response **out_resp)
{
return pc->conv(num_msgs, msgs, out_resp, pc->appdata_ptr);
}
// pam_start_confdir is a recent PAM api to declare a confdir (mostly for
// tests) weaken the linking dependency to detect if its present.
int pam_start_confdir(const char *service_name, const char *user, const struct pam_conv *pam_conversation,
const char *confdir, pam_handle_t **pamh) __attribute__((weak));
static inline int check_pam_start_confdir(void)
{
if (pam_start_confdir == NULL)
return 1;
return 0;
}
static inline void data_cleanup(pam_handle_t *pamh, void *data, int error_status)
{
_go_pam_data_cleanup(pamh, (uintptr_t)data, error_status);
}
static inline int set_data(pam_handle_t *pamh, const char *name, uintptr_t handle)
{
if (handle)
return pam_set_data(pamh, name, (void *)handle, data_cleanup);
return pam_set_data(pamh, name, NULL, NULL);
}
static inline int get_data(pam_handle_t *pamh, const char *name, uintptr_t *out_handle)
{
return pam_get_data(pamh, name, (const void **)out_handle);
}

View File

@@ -2,11 +2,44 @@ package pam
import ( import (
"errors" "errors"
"fmt"
"os"
"os/user" "os/user"
"path/filepath"
"runtime"
"sync/atomic"
"testing" "testing"
"time"
"unsafe"
) )
func maybeEndTransaction(t *testing.T, tx *Transaction) {
t.Helper()
if tx == nil {
return
}
err := tx.End()
if err != nil {
t.Fatalf("end #error: %v", err)
}
}
func ensureTransactionEnds(t *testing.T, tx *Transaction) {
t.Helper()
runtime.SetFinalizer(tx, func(tx *Transaction) {
// #nosec:G103 - the pointer conversion is checked.
handle := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&tx.handle)))
if handle == nil {
return
}
t.Fatalf("transaction has not been finalized")
})
}
func TestPAM_001(t *testing.T) { func TestPAM_001(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
u, _ := user.Current() u, _ := user.Current()
if u.Uid != "0" { if u.Uid != "0" {
t.Skip("run this test as root") t.Skip("run this test as root")
@@ -15,6 +48,8 @@ func TestPAM_001(t *testing.T) {
tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) { tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) {
return p, nil return p, nil
}) })
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
@@ -33,6 +68,7 @@ func TestPAM_001(t *testing.T) {
} }
func TestPAM_002(t *testing.T) { func TestPAM_002(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
u, _ := user.Current() u, _ := user.Current()
if u.Uid != "0" { if u.Uid != "0" {
t.Skip("run this test as root") t.Skip("run this test as root")
@@ -46,6 +82,8 @@ func TestPAM_002(t *testing.T) {
} }
return "", errors.New("unexpected") return "", errors.New("unexpected")
}) })
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
@@ -71,6 +109,7 @@ func (c Credentials) RespondPAM(s Style, msg string) (string, error) {
} }
func TestPAM_003(t *testing.T) { func TestPAM_003(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
u, _ := user.Current() u, _ := user.Current()
if u.Uid != "0" { if u.Uid != "0" {
t.Skip("run this test as root") t.Skip("run this test as root")
@@ -80,6 +119,8 @@ func TestPAM_003(t *testing.T) {
Password: "secret", Password: "secret",
} }
tx, err := Start("", "", c) tx, err := Start("", "", c)
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
@@ -90,6 +131,7 @@ func TestPAM_003(t *testing.T) {
} }
func TestPAM_004(t *testing.T) { func TestPAM_004(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
u, _ := user.Current() u, _ := user.Current()
if u.Uid != "0" { if u.Uid != "0" {
t.Skip("run this test as root") t.Skip("run this test as root")
@@ -98,6 +140,8 @@ func TestPAM_004(t *testing.T) {
Password: "secret", Password: "secret",
} }
tx, err := Start("", "test", c) tx, err := Start("", "test", c)
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
@@ -108,16 +152,29 @@ func TestPAM_004(t *testing.T) {
} }
func TestPAM_005(t *testing.T) { func TestPAM_005(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
u, _ := user.Current() u, _ := user.Current()
if u.Uid != "0" { if u.Uid != "0" {
t.Skip("run this test as root") t.Skip("run this test as root")
} }
if _, found := os.LookupEnv("GO_PAM_TEST_WITH_ASAN"); found {
t.Skip("test fails under ASAN")
}
tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) { tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) {
return "secret", nil return "secret", nil
}) })
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
service, err := tx.GetItem(Service)
if err != nil {
t.Fatalf("GetItem #error: %v", err)
}
if service != "passwd" {
t.Fatalf("Unexpected service: %v", service)
}
err = tx.ChangeAuthTok(Silent) err = tx.ChangeAuthTok(Silent)
if err != nil { if err != nil {
t.Fatalf("chauthtok #error: %v", err) t.Fatalf("chauthtok #error: %v", err)
@@ -125,6 +182,7 @@ func TestPAM_005(t *testing.T) {
} }
func TestPAM_006(t *testing.T) { func TestPAM_006(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
u, _ := user.Current() u, _ := user.Current()
if u.Uid != "0" { if u.Uid != "0" {
t.Skip("run this test as root") t.Skip("run this test as root")
@@ -132,6 +190,8 @@ func TestPAM_006(t *testing.T) {
tx, err := StartFunc("passwd", u.Username, func(s Style, msg string) (string, error) { tx, err := StartFunc("passwd", u.Username, func(s Style, msg string) (string, error) {
return "secret", nil return "secret", nil
}) })
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
@@ -146,6 +206,7 @@ func TestPAM_006(t *testing.T) {
} }
func TestPAM_007(t *testing.T) { func TestPAM_007(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
u, _ := user.Current() u, _ := user.Current()
if u.Uid != "0" { if u.Uid != "0" {
t.Skip("run this test as root") t.Skip("run this test as root")
@@ -153,6 +214,8 @@ func TestPAM_007(t *testing.T) {
tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) { tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) {
return "", errors.New("Sorry, it didn't work") return "", errors.New("Sorry, it didn't work")
}) })
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
@@ -164,15 +227,24 @@ func TestPAM_007(t *testing.T) {
if len(s) == 0 { if len(s) == 0 {
t.Fatalf("error #expected an error message") t.Fatalf("error #expected an error message")
} }
if !errors.Is(err, ErrAuth) {
t.Fatalf("error #unexpected error %v", err)
}
} }
func TestPAM_ConfDir(t *testing.T) { func TestPAM_ConfDir(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
u, _ := user.Current() u, _ := user.Current()
c := Credentials{ c := Credentials{
// the custom service always permits even with wrong password. // the custom service always permits even with wrong password.
Password: "wrongsecret", Password: "wrongsecret",
} }
tx, err := StartConfDir("permit-service", u.Username, c, "test-services") tx, err := StartConfDir("permit-service", u.Username, c, "test-services")
defer func() {
if tx != nil {
_ = tx.End()
}
}()
if !CheckPamHasStartConfdir() { if !CheckPamHasStartConfdir() {
if err == nil { if err == nil {
t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err)
@@ -180,6 +252,13 @@ func TestPAM_ConfDir(t *testing.T) {
// nothing else we do, we don't support it. // nothing else we do, we don't support it.
return return
} }
service, err := tx.GetItem(Service)
if err != nil {
t.Fatalf("GetItem #error: %v", err)
}
if service != "permit-service" {
t.Fatalf("Unexpected service: %v", service)
}
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
@@ -190,21 +269,36 @@ func TestPAM_ConfDir(t *testing.T) {
} }
func TestPAM_ConfDir_FailNoServiceOrUnsupported(t *testing.T) { func TestPAM_ConfDir_FailNoServiceOrUnsupported(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
if !CheckPamHasStartConfdir() {
t.Skip("this requires PAM with Conf dir support")
}
u, _ := user.Current() u, _ := user.Current()
c := Credentials{ c := Credentials{
Password: "secret", Password: "secret",
} }
_, err := StartConfDir("does-not-exists", u.Username, c, ".") tx, err := StartConfDir("does-not-exists", u.Username, c, ".")
if err == nil { if err == nil {
t.Fatalf("authenticate #expected an error") t.Fatalf("authenticate #expected an error")
} }
if tx != nil {
t.Fatalf("authenticate #unexpected transaction")
}
s := err.Error() s := err.Error()
if len(s) == 0 { if len(s) == 0 {
t.Fatalf("error #expected an error message") t.Fatalf("error #expected an error message")
} }
var pamErr Error
if !errors.As(err, &pamErr) {
t.Fatalf("error #unexpected type: %#v", err)
}
if pamErr != ErrAbort {
t.Fatalf("error #unexpected status: %v", pamErr)
}
} }
func TestPAM_ConfDir_InfoMessage(t *testing.T) { func TestPAM_ConfDir_InfoMessage(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
u, _ := user.Current() u, _ := user.Current()
var infoText string var infoText string
tx, err := StartConfDir("echo-service", u.Username, tx, err := StartConfDir("echo-service", u.Username,
@@ -216,24 +310,46 @@ func TestPAM_ConfDir_InfoMessage(t *testing.T) {
} }
return "", errors.New("unexpected") return "", errors.New("unexpected")
}), "test-services") }), "test-services")
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
service, err := tx.GetItem(Service)
if err != nil {
t.Fatalf("GetItem #error: %v", err)
}
if service != "echo-service" {
t.Fatalf("Unexpected service: %v", service)
}
err = tx.Authenticate(0) err = tx.Authenticate(0)
if err != nil { if err != nil {
t.Fatalf("authenticate #error: %v", err) t.Fatalf("authenticate #error: %v", err)
} }
if infoText != "This is an info message for user " + u.Username + " on echo-service" { if infoText != "This is an info message for user "+u.Username+" on echo-service" {
t.Fatalf("Unexpected info message: %v", infoText) t.Fatalf("Unexpected info message: %v", infoText)
} }
} }
func TestPAM_ConfDir_Deny(t *testing.T) { func TestPAM_ConfDir_Deny(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
if !CheckPamHasStartConfdir() {
t.Skip("this requires PAM with Conf dir support")
}
u, _ := user.Current() u, _ := user.Current()
tx, err := StartConfDir("deny-service", u.Username, Credentials{}, "test-services") tx, err := StartConfDir("deny-service", u.Username, Credentials{}, "test-services")
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
service, err := tx.GetItem(Service)
if err != nil {
t.Fatalf("GetItem #error: %v", err)
}
if service != "deny-service" {
t.Fatalf("Unexpected service: %v", service)
}
err = tx.Authenticate(0) err = tx.Authenticate(0)
if err == nil { if err == nil {
t.Fatalf("authenticate #expected an error") t.Fatalf("authenticate #expected an error")
@@ -242,15 +358,21 @@ func TestPAM_ConfDir_Deny(t *testing.T) {
if len(s) == 0 { if len(s) == 0 {
t.Fatalf("error #expected an error message") t.Fatalf("error #expected an error message")
} }
if !errors.Is(err, ErrAuth) {
t.Fatalf("error #unexpected error %v", err)
}
} }
func TestPAM_ConfDir_PromptForUserName(t *testing.T) { func TestPAM_ConfDir_PromptForUserName(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
c := Credentials{ c := Credentials{
User: "testuser", User: "testuser",
// the custom service only cares about correct user name. // the custom service only cares about correct user name.
Password: "wrongsecret", Password: "wrongsecret",
} }
tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services") tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services")
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if !CheckPamHasStartConfdir() { if !CheckPamHasStartConfdir() {
if err == nil { if err == nil {
t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err)
@@ -268,11 +390,14 @@ func TestPAM_ConfDir_PromptForUserName(t *testing.T) {
} }
func TestPAM_ConfDir_WrongUserName(t *testing.T) { func TestPAM_ConfDir_WrongUserName(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
c := Credentials{ c := Credentials{
User: "wronguser", User: "wronguser",
Password: "wrongsecret", Password: "wrongsecret",
} }
tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services") tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services")
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if !CheckPamHasStartConfdir() { if !CheckPamHasStartConfdir() {
if err == nil { if err == nil {
t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err)
@@ -288,12 +413,21 @@ func TestPAM_ConfDir_WrongUserName(t *testing.T) {
if len(s) == 0 { if len(s) == 0 {
t.Fatalf("error #expected an error message") t.Fatalf("error #expected an error message")
} }
if !errors.Is(err, ErrAuth) {
t.Fatalf("error #unexpected error %v", err)
}
} }
func TestItem(t *testing.T) { func TestItem(t *testing.T) {
tx, _ := StartFunc("passwd", "test", func(s Style, msg string) (string, error) { t.Cleanup(maybeDoLeakCheck)
tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) {
return "", nil return "", nil
}) })
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil {
t.Fatalf("start #error: %v", err)
}
s, err := tx.GetItem(Service) s, err := tx.GetItem(Service)
if err != nil { if err != nil {
@@ -325,9 +459,12 @@ func TestItem(t *testing.T) {
} }
func TestEnv(t *testing.T) { func TestEnv(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx, err := StartFunc("", "", func(s Style, msg string) (string, error) { tx, err := StartFunc("", "", func(s Style, msg string) (string, error) {
return "", nil return "", nil
}) })
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil { if err != nil {
t.Fatalf("start #error: %v", err) t.Fatalf("start #error: %v", err)
} }
@@ -390,7 +527,143 @@ func TestEnv(t *testing.T) {
} }
} }
func Test_Error(t *testing.T) {
t.Parallel()
t.Cleanup(maybeDoLeakCheck)
if !CheckPamHasStartConfdir() {
t.Skip("this requires PAM with Conf dir support")
}
statuses := map[string]error{
"success": nil,
"open_err": ErrOpen,
"symbol_err": ErrSymbol,
"service_err": ErrService,
"system_err": ErrSystem,
"buf_err": ErrBuf,
"perm_denied": ErrPermDenied,
"auth_err": ErrAuth,
"cred_insufficient": ErrCredInsufficient,
"authinfo_unavail": ErrAuthinfoUnavail,
"user_unknown": ErrUserUnknown,
"maxtries": ErrMaxtries,
"new_authtok_reqd": ErrNewAuthtokReqd,
"acct_expired": ErrAcctExpired,
"session_err": ErrSession,
"cred_unavail": ErrCredUnavail,
"cred_expired": ErrCredExpired,
"cred_err": ErrCred,
"no_module_data": ErrNoModuleData,
"conv_err": ErrConv,
"authtok_err": ErrAuthtok,
"authtok_recover_err": ErrAuthtokRecovery,
"authtok_lock_busy": ErrAuthtokLockBusy,
"authtok_disable_aging": ErrAuthtokDisableAging,
"try_again": ErrTryAgain,
"ignore": nil, /* Ignore can't be returned */
"abort": ErrAbort,
"authtok_expired": ErrAuthtokExpired,
"module_unknown": ErrModuleUnknown,
"bad_item": ErrBadItem,
"conv_again": ErrConvAgain,
"incomplete": ErrIncomplete,
}
type Action int
const (
account Action = iota + 1
auth
password
session
)
actions := map[string]Action{
"account": account,
"auth": auth,
"password": password,
"session": session,
}
c := Credentials{}
servicePath := t.TempDir()
for ret, expected := range statuses {
ret := ret
expected := expected
for actionName, action := range actions {
actionName := actionName
action := action
t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) {
t.Parallel()
serviceName := ret + "-" + actionName
serviceFile := filepath.Join(servicePath, serviceName)
contents := fmt.Sprintf("%[1]s requisite pam_debug.so "+
"auth=%[2]s cred=%[2]s acct=%[2]s prechauthtok=%[2]s "+
"chauthtok=%[2]s open_session=%[2]s close_session=%[2]s\n"+
"%[1]s requisite pam_permit.so\n", actionName, ret)
if err := os.WriteFile(serviceFile,
[]byte(contents), 0600); err != nil {
t.Fatalf("can't create service file %v: %v", serviceFile, err)
}
tx, err := StartConfDir(serviceName, "user", c, servicePath)
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil {
t.Fatalf("start #error: %v", err)
}
switch action {
case account:
err = tx.AcctMgmt(0)
case auth:
err = tx.Authenticate(0)
case password:
err = tx.ChangeAuthTok(0)
case session:
err = tx.OpenSession(0)
}
if !errors.Is(err, expected) {
t.Fatalf("error #unexpected status %#v vs %#v", err,
expected)
}
if err != nil {
var status Error
if !errors.As(err, &status) || err.Error() != status.Error() {
t.Fatalf("error #unexpected status %#v vs %#v", err.Error(),
status.Error())
}
}
})
}
}
}
func Test_Finalizer(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
if !CheckPamHasStartConfdir() {
t.Skip("this requires PAM with Conf dir support")
}
func() {
tx, err := StartConfDir("permit-service", "", nil, "test-services")
ensureTransactionEnds(t, tx)
defer maybeEndTransaction(t, tx)
if err != nil {
t.Fatalf("start #error: %v", err)
}
}()
runtime.GC()
// sleep to switch to finalizer goroutine
time.Sleep(5 * time.Millisecond)
}
func TestFailure_001(t *testing.T) { func TestFailure_001(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{} tx := Transaction{}
_, err := tx.GetEnvList() _, err := tx.GetEnvList()
if err == nil { if err == nil {
@@ -399,6 +672,7 @@ func TestFailure_001(t *testing.T) {
} }
func TestFailure_002(t *testing.T) { func TestFailure_002(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{} tx := Transaction{}
err := tx.PutEnv("") err := tx.PutEnv("")
if err == nil { if err == nil {
@@ -407,6 +681,7 @@ func TestFailure_002(t *testing.T) {
} }
func TestFailure_003(t *testing.T) { func TestFailure_003(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{} tx := Transaction{}
err := tx.CloseSession(0) err := tx.CloseSession(0)
if err == nil { if err == nil {
@@ -415,6 +690,7 @@ func TestFailure_003(t *testing.T) {
} }
func TestFailure_004(t *testing.T) { func TestFailure_004(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{} tx := Transaction{}
err := tx.OpenSession(0) err := tx.OpenSession(0)
if err == nil { if err == nil {
@@ -423,6 +699,7 @@ func TestFailure_004(t *testing.T) {
} }
func TestFailure_005(t *testing.T) { func TestFailure_005(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{} tx := Transaction{}
err := tx.ChangeAuthTok(0) err := tx.ChangeAuthTok(0)
if err == nil { if err == nil {
@@ -431,6 +708,7 @@ func TestFailure_005(t *testing.T) {
} }
func TestFailure_006(t *testing.T) { func TestFailure_006(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{} tx := Transaction{}
err := tx.AcctMgmt(0) err := tx.AcctMgmt(0)
if err == nil { if err == nil {
@@ -439,6 +717,7 @@ func TestFailure_006(t *testing.T) {
} }
func TestFailure_007(t *testing.T) { func TestFailure_007(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{} tx := Transaction{}
err := tx.SetCred(0) err := tx.SetCred(0)
if err == nil { if err == nil {
@@ -447,6 +726,7 @@ func TestFailure_007(t *testing.T) {
} }
func TestFailure_008(t *testing.T) { func TestFailure_008(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{} tx := Transaction{}
err := tx.SetItem(User, "test") err := tx.SetItem(User, "test")
if err == nil { if err == nil {
@@ -455,9 +735,19 @@ func TestFailure_008(t *testing.T) {
} }
func TestFailure_009(t *testing.T) { func TestFailure_009(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{} tx := Transaction{}
_, err := tx.GetItem(User) _, err := tx.GetItem(User)
if err == nil { if err == nil {
t.Fatalf("getenvlist #expected an error") t.Fatalf("getenvlist #expected an error")
} }
} }
func TestFailure_010(t *testing.T) {
t.Cleanup(maybeDoLeakCheck)
tx := Transaction{}
err := tx.End()
if err != nil {
t.Fatalf("end #unexpected error %v", err)
}
}

42
utils.go Normal file
View File

@@ -0,0 +1,42 @@
// Package pam provides a wrapper for the PAM application API.
package pam
/*
#include <stdlib.h>
#ifdef __SANITIZE_ADDRESS__
#include <sanitizer/lsan_interface.h>
#endif
static inline void
maybe_do_leak_check (void)
{
#ifdef __SANITIZE_ADDRESS__
__lsan_do_leak_check();
#endif
}
*/
import "C"
import (
"os"
"runtime"
"time"
"unsafe"
)
func maybeDoLeakCheck() {
runtime.GC()
time.Sleep(time.Millisecond * 20)
if os.Getenv("GO_PAM_SKIP_LEAK_CHECK") == "" {
C.maybe_do_leak_check()
}
}
func allocateCBytes(bytes []byte) BinaryPointer {
return BinaryPointer(C.CBytes(bytes))
}
func binaryPointerCBytesFinalizer(ptr BinaryPointer) {
C.free(unsafe.Pointer(ptr))
}