diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..5066aeb --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,3 @@ +ignore: + # Ignore pam-moduler generated files + - "**/pam_module.go" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1a296fa..da8e2ca 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -18,6 +18,12 @@ jobs: run: sudo useradd -d /tmp/test -p '$1$Qd8H95T5$RYSZQeoFbEB.gS19zS99A0' -s /bin/false test - name: Checkout code uses: actions/checkout@v4 + - 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: Test run: sudo go test -v -cover -coverprofile=coverage.out ./... - name: Upload coverage reports to Codecov diff --git a/.gitignore b/.gitignore index 2d83068..a2f238d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ coverage.out +example-module/*.so +example-module/*.h diff --git a/README.md b/README.md index fab308e..bfbd88f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,123 @@ 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 +#include +#include +#include + +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 To run the full suite, the tests must be run as the root user. To setup your diff --git a/cmd/pam-moduler/moduler.go b/cmd/pam-moduler/moduler.go new file mode 100644 index 0000000..b95195d --- /dev/null +++ b/cmd/pam-moduler/moduler.go @@ -0,0 +1,305 @@ +// 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/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") +) + +// 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 + } + + var generateTags []string + if len(*moduleBuildTags) > 0 { + 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, + } + + // 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 +} + +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, ",")) + } + + // 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 + +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) + } + + err := moduleFunc(pam.NewModuleTransaction(pam.NativeHandle(pamh)), + pam.Flags(flags), sliceFromArgv(argc, argv)) + + if err == nil { + return 0; + } + + if (pam.Flags(flags) & pam.Silent) == 0 { + 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) +} +`) + + 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 +} diff --git a/example-module/module.go b/example-module/module.go new file mode 100644 index 0000000..634e3ac --- /dev/null +++ b/example-module/module.go @@ -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) +} diff --git a/example-module/pam_module.go b/example-module/pam_module.go new file mode 100644 index 0000000..b3bfb08 --- /dev/null +++ b/example-module/pam_module.go @@ -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 + +// Package main is the package for the PAM module library. +package main + +/* +#cgo LDFLAGS: -lpam -fPIC +#include + +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) + } + + err := moduleFunc(pam.NewModuleTransaction(pam.NativeHandle(pamh)), + pam.Flags(flags), sliceFromArgv(argc, argv)) + + if err == nil { + return 0 + } + + if (pam.Flags(flags) & pam.Silent) == 0 { + 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() {} diff --git a/module-transaction.go b/module-transaction.go index 9a3aae6..12b3a40 100644 --- a/module-transaction.go +++ b/module-transaction.go @@ -15,7 +15,9 @@ type ModuleTransaction interface { type ModuleHandlerFunc func(ModuleTransaction, Flags, []string) error // ModuleTransaction is the module-side handle for a PAM transaction. -type moduleTransaction = transactionBase +type moduleTransaction struct { + transactionBase +} // ModuleHandler is an interface for objects that can be used to create // PAM modules from go. @@ -27,3 +29,9 @@ type ModuleHandler interface { OpenSession(ModuleTransaction, Flags, []string) error SetCred(ModuleTransaction, Flags, []string) error } + +// NewModuleTransaction allows initializing a transaction invoker from +// the module side. +func NewModuleTransaction(handle NativeHandle) ModuleTransaction { + return &moduleTransaction{transactionBase{handle: handle}} +}