├── .gitignore ├── test-services ├── deny-service ├── permit-service ├── succeed-if-user-test └── echo-service ├── go.mod ├── errors_bsd.go ├── go.sum ├── transaction_linux.go ├── .github └── workflows │ ├── lint.yaml │ └── test.yaml ├── errors_linux.go ├── README.md ├── LICENSE ├── .golangci.yaml ├── example_test.go ├── transaction.c ├── transaction_linux_test.go ├── .clang-format ├── errors.go ├── transaction.go └── transaction_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out 2 | -------------------------------------------------------------------------------- /test-services/deny-service: -------------------------------------------------------------------------------- 1 | # Custom stack to deny permit, independent of the user name/pass 2 | auth requisite pam_deny.so 3 | -------------------------------------------------------------------------------- /test-services/permit-service: -------------------------------------------------------------------------------- 1 | # Custom stack to always permit, independent of the user name/pass 2 | auth required pam_permit.so 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/msteinert/pam/v2 2 | 3 | go 1.20 4 | 5 | require golang.org/x/term v0.6.0 6 | 7 | require golang.org/x/sys v0.6.0 // indirect 8 | -------------------------------------------------------------------------------- /test-services/succeed-if-user-test: -------------------------------------------------------------------------------- 1 | # Custom stack to deny permit, independent of the user name/pass 2 | auth requisite pam_succeed_if.so user = testuser 3 | -------------------------------------------------------------------------------- /test-services/echo-service: -------------------------------------------------------------------------------- 1 | # Custom stack to always permit, independent of the user name/pass 2 | auth optional pam_echo.so This is an info message for user %u on %s 3 | auth required pam_permit.so 4 | -------------------------------------------------------------------------------- /errors_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | 3 | package pam 4 | 5 | /* 6 | #include 7 | */ 8 | import "C" 9 | 10 | // Various errors returned by PAM. 11 | const ( 12 | // ErrBadItem indicates a bad item passed to pam_*_item(). 13 | ErrBadItem Error = C.PAM_BAD_ITEM 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 2 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 3 | golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= 4 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 5 | -------------------------------------------------------------------------------- /transaction_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package pam 4 | 5 | /* 6 | #include 7 | */ 8 | import "C" 9 | 10 | // PAM Item types. 11 | const ( 12 | // FailDelay is the app supplied function to override failure delays. 13 | FailDelay Item = C.PAM_FAIL_DELAY 14 | // Xdisplay is the X display name. 15 | Xdisplay Item = C.PAM_XDISPLAY 16 | // Xauthdata is the X server authentication data. 17 | Xauthdata Item = C.PAM_XAUTHDATA 18 | // AuthtokType is the type for pam_get_authtok. 19 | AuthtokType Item = C.PAM_AUTHTOK_TYPE 20 | ) 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Lint 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | golangci: 9 | name: lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.24' 16 | cache: false 17 | - name: Install PAM 18 | run: | 19 | sudo apt-get update 20 | sudo apt-get install -y libpam-dev 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v8 23 | with: 24 | version: v2.1.6 25 | -------------------------------------------------------------------------------- /errors_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package pam 4 | 5 | /* 6 | #include 7 | */ 8 | import "C" 9 | 10 | // Various errors returned by PAM. 11 | const ( 12 | // ErrBadItem indicates a bad item passed to pam_*_item(). 13 | ErrBadItem Error = C.PAM_BAD_ITEM 14 | // ErrConvAgain indicates a conversation function is event driven and data 15 | // is not available yet. 16 | ErrConvAgain Error = C.PAM_CONV_AGAIN 17 | // ErrIncomplete indicates to please call this function again to complete 18 | // authentication stack. Before calling again, verify that conversation 19 | // is completed. 20 | ErrIncomplete Error = C.PAM_INCOMPLETE 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/msteinert/pam/v2?status.svg)](http://godoc.org/github.com/msteinert/pam/v2) 2 | [![codecov](https://codecov.io/gh/msteinert/pam/graph/badge.svg?token=L1K3UTB065)](https://codecov.io/gh/msteinert/pam) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/msteinert/pam/v2)](https://goreportcard.com/report/github.com/msteinert/pam/v2) 4 | 5 | # Go PAM 6 | 7 | This is a Go wrapper for the PAM application API. 8 | 9 | ## Testing 10 | 11 | To run the full suite, the tests must be run as the root user. To setup your 12 | system for testing, create a user named "test" with the password "secret". For 13 | example: 14 | 15 | ``` 16 | $ sudo useradd test \ 17 | -d /tmp/test \ 18 | -p '$1$Qd8H95T5$RYSZQeoFbEB.gS19zS99A0' \ 19 | -s /bin/false 20 | ``` 21 | 22 | Then execute the tests: 23 | 24 | ``` 25 | $ sudo GOPATH=$GOPATH $(which go) test -v 26 | ``` 27 | 28 | [1]: http://godoc.org/github.com/msteinert/pam/v2 29 | [2]: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_ADG.html 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011, krockot 2 | Copyright 2015, Michael Steinert 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - dupl 5 | - durationcheck 6 | - errname 7 | - errorlint 8 | - forbidigo 9 | - forcetypeassert 10 | - godot 11 | - gosec 12 | - misspell 13 | - nakedret 14 | - nolintlint 15 | - revive 16 | - thelper 17 | - tparallel 18 | - unconvert 19 | - unparam 20 | - whitespace 21 | settings: 22 | forbidigo: 23 | forbid: 24 | - pattern: ioutil\. 25 | - pattern: ^print.*$ 26 | nakedret: 27 | max-func-lines: 1 28 | nolintlint: 29 | require-explanation: true 30 | require-specific: true 31 | exclusions: 32 | generated: lax 33 | rules: 34 | - path: (.+)\.go$ 35 | text: Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv|w\.Stop). is not checked 36 | - path: (.+)\.go$ 37 | text: (G104|G307) 38 | - path: (.+)\.go$ 39 | text: Potential file inclusion via variable 40 | - path: (.+)\.go$ 41 | text: unused-parameter 42 | - path: (.+)\.go$ 43 | text: if-return 44 | paths: 45 | - third_party$ 46 | - builtin$ 47 | - examples$ 48 | issues: 49 | max-issues-per-linter: 0 50 | max-same-issues: 0 51 | fix: false 52 | formatters: 53 | enable: 54 | - gci 55 | - gofmt 56 | exclusions: 57 | generated: lax 58 | paths: 59 | - third_party$ 60 | - builtin$ 61 | - examples$ 62 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package pam_test 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/msteinert/pam/v2" 10 | "golang.org/x/term" 11 | ) 12 | 13 | // This example uses the default PAM service to authenticate any users. This 14 | // should cause PAM to ask its conversation handler for a username and password 15 | // in sequence. 16 | func Example() { 17 | t, err := pam.StartFunc("passwd", "", func(s pam.Style, msg string) (string, error) { 18 | switch s { 19 | case pam.PromptEchoOff: 20 | fmt.Print(msg) 21 | pw, err := term.ReadPassword(int(os.Stdin.Fd())) 22 | if err != nil { 23 | return "", err 24 | } 25 | fmt.Println() 26 | return string(pw), nil 27 | case pam.PromptEchoOn: 28 | fmt.Print(msg) 29 | s := bufio.NewScanner(os.Stdin) 30 | s.Scan() 31 | return s.Text(), nil 32 | case pam.ErrorMsg: 33 | fmt.Fprintf(os.Stderr, "%s\n", msg) 34 | return "", nil 35 | case pam.TextInfo: 36 | fmt.Println(msg) 37 | return "", nil 38 | default: 39 | return "", errors.New("unrecognized message style") 40 | } 41 | }) 42 | if err != nil { 43 | fmt.Fprintf(os.Stderr, "start: %v\n", err) 44 | os.Exit(1) 45 | } 46 | defer func() { 47 | err := t.End() 48 | if err != nil { 49 | fmt.Fprintf(os.Stderr, "end: %v\n", err) 50 | os.Exit(1) 51 | } 52 | }() 53 | err = t.Authenticate(0) 54 | if err != nil { 55 | fmt.Fprintf(os.Stderr, "authenticate: %v\n", err) 56 | os.Exit(1) 57 | } 58 | fmt.Println("authentication succeeded!") 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.23.x, 1.24.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Update system 16 | run: | 17 | sudo apt-get update -y 18 | sudo apt-get dist-upgrade -y 19 | - name: Install PAM with debug symbols 20 | run: | 21 | sudo apt-get install ubuntu-dbgsym-keyring -y 22 | echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse 23 | deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse 24 | deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | \ 25 | sudo tee -a /etc/apt/sources.list.d/ddebs.list 26 | sudo apt-get update -y 27 | sudo apt-get install -y libpam-dev libpam-modules-dbgsym libpam0*-dbgsym 28 | - name: Add a test user 29 | run: sudo useradd -d /tmp/test -p '$1$Qd8H95T5$RYSZQeoFbEB.gS19zS99A0' -s /bin/false test 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | - name: Test 33 | run: sudo go test -v -cover -coverprofile=coverage.out ./... 34 | - name: Upload coverage reports to Codecov 35 | uses: codecov/codecov-action@v3 36 | env: 37 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 38 | -------------------------------------------------------------------------------- /transaction.c: -------------------------------------------------------------------------------- 1 | #include "_cgo_export.h" 2 | #include 3 | #include 4 | #include 5 | 6 | #if defined(__sun) && !defined(__illumos__) 7 | #define PAM_CONST 8 | #else 9 | #define PAM_CONST const 10 | #endif 11 | 12 | int cb_pam_conv(int num_msg, PAM_CONST struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) 13 | { 14 | if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) 15 | return PAM_CONV_ERR; 16 | 17 | *resp = calloc(num_msg, sizeof **resp); 18 | if (!*resp) 19 | return PAM_BUF_ERR; 20 | 21 | for (size_t i = 0; i < num_msg; ++i) { 22 | struct cbPAMConv_return result = cbPAMConv(msg[i]->msg_style, (char *)msg[i]->msg, (uintptr_t)appdata_ptr); 23 | if (result.r1 != PAM_SUCCESS) 24 | goto error; 25 | 26 | (*resp)[i].resp = result.r0; 27 | } 28 | 29 | return PAM_SUCCESS; 30 | error: 31 | for (size_t i = 0; i < num_msg; ++i) { 32 | if ((*resp)[i].resp) { 33 | memset((*resp)[i].resp, 0, strlen((*resp)[i].resp)); 34 | free((*resp)[i].resp); 35 | } 36 | } 37 | 38 | memset(*resp, 0, num_msg * sizeof *resp); 39 | free(*resp); 40 | *resp = NULL; 41 | return PAM_CONV_ERR; 42 | } 43 | 44 | void init_pam_conv(struct pam_conv *conv, uintptr_t appdata) 45 | { 46 | conv->conv = cb_pam_conv; 47 | conv->appdata_ptr = (void *)appdata; 48 | } 49 | 50 | int pam_start_confdir_wrapper(pam_start_confdir_fn fn, const char *service_name, const char *user, 51 | const struct pam_conv *pam_conversation, const char *confdir, pam_handle_t **pamh) 52 | { 53 | return (fn)(service_name, user, pam_conversation, confdir, pamh); 54 | } 55 | -------------------------------------------------------------------------------- /transaction_linux_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package pam 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func Test_LinuxError(t *testing.T) { 10 | t.Parallel() 11 | if !CheckPamHasStartConfdir() { 12 | t.Skip("this requires PAM with Conf dir support") 13 | } 14 | 15 | statuses := map[string]error{ 16 | "bad_item": ErrBadItem, 17 | "conv_again": ErrConvAgain, 18 | "incomplete": ErrIncomplete, 19 | } 20 | 21 | testError(t, statuses) 22 | } 23 | 24 | func TestFailure_001(t *testing.T) { 25 | t.Parallel() 26 | tx := Transaction{} 27 | _, err := tx.GetEnvList() 28 | if err == nil { 29 | t.Fatalf("getenvlist #expected an error") 30 | } 31 | } 32 | 33 | func TestFailure_002(t *testing.T) { 34 | t.Parallel() 35 | tx := Transaction{} 36 | err := tx.PutEnv("") 37 | if err == nil { 38 | t.Fatalf("getenvlist #expected an error") 39 | } 40 | } 41 | 42 | func TestFailure_003(t *testing.T) { 43 | t.Parallel() 44 | tx := Transaction{} 45 | err := tx.CloseSession(0) 46 | if err == nil { 47 | t.Fatalf("getenvlist #expected an error") 48 | } 49 | } 50 | 51 | func TestFailure_004(t *testing.T) { 52 | t.Parallel() 53 | tx := Transaction{} 54 | err := tx.OpenSession(0) 55 | if err == nil { 56 | t.Fatalf("getenvlist #expected an error") 57 | } 58 | } 59 | 60 | func TestFailure_005(t *testing.T) { 61 | t.Parallel() 62 | tx := Transaction{} 63 | err := tx.ChangeAuthTok(0) 64 | if err == nil { 65 | t.Fatalf("getenvlist #expected an error") 66 | } 67 | } 68 | 69 | func TestFailure_006(t *testing.T) { 70 | t.Parallel() 71 | tx := Transaction{} 72 | err := tx.AcctMgmt(0) 73 | if err == nil { 74 | t.Fatalf("getenvlist #expected an error") 75 | } 76 | } 77 | 78 | func TestFailure_007(t *testing.T) { 79 | t.Parallel() 80 | tx := Transaction{} 81 | err := tx.SetCred(0) 82 | if err == nil { 83 | t.Fatalf("getenvlist #expected an error") 84 | } 85 | } 86 | 87 | func TestFailure_008(t *testing.T) { 88 | t.Parallel() 89 | tx := Transaction{} 90 | err := tx.SetItem(User, "test") 91 | if err == nil { 92 | t.Fatalf("getenvlist #expected an error") 93 | } 94 | } 95 | 96 | func TestFailure_009(t *testing.T) { 97 | t.Parallel() 98 | tx := Transaction{} 99 | _, err := tx.GetItem(User) 100 | if err == nil { 101 | t.Fatalf("getenvlist #expected an error") 102 | } 103 | } 104 | 105 | func TestFailure_010(t *testing.T) { 106 | t.Parallel() 107 | tx := Transaction{} 108 | err := tx.End() 109 | if err != nil { 110 | t.Fatalf("end #unexpected error %v", err) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # clang-format configuration file. For more information, see: 2 | # 3 | # https://clang.llvm.org/docs/ClangFormat.html 4 | # https://clang.llvm.org/docs/ClangFormatStyleOptions.html 5 | # 6 | --- 7 | AccessModifierOffset: -4 8 | AlignAfterOpenBracket: Align 9 | AlignConsecutiveAssignments: false 10 | AlignConsecutiveDeclarations: false 11 | AlignEscapedNewlines: Left 12 | AlignOperands: true 13 | AlignTrailingComments: false 14 | AllowAllParametersOfDeclarationOnNextLine: false 15 | AllowShortBlocksOnASingleLine: false 16 | AllowShortCaseLabelsOnASingleLine: false 17 | AllowShortFunctionsOnASingleLine: None 18 | AllowShortIfStatementsOnASingleLine: false 19 | AllowShortLoopsOnASingleLine: false 20 | AlwaysBreakAfterDefinitionReturnType: None 21 | AlwaysBreakAfterReturnType: None 22 | AlwaysBreakBeforeMultilineStrings: false 23 | AlwaysBreakTemplateDeclarations: false 24 | BinPackArguments: true 25 | BinPackParameters: true 26 | BraceWrapping: 27 | AfterClass: false 28 | AfterControlStatement: false 29 | AfterEnum: false 30 | AfterFunction: true 31 | AfterNamespace: true 32 | AfterObjCDeclaration: false 33 | AfterStruct: false 34 | AfterUnion: false 35 | AfterExternBlock: false 36 | BeforeCatch: false 37 | BeforeElse: false 38 | IndentBraces: false 39 | SplitEmptyFunction: true 40 | SplitEmptyRecord: true 41 | SplitEmptyNamespace: true 42 | BreakBeforeBinaryOperators: None 43 | BreakBeforeBraces: Custom 44 | BreakBeforeInheritanceComma: false 45 | BreakBeforeTernaryOperators: true 46 | BreakConstructorInitializersBeforeComma: false 47 | BreakConstructorInitializers: BeforeComma 48 | BreakStringLiterals: false 49 | ColumnLimit: 120 50 | CompactNamespaces: false 51 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 52 | ConstructorInitializerIndentWidth: 8 53 | ContinuationIndentWidth: 8 54 | DerivePointerAlignment: false 55 | DisableFormat: false 56 | ExperimentalAutoDetectBinPacking: false 57 | FixNamespaceComments: false 58 | IncludeBlocks: Regroup 59 | IncludeCategories: 60 | - Regex: '^"(allez)/' 61 | Priority: 2 62 | SortPriority: 2 63 | CaseSensitive: true 64 | - Regex: '.*' 65 | Priority: 1 66 | SortPriority: 0 67 | IndentCaseLabels: false 68 | IndentGotoLabels: false 69 | IndentPPDirectives: None 70 | IndentWidth: 8 71 | IndentWrappedFunctionNames: false 72 | KeepEmptyLinesAtTheStartOfBlocks: false 73 | MacroBlockBegin: '' 74 | MacroBlockEnd: '' 75 | MaxEmptyLinesToKeep: 1 76 | NamespaceIndentation: None 77 | ObjCBinPackProtocolList: Auto 78 | ObjCBlockIndentWidth: 8 79 | ObjCSpaceAfterProperty: true 80 | ObjCSpaceBeforeProtocolList: true 81 | 82 | # Taken from git's rules 83 | PenaltyBreakAssignment: 10 84 | PenaltyBreakBeforeFirstCallParameter: 30 85 | PenaltyBreakComment: 10 86 | PenaltyBreakFirstLessLess: 0 87 | PenaltyBreakString: 10 88 | PenaltyExcessCharacter: 2 89 | PenaltyReturnTypeOnItsOwnLine: 60 90 | 91 | PointerAlignment: Right 92 | ReflowComments: false 93 | SortIncludes: true 94 | SortUsingDeclarations: false 95 | SpaceAfterCStyleCast: false 96 | SpaceAfterTemplateKeyword: true 97 | SpaceBeforeAssignmentOperators: true 98 | SpaceBeforeCtorInitializerColon: true 99 | SpaceBeforeInheritanceColon: true 100 | SpaceBeforeParens: ControlStatementsExceptForEachMacros 101 | SpaceBeforeRangeBasedForLoopColon: true 102 | SpaceInEmptyParentheses: false 103 | SpacesBeforeTrailingComments: 1 104 | SpacesInAngles: false 105 | SpacesInContainerLiterals: false 106 | SpacesInCStyleCastParentheses: false 107 | SpacesInParentheses: false 108 | SpacesInSquareBrackets: false 109 | TabWidth: 8 110 | UseTab: Always 111 | ... 112 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package pam 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | 8 | // Error represents a PAM error. 9 | type Error int 10 | 11 | // Various errors returned by PAM. 12 | const ( 13 | // OpenErr indicates a dlopen() failure when dynamically loading a 14 | // service module. 15 | ErrOpen Error = C.PAM_OPEN_ERR 16 | // ErrSymbol indicates a symbol not found. 17 | ErrSymbol Error = C.PAM_SYMBOL_ERR 18 | // ErrService indicates a error in service module. 19 | ErrService Error = C.PAM_SERVICE_ERR 20 | // ErrSystem indicates a system error. 21 | ErrSystem Error = C.PAM_SYSTEM_ERR 22 | // ErrBuf indicates a memory buffer error. 23 | ErrBuf Error = C.PAM_BUF_ERR 24 | // ErrPermDenied indicates a permission denied. 25 | ErrPermDenied Error = C.PAM_PERM_DENIED 26 | // ErrAuth indicates a authentication failure. 27 | ErrAuth Error = C.PAM_AUTH_ERR 28 | // ErrCredInsufficient indicates a can not access authentication data due to 29 | // insufficient credentials. 30 | ErrCredInsufficient Error = C.PAM_CRED_INSUFFICIENT 31 | // ErrAuthinfoUnavail indicates that the underlying authentication service 32 | // can not retrieve authentication information. 33 | ErrAuthinfoUnavail Error = C.PAM_AUTHINFO_UNAVAIL 34 | // ErrUserUnknown indicates a user not known to the underlying authentication 35 | // module. 36 | ErrUserUnknown Error = C.PAM_USER_UNKNOWN 37 | // ErrMaxtries indicates that an authentication service has maintained a retry 38 | // count which has been reached. No further retries should be attempted. 39 | ErrMaxtries Error = C.PAM_MAXTRIES 40 | // ErrNewAuthtokReqd indicates a new authentication token required. This is 41 | // normally returned if the machine security policies require that the 42 | // password should be changed because the password is nil or it has aged. 43 | ErrNewAuthtokReqd Error = C.PAM_NEW_AUTHTOK_REQD 44 | // ErrAcctExpired indicates that an user account has expired. 45 | ErrAcctExpired Error = C.PAM_ACCT_EXPIRED 46 | // ErrSession indicates a can not make/remove an entry for the 47 | // specified session. 48 | ErrSession Error = C.PAM_SESSION_ERR 49 | // ErrCredUnavail indicates that an underlying authentication service can not 50 | // retrieve user credentials. 51 | ErrCredUnavail Error = C.PAM_CRED_UNAVAIL 52 | // ErrCredExpired indicates that an user credentials expired. 53 | ErrCredExpired Error = C.PAM_CRED_EXPIRED 54 | // ErrCred indicates a failure setting user credentials. 55 | ErrCred Error = C.PAM_CRED_ERR 56 | // ErrNoModuleData indicates a no module specific data is present. 57 | ErrNoModuleData Error = C.PAM_NO_MODULE_DATA 58 | // ErrConv indicates a conversation error. 59 | ErrConv Error = C.PAM_CONV_ERR 60 | // ErrAuthtokErr indicates an authentication token manipulation error. 61 | ErrAuthtok Error = C.PAM_AUTHTOK_ERR 62 | // ErrAuthtokRecoveryErr indicates an authentication information cannot 63 | // be recovered. 64 | ErrAuthtokRecovery Error = C.PAM_AUTHTOK_RECOVERY_ERR 65 | // ErrAuthtokLockBusy indicates am authentication token lock busy. 66 | ErrAuthtokLockBusy Error = C.PAM_AUTHTOK_LOCK_BUSY 67 | // ErrAuthtokDisableAging indicates an authentication token aging disabled. 68 | ErrAuthtokDisableAging Error = C.PAM_AUTHTOK_DISABLE_AGING 69 | // ErrTryAgain indicates a preliminary check by password service. 70 | ErrTryAgain Error = C.PAM_TRY_AGAIN 71 | // ErrIgnore indicates to ignore underlying account module regardless of 72 | // whether the control flag is required, optional, or sufficient. 73 | ErrIgnore Error = C.PAM_IGNORE 74 | // ErrAbort indicates a critical error (module fail now request). 75 | ErrAbort Error = C.PAM_ABORT 76 | // ErrAuthtokExpired indicates an user's authentication token has expired. 77 | ErrAuthtokExpired Error = C.PAM_AUTHTOK_EXPIRED 78 | // ErrModuleUnknown indicates a module is not known. 79 | ErrModuleUnknown Error = C.PAM_MODULE_UNKNOWN 80 | ) 81 | 82 | // Error returns the error message for the given status. 83 | func (status Error) Error() string { 84 | return C.GoString(C.pam_strerror(nil, C.int(status))) 85 | } 86 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | // Package pam provides a wrapper for the PAM application API. 2 | package pam 3 | 4 | /* 5 | #cgo CFLAGS: -Wall -Wno-unused-variable -std=c99 6 | #cgo LDFLAGS: -ldl -lpam 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #ifdef PAM_BINARY_PROMPT 14 | #define BINARY_PROMPT_IS_SUPPORTED 1 15 | #else 16 | #include 17 | #define PAM_BINARY_PROMPT INT_MAX 18 | #define BINARY_PROMPT_IS_SUPPORTED 0 19 | #endif 20 | 21 | void init_pam_conv(struct pam_conv *conv, uintptr_t); 22 | 23 | typedef int (*pam_start_confdir_fn)(const char *service_name, const char *user, const struct pam_conv *pam_conversation, const char *confdir, pam_handle_t **pamh); 24 | 25 | int pam_start_confdir_wrapper(pam_start_confdir_fn fn, const char *service_name, const char *user, const struct pam_conv *pam_conversation, const char *confdir, pam_handle_t **pamh); 26 | */ 27 | import "C" 28 | 29 | import ( 30 | "fmt" 31 | "runtime/cgo" 32 | "strings" 33 | "sync" 34 | "sync/atomic" 35 | "unsafe" 36 | ) 37 | 38 | // success indicates a successful function return. 39 | const success = C.PAM_SUCCESS 40 | 41 | // Style is the type of message that the conversation handler should display. 42 | type Style int 43 | 44 | // Coversation handler style types. 45 | const ( 46 | // PromptEchoOff indicates the conversation handler should obtain a 47 | // string without echoing any text. 48 | PromptEchoOff Style = C.PAM_PROMPT_ECHO_OFF 49 | // PromptEchoOn indicates the conversation handler should obtain a 50 | // string while echoing text. 51 | PromptEchoOn Style = C.PAM_PROMPT_ECHO_ON 52 | // ErrorMsg indicates the conversation handler should display an 53 | // error message. 54 | ErrorMsg Style = C.PAM_ERROR_MSG 55 | // TextInfo indicates the conversation handler should display some 56 | // text. 57 | TextInfo Style = C.PAM_TEXT_INFO 58 | // BinaryPrompt indicates the conversation handler that should 59 | // implement the private binary protocol. 60 | BinaryPrompt Style = C.PAM_BINARY_PROMPT 61 | ) 62 | 63 | // ConversationHandler is an interface for objects that can be used as 64 | // conversation callbacks during PAM authentication. 65 | type ConversationHandler interface { 66 | // RespondPAM receives a message style and a message string. If the 67 | // message Style is PromptEchoOff or PromptEchoOn then the function 68 | // should return a response string. 69 | RespondPAM(Style, string) (string, error) 70 | } 71 | 72 | // BinaryPointer exposes the type used for the data in a binary conversation. 73 | // It represents a pointer to data that is produced by the module and must be 74 | // parsed depending on the protocol in use. 75 | type BinaryPointer unsafe.Pointer 76 | 77 | // BinaryConversationHandler is an interface for objects that can be used as 78 | // conversation callbacks during PAM authentication if binary protocol is going 79 | // to be supported. 80 | type BinaryConversationHandler interface { 81 | ConversationHandler 82 | // RespondPAMBinary receives a pointer to the binary message. It's up to 83 | // the receiver to parse it according to the protocol specifications. 84 | // The function can return a byte array that will passed as pointer back 85 | // to the module. 86 | RespondPAMBinary(BinaryPointer) ([]byte, error) 87 | } 88 | 89 | // ConversationFunc is an adapter to allow the use of ordinary functions as 90 | // conversation callbacks. 91 | type ConversationFunc func(Style, string) (string, error) 92 | 93 | // RespondPAM is a conversation callback adapter. 94 | func (f ConversationFunc) RespondPAM(s Style, msg string) (string, error) { 95 | return f(s, msg) 96 | } 97 | 98 | // cbPAMConv is a wrapper for the conversation callback function. 99 | // 100 | //export cbPAMConv 101 | func cbPAMConv(s C.int, msg *C.char, c C.uintptr_t) (*C.char, C.int) { 102 | var r string 103 | var err error 104 | v := cgo.Handle(c).Value() 105 | style := Style(s) 106 | var handler ConversationHandler 107 | switch cb := v.(type) { 108 | case BinaryConversationHandler: 109 | if style == BinaryPrompt { 110 | bytes, err := cb.RespondPAMBinary(BinaryPointer(msg)) 111 | if err != nil { 112 | return nil, C.int(ErrConv) 113 | } 114 | return (*C.char)(C.CBytes(bytes)), success 115 | } 116 | handler = cb 117 | case ConversationHandler: 118 | if style == BinaryPrompt { 119 | return nil, C.int(ErrConv) 120 | } 121 | handler = cb 122 | } 123 | if handler == nil { 124 | return nil, C.int(ErrConv) 125 | } 126 | r, err = handler.RespondPAM(style, C.GoString(msg)) 127 | if err != nil { 128 | return nil, C.int(ErrConv) 129 | } 130 | return C.CString(r), success 131 | } 132 | 133 | // Transaction is the application's handle for a PAM transaction. 134 | type Transaction struct { 135 | handle *C.pam_handle_t 136 | conv *C.struct_pam_conv 137 | lastStatus atomic.Int32 138 | c cgo.Handle 139 | } 140 | 141 | // End cleans up the PAM handle and deletes the callback function. 142 | // It must be called when done with the transaction. 143 | func (t *Transaction) End() error { 144 | handle := atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&t.handle)), nil) 145 | if handle == nil { 146 | return nil 147 | } 148 | 149 | defer t.c.Delete() 150 | return t.handlePamStatus(C.pam_end((*C.pam_handle_t)(handle), 151 | C.int(t.lastStatus.Load()))) 152 | } 153 | 154 | // handlePamStatus stores the last error returned by PAM and converts it to a 155 | // Go error. 156 | func (t *Transaction) handlePamStatus(cStatus C.int) error { 157 | t.lastStatus.Store(int32(cStatus)) 158 | if status := Error(cStatus); status != success { 159 | return status 160 | } 161 | return nil 162 | } 163 | 164 | // Start initiates a new PAM transaction. Service is treated identically to 165 | // how pam_start treats it internally. 166 | // 167 | // All application calls to PAM begin with Start*. The returned 168 | // transaction provides an interface to the remainder of the API. 169 | // 170 | // It's responsibility of the Transaction owner to release all the resources 171 | // allocated underneath by PAM by calling End() once done. 172 | // 173 | // It's not advised to End the transaction using a runtime.SetFinalizer unless 174 | // you're absolutely sure that your stack is multi-thread friendly (normally it 175 | // is not!) and using a LockOSThread/UnlockOSThread pair. 176 | func Start(service, user string, handler ConversationHandler) (*Transaction, error) { 177 | return start(service, user, handler, "") 178 | } 179 | 180 | // StartFunc registers the handler func as a conversation handler and starts 181 | // the transaction (see Start() documentation). 182 | func StartFunc(service, user string, handler func(Style, string) (string, error)) (*Transaction, error) { 183 | return start(service, user, ConversationFunc(handler), "") 184 | } 185 | 186 | // StartConfDir initiates a new PAM transaction. Service is treated identically to 187 | // how pam_start treats it internally. 188 | // confdir allows to define where all pam services are defined. This is used to provide 189 | // custom paths for tests. 190 | // 191 | // All application calls to PAM begin with Start*. The returned 192 | // transaction provides an interface to the remainder of the API. 193 | // 194 | // It's responsibility of the Transaction owner to release all the resources 195 | // allocated underneath by PAM by calling End() once done. 196 | // 197 | // It's not advised to End the transaction using a runtime.SetFinalizer unless 198 | // you're absolutely sure that your stack is multi-thread friendly (normally it 199 | // is not!) and using a LockOSThread/UnlockOSThread pair. 200 | func StartConfDir(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) { 201 | if !CheckPamHasStartConfdir() { 202 | return nil, fmt.Errorf( 203 | "%w: StartConfDir was used, but the pam version on the system is not recent enough", 204 | ErrSystem) 205 | } 206 | 207 | return start(service, user, handler, confDir) 208 | } 209 | 210 | func start(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) { 211 | switch handler.(type) { 212 | case BinaryConversationHandler: 213 | if !CheckPamHasBinaryProtocol() { 214 | return nil, fmt.Errorf("%w: BinaryConversationHandler was used, but it is not supported by this platform", 215 | ErrSystem) 216 | } 217 | } 218 | t := &Transaction{ 219 | conv: &C.struct_pam_conv{}, 220 | c: cgo.NewHandle(handler), 221 | } 222 | 223 | C.init_pam_conv(t.conv, C.uintptr_t(t.c)) 224 | s := C.CString(service) 225 | defer C.free(unsafe.Pointer(s)) 226 | var u *C.char 227 | if len(user) != 0 { 228 | u = C.CString(user) 229 | defer C.free(unsafe.Pointer(u)) 230 | } 231 | var err error 232 | if confDir == "" { 233 | err = t.handlePamStatus(C.pam_start(s, u, t.conv, &t.handle)) 234 | } else { 235 | c := C.CString(confDir) 236 | defer C.free(unsafe.Pointer(c)) 237 | err = t.handlePamStatus(C.pam_start_confdir_wrapper(pamStartConfdirPtr, s, u, t.conv, c, &t.handle)) 238 | } 239 | if err != nil { 240 | var _ = t.End() 241 | return nil, err 242 | } 243 | return t, nil 244 | } 245 | 246 | // Item is a an PAM information type. 247 | type Item int 248 | 249 | // PAM Item types. 250 | const ( 251 | // Service is the name which identifies the PAM stack. 252 | Service Item = C.PAM_SERVICE 253 | // User identifies the username identity used by a service. 254 | User Item = C.PAM_USER 255 | // Tty is the terminal name. 256 | Tty Item = C.PAM_TTY 257 | // Rhost is the requesting host name. 258 | Rhost Item = C.PAM_RHOST 259 | // Authtok is the currently active authentication token. 260 | Authtok Item = C.PAM_AUTHTOK 261 | // Oldauthtok is the old authentication token. 262 | Oldauthtok Item = C.PAM_OLDAUTHTOK 263 | // Ruser is the requesting user name. 264 | Ruser Item = C.PAM_RUSER 265 | // UserPrompt is the string use to prompt for a username. 266 | UserPrompt Item = C.PAM_USER_PROMPT 267 | ) 268 | 269 | // SetItem sets a PAM information item. 270 | func (t *Transaction) SetItem(i Item, item string) error { 271 | cs := unsafe.Pointer(C.CString(item)) 272 | defer C.free(cs) 273 | return t.handlePamStatus(C.pam_set_item(t.handle, C.int(i), cs)) 274 | } 275 | 276 | // GetItem retrieves a PAM information item. 277 | func (t *Transaction) GetItem(i Item) (string, error) { 278 | var s unsafe.Pointer 279 | err := t.handlePamStatus(C.pam_get_item(t.handle, C.int(i), &s)) 280 | if err != nil { 281 | return "", err 282 | } 283 | return C.GoString((*C.char)(s)), nil 284 | } 285 | 286 | // Flags are inputs to various PAM functions than be combined with a bitwise 287 | // or. Refer to the official PAM documentation for which flags are accepted 288 | // by which functions. 289 | type Flags int 290 | 291 | // PAM Flag types. 292 | const ( 293 | // Silent indicates that no messages should be emitted. 294 | Silent Flags = C.PAM_SILENT 295 | // DisallowNullAuthtok indicates that authorization should fail 296 | // if the user does not have a registered authentication token. 297 | DisallowNullAuthtok Flags = C.PAM_DISALLOW_NULL_AUTHTOK 298 | // EstablishCred indicates that credentials should be established 299 | // for the user. 300 | EstablishCred Flags = C.PAM_ESTABLISH_CRED 301 | // DeleteCred indicates that credentials should be deleted. 302 | DeleteCred Flags = C.PAM_DELETE_CRED 303 | // ReinitializeCred indicates that credentials should be fully 304 | // reinitialized. 305 | ReinitializeCred Flags = C.PAM_REINITIALIZE_CRED 306 | // RefreshCred indicates that the lifetime of existing credentials 307 | // should be extended. 308 | RefreshCred Flags = C.PAM_REFRESH_CRED 309 | // ChangeExpiredAuthtok indicates that the authentication token 310 | // should be changed if it has expired. 311 | ChangeExpiredAuthtok Flags = C.PAM_CHANGE_EXPIRED_AUTHTOK 312 | ) 313 | 314 | // Authenticate is used to authenticate the user. 315 | // 316 | // Valid flags: Silent, DisallowNullAuthtok. 317 | func (t *Transaction) Authenticate(f Flags) error { 318 | return t.handlePamStatus(C.pam_authenticate(t.handle, C.int(f))) 319 | } 320 | 321 | // SetCred is used to establish, maintain and delete the credentials of a 322 | // user. 323 | // 324 | // Valid flags: EstablishCred, DeleteCred, ReinitializeCred, RefreshCred. 325 | func (t *Transaction) SetCred(f Flags) error { 326 | return t.handlePamStatus(C.pam_setcred(t.handle, C.int(f))) 327 | } 328 | 329 | // AcctMgmt is used to determine if the user's account is valid. 330 | // 331 | // Valid flags: Silent, DisallowNullAuthtok. 332 | func (t *Transaction) AcctMgmt(f Flags) error { 333 | return t.handlePamStatus(C.pam_acct_mgmt(t.handle, C.int(f))) 334 | } 335 | 336 | // ChangeAuthTok is used to change the authentication token. 337 | // 338 | // Valid flags: Silent, ChangeExpiredAuthtok. 339 | func (t *Transaction) ChangeAuthTok(f Flags) error { 340 | return t.handlePamStatus(C.pam_chauthtok(t.handle, C.int(f))) 341 | } 342 | 343 | // OpenSession sets up a user session for an authenticated user. 344 | // 345 | // Valid flags: Silent. 346 | func (t *Transaction) OpenSession(f Flags) error { 347 | return t.handlePamStatus(C.pam_open_session(t.handle, C.int(f))) 348 | } 349 | 350 | // CloseSession closes a previously opened session. 351 | // 352 | // Valid flags: Silent. 353 | func (t *Transaction) CloseSession(f Flags) error { 354 | return t.handlePamStatus(C.pam_close_session(t.handle, C.int(f))) 355 | } 356 | 357 | // PutEnv adds or changes the value of PAM environment variables. 358 | // 359 | // NAME=value will set a variable to a value. 360 | // NAME= will set a variable to an empty value. 361 | // NAME (without an "=") will delete a variable. 362 | func (t *Transaction) PutEnv(nameval string) error { 363 | cs := C.CString(nameval) 364 | defer C.free(unsafe.Pointer(cs)) 365 | return t.handlePamStatus(C.pam_putenv(t.handle, cs)) 366 | } 367 | 368 | // GetEnv is used to retrieve a PAM environment variable. 369 | func (t *Transaction) GetEnv(name string) string { 370 | cs := C.CString(name) 371 | defer C.free(unsafe.Pointer(cs)) 372 | value := C.pam_getenv(t.handle, cs) 373 | if value == nil { 374 | return "" 375 | } 376 | return C.GoString(value) 377 | } 378 | 379 | func next(p **C.char) **C.char { 380 | return (**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(p))) 381 | } 382 | 383 | // GetEnvList returns a copy of the PAM environment as a map. 384 | func (t *Transaction) GetEnvList() (map[string]string, error) { 385 | env := make(map[string]string) 386 | p := C.pam_getenvlist(t.handle) 387 | if p == nil { 388 | t.lastStatus.Store(int32(ErrBuf)) 389 | return nil, ErrBuf 390 | } 391 | t.lastStatus.Store(success) 392 | for q := p; *q != nil; q = next(q) { 393 | chunks := strings.SplitN(C.GoString(*q), "=", 2) 394 | if len(chunks) == 2 { 395 | env[chunks[0]] = chunks[1] 396 | } 397 | C.free(unsafe.Pointer(*q)) 398 | } 399 | C.free(unsafe.Pointer(p)) 400 | return env, nil 401 | } 402 | 403 | var once sync.Once 404 | var pamStartConfdirPtr C.pam_start_confdir_fn 405 | 406 | // CheckPamHasStartConfdir reports whether PAM supports pam_system_confdir. 407 | func CheckPamHasStartConfdir() bool { 408 | once.Do(func() { 409 | pamStartConfdirPtr = C.pam_start_confdir_fn(C.dlsym(C.RTLD_NEXT, C.CString("pam_start_confdir"))) 410 | }) 411 | return pamStartConfdirPtr != nil 412 | } 413 | 414 | // CheckPamHasBinaryProtocol reports whether PAM supports PAM_BINARY_PROMPT. 415 | func CheckPamHasBinaryProtocol() bool { 416 | return C.BINARY_PROMPT_IS_SUPPORTED != 0 417 | } 418 | -------------------------------------------------------------------------------- /transaction_test.go: -------------------------------------------------------------------------------- 1 | package pam 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/user" 8 | "path/filepath" 9 | "runtime" 10 | "sync/atomic" 11 | "testing" 12 | "time" 13 | "unsafe" 14 | ) 15 | 16 | func maybeEndTransaction(t *testing.T, tx *Transaction) { 17 | t.Helper() 18 | 19 | if tx == nil { 20 | return 21 | } 22 | err := tx.End() 23 | if err != nil { 24 | t.Fatalf("end #error: %v", err) 25 | } 26 | } 27 | 28 | func ensureTransactionEnds(t *testing.T, tx *Transaction) { 29 | t.Helper() 30 | 31 | runtime.SetFinalizer(tx, func(tx *Transaction) { 32 | // #nosec:G103 - the pointer conversion is checked. 33 | handle := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&tx.handle))) 34 | if handle == nil { 35 | return 36 | } 37 | t.Fatalf("transaction has not been finalized") 38 | }) 39 | } 40 | 41 | func TestPAM_001(t *testing.T) { 42 | u, _ := user.Current() 43 | if u.Uid != "0" { 44 | t.Skip("run this test as root") 45 | } 46 | p := "secret" 47 | tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) { 48 | return p, nil 49 | }) 50 | defer maybeEndTransaction(t, tx) 51 | if err != nil { 52 | t.Fatalf("start #error: %v", err) 53 | } 54 | ensureTransactionEnds(t, tx) 55 | err = tx.Authenticate(0) 56 | if err != nil { 57 | t.Fatalf("authenticate #error: %v", err) 58 | } 59 | err = tx.AcctMgmt(Silent) 60 | if err != nil { 61 | t.Fatalf("acct_mgmt #error: %v", err) 62 | } 63 | err = tx.SetCred(Silent | EstablishCred) 64 | if err != nil { 65 | t.Fatalf("setcred #error: %v", err) 66 | } 67 | } 68 | 69 | func TestPAM_002(t *testing.T) { 70 | u, _ := user.Current() 71 | if u.Uid != "0" { 72 | t.Skip("run this test as root") 73 | } 74 | tx, err := StartFunc("", "", func(s Style, msg string) (string, error) { 75 | switch s { 76 | case PromptEchoOn: 77 | return "test", nil 78 | case PromptEchoOff: 79 | return "secret", nil 80 | } 81 | return "", errors.New("unexpected") 82 | }) 83 | defer maybeEndTransaction(t, tx) 84 | if err != nil { 85 | t.Fatalf("start #error: %v", err) 86 | } 87 | ensureTransactionEnds(t, tx) 88 | err = tx.Authenticate(0) 89 | if err != nil { 90 | t.Fatalf("authenticate #error: %v", err) 91 | } 92 | } 93 | 94 | type Credentials struct { 95 | User string 96 | Password string 97 | } 98 | 99 | func (c Credentials) RespondPAM(s Style, msg string) (string, error) { 100 | switch s { 101 | case PromptEchoOn: 102 | return c.User, nil 103 | case PromptEchoOff: 104 | return c.Password, nil 105 | } 106 | return "", errors.New("unexpected") 107 | } 108 | 109 | func TestPAM_003(t *testing.T) { 110 | u, _ := user.Current() 111 | if u.Uid != "0" { 112 | t.Skip("run this test as root") 113 | } 114 | c := Credentials{ 115 | User: "test", 116 | Password: "secret", 117 | } 118 | tx, err := Start("", "", c) 119 | defer maybeEndTransaction(t, tx) 120 | if err != nil { 121 | t.Fatalf("start #error: %v", err) 122 | } 123 | ensureTransactionEnds(t, tx) 124 | err = tx.Authenticate(0) 125 | if err != nil { 126 | t.Fatalf("authenticate #error: %v", err) 127 | } 128 | } 129 | 130 | func TestPAM_004(t *testing.T) { 131 | u, _ := user.Current() 132 | if u.Uid != "0" { 133 | t.Skip("run this test as root") 134 | } 135 | c := Credentials{ 136 | Password: "secret", 137 | } 138 | tx, err := Start("", "test", c) 139 | defer maybeEndTransaction(t, tx) 140 | if err != nil { 141 | t.Fatalf("start #error: %v", err) 142 | } 143 | ensureTransactionEnds(t, tx) 144 | err = tx.Authenticate(0) 145 | if err != nil { 146 | t.Fatalf("authenticate #error: %v", err) 147 | } 148 | } 149 | 150 | func TestPAM_005(t *testing.T) { 151 | u, _ := user.Current() 152 | if u.Uid != "0" { 153 | t.Skip("run this test as root") 154 | } 155 | tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) { 156 | return "secret", nil 157 | }) 158 | defer maybeEndTransaction(t, tx) 159 | if err != nil { 160 | t.Fatalf("start #error: %v", err) 161 | } 162 | ensureTransactionEnds(t, tx) 163 | service, err := tx.GetItem(Service) 164 | if err != nil { 165 | t.Fatalf("GetItem #error: %v", err) 166 | } 167 | if service != "passwd" { 168 | t.Fatalf("Unexpected service: %v", service) 169 | } 170 | err = tx.ChangeAuthTok(Silent) 171 | if err != nil { 172 | t.Fatalf("chauthtok #error: %v", err) 173 | } 174 | } 175 | 176 | func TestPAM_006(t *testing.T) { 177 | u, _ := user.Current() 178 | if u.Uid != "0" { 179 | t.Skip("run this test as root") 180 | } 181 | tx, err := StartFunc("passwd", u.Username, func(s Style, msg string) (string, error) { 182 | return "secret", nil 183 | }) 184 | defer maybeEndTransaction(t, tx) 185 | if err != nil { 186 | t.Fatalf("start #error: %v", err) 187 | } 188 | ensureTransactionEnds(t, tx) 189 | err = tx.OpenSession(Silent) 190 | if err != nil { 191 | t.Fatalf("open_session #error: %v", err) 192 | } 193 | err = tx.CloseSession(Silent) 194 | if err != nil { 195 | t.Fatalf("close_session #error: %v", err) 196 | } 197 | } 198 | 199 | func TestPAM_007(t *testing.T) { 200 | u, _ := user.Current() 201 | if u.Uid != "0" { 202 | t.Skip("run this test as root") 203 | } 204 | tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) { 205 | return "", errors.New("Sorry, it didn't work") 206 | }) 207 | defer maybeEndTransaction(t, tx) 208 | if err != nil { 209 | t.Fatalf("start #error: %v", err) 210 | } 211 | ensureTransactionEnds(t, tx) 212 | err = tx.Authenticate(0) 213 | if err == nil { 214 | t.Fatalf("authenticate #expected an error") 215 | } 216 | s := err.Error() 217 | if len(s) == 0 { 218 | t.Fatalf("error #expected an error message") 219 | } 220 | if !errors.Is(err, ErrAuth) { 221 | t.Fatalf("error #unexpected error %v", err) 222 | } 223 | } 224 | 225 | func TestPAM_ConfDir(t *testing.T) { 226 | u, _ := user.Current() 227 | c := Credentials{ 228 | // the custom service always permits even with wrong password. 229 | Password: "wrongsecret", 230 | } 231 | tx, err := StartConfDir("permit-service", u.Username, c, "test-services") 232 | defer func() { 233 | if tx != nil { 234 | _ = tx.End() 235 | } 236 | }() 237 | if !CheckPamHasStartConfdir() { 238 | if err == nil { 239 | t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) 240 | } 241 | // nothing else we do, we don't support it. 242 | return 243 | } 244 | service, err := tx.GetItem(Service) 245 | if err != nil { 246 | t.Fatalf("GetItem #error: %v", err) 247 | } 248 | if service != "permit-service" { 249 | t.Fatalf("Unexpected service: %v", service) 250 | } 251 | if err != nil { 252 | t.Fatalf("start #error: %v", err) 253 | } 254 | err = tx.Authenticate(0) 255 | if err != nil { 256 | t.Fatalf("authenticate #error: %v", err) 257 | } 258 | } 259 | 260 | func TestPAM_ConfDir_FailNoServiceOrUnsupported(t *testing.T) { 261 | if !CheckPamHasStartConfdir() { 262 | t.Skip("this requires PAM with Conf dir support") 263 | } 264 | u, _ := user.Current() 265 | c := Credentials{ 266 | Password: "secret", 267 | } 268 | tx, err := StartConfDir("does-not-exists", u.Username, c, ".") 269 | if err == nil { 270 | t.Fatalf("authenticate #expected an error") 271 | } 272 | if tx != nil { 273 | t.Fatalf("authenticate #unexpected transaction") 274 | } 275 | s := err.Error() 276 | if len(s) == 0 { 277 | t.Fatalf("error #expected an error message") 278 | } 279 | var pamErr Error 280 | if !errors.As(err, &pamErr) { 281 | t.Fatalf("error #unexpected type: %#v", err) 282 | } 283 | if pamErr != ErrAbort { 284 | t.Fatalf("error #unexpected status: %v", pamErr) 285 | } 286 | } 287 | 288 | func TestPAM_ConfDir_InfoMessage(t *testing.T) { 289 | u, _ := user.Current() 290 | var infoText string 291 | tx, err := StartConfDir("echo-service", u.Username, 292 | ConversationFunc(func(s Style, msg string) (string, error) { 293 | switch s { 294 | case TextInfo: 295 | infoText = msg 296 | return "", nil 297 | } 298 | return "", errors.New("unexpected") 299 | }), "test-services") 300 | defer maybeEndTransaction(t, tx) 301 | if !CheckPamHasStartConfdir() { 302 | if err == nil { 303 | t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) 304 | } 305 | // nothing else we do, we don't support it. 306 | return 307 | } 308 | if err != nil { 309 | t.Fatalf("start #error: %v", err) 310 | } 311 | ensureTransactionEnds(t, tx) 312 | service, err := tx.GetItem(Service) 313 | if err != nil { 314 | t.Fatalf("GetItem #error: %v", err) 315 | } 316 | if service != "echo-service" { 317 | t.Fatalf("Unexpected service: %v", service) 318 | } 319 | err = tx.Authenticate(0) 320 | if err != nil { 321 | t.Fatalf("authenticate #error: %v", err) 322 | } 323 | if infoText != "This is an info message for user "+u.Username+" on echo-service" { 324 | t.Fatalf("Unexpected info message: %v", infoText) 325 | } 326 | } 327 | 328 | func TestPAM_ConfDir_Deny(t *testing.T) { 329 | if !CheckPamHasStartConfdir() { 330 | t.Skip("this requires PAM with Conf dir support") 331 | } 332 | u, _ := user.Current() 333 | tx, err := StartConfDir("deny-service", u.Username, Credentials{}, "test-services") 334 | defer maybeEndTransaction(t, tx) 335 | if err != nil { 336 | t.Fatalf("start #error: %v", err) 337 | } 338 | ensureTransactionEnds(t, tx) 339 | service, err := tx.GetItem(Service) 340 | if err != nil { 341 | t.Fatalf("GetItem #error: %v", err) 342 | } 343 | if service != "deny-service" { 344 | t.Fatalf("Unexpected service: %v", service) 345 | } 346 | err = tx.Authenticate(0) 347 | if err == nil { 348 | t.Fatalf("authenticate #expected an error") 349 | } 350 | s := err.Error() 351 | if len(s) == 0 { 352 | t.Fatalf("error #expected an error message") 353 | } 354 | if !errors.Is(err, ErrAuth) { 355 | t.Fatalf("error #unexpected error %v", err) 356 | } 357 | } 358 | 359 | func TestPAM_ConfDir_PromptForUserName(t *testing.T) { 360 | c := Credentials{ 361 | User: "testuser", 362 | // the custom service only cares about correct user name. 363 | Password: "wrongsecret", 364 | } 365 | tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services") 366 | defer maybeEndTransaction(t, tx) 367 | if !CheckPamHasStartConfdir() { 368 | if err == nil { 369 | t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) 370 | } 371 | // nothing else we do, we don't support it. 372 | return 373 | } 374 | if err != nil { 375 | t.Fatalf("start #error: %v", err) 376 | } 377 | ensureTransactionEnds(t, tx) 378 | err = tx.Authenticate(0) 379 | if err != nil { 380 | t.Fatalf("authenticate #error: %v", err) 381 | } 382 | } 383 | 384 | func TestPAM_ConfDir_WrongUserName(t *testing.T) { 385 | c := Credentials{ 386 | User: "wronguser", 387 | Password: "wrongsecret", 388 | } 389 | tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services") 390 | defer maybeEndTransaction(t, tx) 391 | if !CheckPamHasStartConfdir() { 392 | if err == nil { 393 | t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err) 394 | } 395 | // nothing else we do, we don't support it. 396 | return 397 | } 398 | if err != nil { 399 | t.Fatalf("start #error: %v", err) 400 | } 401 | ensureTransactionEnds(t, tx) 402 | err = tx.Authenticate(0) 403 | if err == nil { 404 | t.Fatalf("authenticate #expected an error") 405 | } 406 | s := err.Error() 407 | if len(s) == 0 { 408 | t.Fatalf("error #expected an error message") 409 | } 410 | if !errors.Is(err, ErrAuth) { 411 | t.Fatalf("error #unexpected error %v", err) 412 | } 413 | } 414 | 415 | func TestItem(t *testing.T) { 416 | tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) { 417 | return "", nil 418 | }) 419 | defer maybeEndTransaction(t, tx) 420 | if err != nil { 421 | t.Fatalf("start #error: %v", err) 422 | } 423 | ensureTransactionEnds(t, tx) 424 | 425 | s, err := tx.GetItem(Service) 426 | if err != nil { 427 | t.Fatalf("getitem #error: %v", err) 428 | } 429 | if s != "passwd" { 430 | t.Fatalf("getitem #error: expected passwd, got %v", s) 431 | } 432 | 433 | s, err = tx.GetItem(User) 434 | if err != nil { 435 | t.Fatalf("getitem #error: %v", err) 436 | } 437 | if s != "test" { 438 | t.Fatalf("getitem #error: expected test, got %v", s) 439 | } 440 | 441 | err = tx.SetItem(User, "root") 442 | if err != nil { 443 | t.Fatalf("setitem #error: %v", err) 444 | } 445 | s, err = tx.GetItem(User) 446 | if err != nil { 447 | t.Fatalf("getitem #error: %v", err) 448 | } 449 | if s != "root" { 450 | t.Fatalf("getitem #error: expected root, got %v", s) 451 | } 452 | } 453 | 454 | func TestEnv(t *testing.T) { 455 | tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) { 456 | return "", nil 457 | }) 458 | defer maybeEndTransaction(t, tx) 459 | if err != nil { 460 | t.Fatalf("start #error: %v", err) 461 | } 462 | ensureTransactionEnds(t, tx) 463 | 464 | m, err := tx.GetEnvList() 465 | if err != nil { 466 | t.Fatalf("getenvlist #error: %v", err) 467 | } 468 | n := len(m) 469 | if n != 0 { 470 | t.Fatalf("putenv #error: expected 0 items, got %v", n) 471 | } 472 | 473 | vals := []string{ 474 | "VAL1=1", 475 | "VAL2=2", 476 | "VAL3=3", 477 | } 478 | for _, s := range vals { 479 | err = tx.PutEnv(s) 480 | if err != nil { 481 | t.Fatalf("putenv #error: %v", err) 482 | } 483 | } 484 | 485 | s := tx.GetEnv("VAL0") 486 | if s != "" { 487 | t.Fatalf("getenv #error: expected \"\", got %v", s) 488 | } 489 | 490 | s = tx.GetEnv("VAL1") 491 | if s != "1" { 492 | t.Fatalf("getenv #error: expected 1, got %v", s) 493 | } 494 | s = tx.GetEnv("VAL2") 495 | if s != "2" { 496 | t.Fatalf("getenv #error: expected 2, got %v", s) 497 | } 498 | s = tx.GetEnv("VAL3") 499 | if s != "3" { 500 | t.Fatalf("getenv #error: expected 3, got %v", s) 501 | } 502 | 503 | m, err = tx.GetEnvList() 504 | if err != nil { 505 | t.Fatalf("getenvlist #error: %v", err) 506 | } 507 | n = len(m) 508 | if n != 3 { 509 | t.Fatalf("getenvlist #error: expected 3 items, got %v", n) 510 | } 511 | if m["VAL1"] != "1" { 512 | t.Fatalf("getenvlist #error: expected 1, got %v", m["VAL1"]) 513 | } 514 | if m["VAL2"] != "2" { 515 | t.Fatalf("getenvlist #error: expected 2, got %v", m["VAL1"]) 516 | } 517 | if m["VAL3"] != "3" { 518 | t.Fatalf("getenvlist #error: expected 3, got %v", m["VAL1"]) 519 | } 520 | } 521 | 522 | func testError(t *testing.T, statuses map[string]error) { 523 | t.Helper() 524 | 525 | type Action int 526 | const ( 527 | account Action = iota + 1 528 | auth 529 | password 530 | session 531 | ) 532 | actions := map[string]Action{ 533 | "account": account, 534 | "auth": auth, 535 | "password": password, 536 | "session": session, 537 | } 538 | 539 | c := Credentials{} 540 | 541 | servicePath := t.TempDir() 542 | 543 | for ret, expected := range statuses { 544 | ret := ret 545 | expected := expected 546 | for actionName, action := range actions { 547 | actionName := actionName 548 | action := action 549 | t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) { 550 | t.Parallel() 551 | serviceName := ret + "-" + actionName 552 | serviceFile := filepath.Join(servicePath, serviceName) 553 | contents := fmt.Sprintf("%[1]s requisite pam_debug.so "+ 554 | "auth=%[2]s cred=%[2]s acct=%[2]s prechauthtok=%[2]s "+ 555 | "chauthtok=%[2]s open_session=%[2]s close_session=%[2]s\n"+ 556 | "%[1]s requisite pam_permit.so\n", actionName, ret) 557 | 558 | if err := os.WriteFile(serviceFile, 559 | []byte(contents), 0600); err != nil { 560 | t.Fatalf("can't create service file %v: %v", serviceFile, err) 561 | } 562 | 563 | tx, err := StartConfDir(serviceName, "user", c, servicePath) 564 | defer maybeEndTransaction(t, tx) 565 | if err != nil { 566 | t.Fatalf("start #error: %v", err) 567 | } 568 | ensureTransactionEnds(t, tx) 569 | 570 | switch action { 571 | case account: 572 | err = tx.AcctMgmt(0) 573 | case auth: 574 | err = tx.Authenticate(0) 575 | case password: 576 | err = tx.ChangeAuthTok(0) 577 | case session: 578 | err = tx.OpenSession(0) 579 | } 580 | 581 | if !errors.Is(err, expected) { 582 | t.Fatalf("error #unexpected status %#v vs %#v", err, 583 | expected) 584 | } 585 | 586 | if err != nil { 587 | var status Error 588 | if !errors.As(err, &status) || err.Error() != status.Error() { 589 | t.Fatalf("error #unexpected status %#v vs %#v", err.Error(), 590 | status.Error()) 591 | } 592 | } 593 | }) 594 | } 595 | } 596 | } 597 | 598 | func Test_Error(t *testing.T) { 599 | t.Parallel() 600 | if !CheckPamHasStartConfdir() { 601 | t.Skip("this requires PAM with Conf dir support") 602 | } 603 | 604 | statuses := map[string]error{ 605 | "success": nil, 606 | "open_err": ErrOpen, 607 | "symbol_err": ErrSymbol, 608 | "service_err": ErrService, 609 | "system_err": ErrSystem, 610 | "buf_err": ErrBuf, 611 | "perm_denied": ErrPermDenied, 612 | "auth_err": ErrAuth, 613 | "cred_insufficient": ErrCredInsufficient, 614 | "authinfo_unavail": ErrAuthinfoUnavail, 615 | "user_unknown": ErrUserUnknown, 616 | "maxtries": ErrMaxtries, 617 | "new_authtok_reqd": ErrNewAuthtokReqd, 618 | "acct_expired": ErrAcctExpired, 619 | "session_err": ErrSession, 620 | "cred_unavail": ErrCredUnavail, 621 | "cred_expired": ErrCredExpired, 622 | "cred_err": ErrCred, 623 | "no_module_data": ErrNoModuleData, 624 | "conv_err": ErrConv, 625 | "authtok_err": ErrAuthtok, 626 | "authtok_recover_err": ErrAuthtokRecovery, 627 | "authtok_lock_busy": ErrAuthtokLockBusy, 628 | "authtok_disable_aging": ErrAuthtokDisableAging, 629 | "try_again": ErrTryAgain, 630 | "ignore": nil, /* Ignore can't be returned */ 631 | "abort": ErrAbort, 632 | "authtok_expired": ErrAuthtokExpired, 633 | "module_unknown": ErrModuleUnknown, 634 | } 635 | 636 | testError(t, statuses) 637 | } 638 | 639 | func Test_Finalizer(t *testing.T) { 640 | if !CheckPamHasStartConfdir() { 641 | t.Skip("this requires PAM with Conf dir support") 642 | } 643 | 644 | func() { 645 | tx, err := StartConfDir("permit-service", "", nil, "test-services") 646 | defer maybeEndTransaction(t, tx) 647 | if err != nil { 648 | t.Fatalf("start #error: %v", err) 649 | } 650 | ensureTransactionEnds(t, tx) 651 | }() 652 | 653 | runtime.GC() 654 | // sleep to switch to finalizer goroutine 655 | time.Sleep(5 * time.Millisecond) 656 | } 657 | --------------------------------------------------------------------------------