aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCy Schubert <cy@FreeBSD.org>2025-04-17 02:13:41 +0000
committerCy Schubert <cy@FreeBSD.org>2025-05-27 16:20:06 +0000
commit24f0b4ca2d565cdbb4fe7839ff28320706bf2386 (patch)
treebc9ce87edb73f767f5580887d0fc8c643b9d7a49
-rw-r--r--.clang-format30
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/build.yaml44
-rw-r--r--LICENSE344
-rw-r--r--Makefile.am210
-rw-r--r--NEWS1215
-rw-r--r--README641
-rw-r--r--README.md665
-rw-r--r--TODO101
-rwxr-xr-xbootstrap13
-rw-r--r--ci/README.md13
-rw-r--r--ci/files/heimdal/heimdal-kdc9
-rw-r--r--ci/files/heimdal/kadmind.acl1
-rw-r--r--ci/files/heimdal/kdc.conf30
-rw-r--r--ci/files/heimdal/krb5.conf19
-rw-r--r--ci/files/heimdal/pki-mapping1
-rw-r--r--ci/files/mit/extensions.client19
-rw-r--r--ci/files/mit/extensions.kdc20
-rw-r--r--ci/files/mit/kadm5.acl1
-rw-r--r--ci/files/mit/kdc.conf19
-rw-r--r--ci/files/mit/krb5.conf19
-rwxr-xr-xci/install18
-rwxr-xr-xci/kdc-setup-heimdal105
-rwxr-xr-xci/kdc-setup-mit102
-rwxr-xr-xci/test44
-rw-r--r--configure.ac145
-rw-r--r--docs/docknot.yaml551
-rw-r--r--docs/pam_krb5.pod1056
-rw-r--r--m4/cc-flags.m4131
-rw-r--r--m4/clang.m428
-rw-r--r--m4/kadm5clnt.m4103
-rw-r--r--m4/krb5-config.m4104
-rw-r--r--m4/krb5-pkinit.m447
-rw-r--r--m4/krb5.m4384
-rw-r--r--m4/ld-version.m440
-rw-r--r--m4/lib-depends.m430
-rw-r--r--m4/lib-helper.m4149
-rw-r--r--m4/lib-pathname.m454
-rw-r--r--m4/pam-const.m453
-rw-r--r--module/account.c92
-rw-r--r--module/alt-auth.c240
-rw-r--r--module/auth.c1135
-rw-r--r--module/cache.c185
-rw-r--r--module/context.c177
-rw-r--r--module/fast.c288
-rw-r--r--module/internal.h261
-rw-r--r--module/options.c259
-rw-r--r--module/pam_krb5.map11
-rw-r--r--module/pam_krb5.sym6
-rw-r--r--module/password.c401
-rw-r--r--module/prompting.c481
-rw-r--r--module/public.c260
-rw-r--r--module/setcred.c474
-rw-r--r--module/support.c141
-rw-r--r--pam-util/args.c105
-rw-r--r--pam-util/args.h84
-rw-r--r--pam-util/logging.c345
-rw-r--r--pam-util/logging.h131
-rw-r--r--pam-util/options.c720
-rw-r--r--pam-util/options.h205
-rw-r--r--pam-util/vector.c289
-rw-r--r--pam-util/vector.h120
-rw-r--r--portable/asprintf.c84
-rw-r--r--portable/dummy.c33
-rw-r--r--portable/issetugid.c35
-rw-r--r--portable/kadmin.h82
-rw-r--r--portable/krb5-extra.c186
-rw-r--r--portable/krb5-profile.c237
-rw-r--r--portable/krb5.h248
-rw-r--r--portable/macros.h72
-rw-r--r--portable/mkstemp.c101
-rw-r--r--portable/pam.h129
-rw-r--r--portable/pam_syslog.c36
-rw-r--r--portable/pam_vsyslog.c63
-rw-r--r--portable/reallocarray.c64
-rw-r--r--portable/stdbool.h63
-rw-r--r--portable/strndup.c56
-rw-r--r--portable/system.h154
-rw-r--r--tests/README252
-rw-r--r--tests/TESTS46
-rw-r--r--tests/config/README70
-rw-r--r--tests/data/cppcheck.supp72
-rwxr-xr-xtests/data/generate-krb5-conf86
-rw-r--r--tests/data/krb5-pam.conf30
-rw-r--r--tests/data/krb5.conf30
-rw-r--r--tests/data/perl.conf19
-rw-r--r--tests/data/scripts/alt-auth/basic19
-rw-r--r--tests/data/scripts/alt-auth/basic-debug25
-rw-r--r--tests/data/scripts/alt-auth/fail19
-rw-r--r--tests/data/scripts/alt-auth/fail-debug28
-rw-r--r--tests/data/scripts/alt-auth/fallback25
-rw-r--r--tests/data/scripts/alt-auth/fallback-debug38
-rw-r--r--tests/data/scripts/alt-auth/fallback-realm25
-rw-r--r--tests/data/scripts/alt-auth/force19
-rw-r--r--tests/data/scripts/alt-auth/force-fail-debug26
-rw-r--r--tests/data/scripts/alt-auth/force-fallback25
-rw-r--r--tests/data/scripts/alt-auth/only19
-rw-r--r--tests/data/scripts/alt-auth/only-fail22
-rw-r--r--tests/data/scripts/alt-auth/username-map19
-rw-r--r--tests/data/scripts/alt-auth/username-map-prefix19
-rw-r--r--tests/data/scripts/bad-authtok/no-prompt25
-rw-r--r--tests/data/scripts/bad-authtok/try-first25
-rw-r--r--tests/data/scripts/bad-authtok/try-first-debug36
-rw-r--r--tests/data/scripts/bad-authtok/use-first22
-rw-r--r--tests/data/scripts/bad-authtok/use-first-debug33
-rw-r--r--tests/data/scripts/basic/force-first22
-rw-r--r--tests/data/scripts/basic/force-first-debug32
-rw-r--r--tests/data/scripts/basic/ignore-root16
-rw-r--r--tests/data/scripts/basic/ignore-root-debug24
-rw-r--r--tests/data/scripts/basic/minimum-uid13
-rw-r--r--tests/data/scripts/basic/minimum-uid-debug21
-rw-r--r--tests/data/scripts/basic/no-context17
-rw-r--r--tests/data/scripts/basic/no-context-debug47
-rw-r--r--tests/data/scripts/cache-cleanup/auth-only17
-rw-r--r--tests/data/scripts/cache/basic21
-rw-r--r--tests/data/scripts/cache/end-data-silent27
-rw-r--r--tests/data/scripts/cache/open-session20
-rw-r--r--tests/data/scripts/cache/search-k5login20
-rw-r--r--tests/data/scripts/cache/search-k5login-debug34
-rw-r--r--tests/data/scripts/expired/basic-heimdal31
-rw-r--r--tests/data/scripts/expired/basic-heimdal-debug44
-rw-r--r--tests/data/scripts/expired/basic-heimdal-flag-silent27
-rw-r--r--tests/data/scripts/expired/basic-heimdal-old30
-rw-r--r--tests/data/scripts/expired/basic-heimdal-old-debug43
-rw-r--r--tests/data/scripts/expired/basic-heimdal-silent27
-rw-r--r--tests/data/scripts/expired/basic-mit28
-rw-r--r--tests/data/scripts/expired/basic-mit-debug41
-rw-r--r--tests/data/scripts/expired/basic-mit-flag-silent27
-rw-r--r--tests/data/scripts/expired/basic-mit-silent27
-rw-r--r--tests/data/scripts/expired/defer-mit33
-rw-r--r--tests/data/scripts/expired/defer-mit-debug57
-rw-r--r--tests/data/scripts/expired/fail20
-rw-r--r--tests/data/scripts/expired/fail-debug24
-rw-r--r--tests/data/scripts/fast/anonymous17
-rw-r--r--tests/data/scripts/fast/anonymous-debug22
-rw-r--r--tests/data/scripts/fast/ccache17
-rw-r--r--tests/data/scripts/fast/ccache-debug21
-rw-r--r--tests/data/scripts/fast/no-ccache17
-rw-r--r--tests/data/scripts/fast/no-ccache-debug21
-rw-r--r--tests/data/scripts/long/password14
-rw-r--r--tests/data/scripts/long/password-debug20
-rw-r--r--tests/data/scripts/long/use-first14
-rw-r--r--tests/data/scripts/long/use-first-debug17
-rw-r--r--tests/data/scripts/no-cache/no-prompt25
-rw-r--r--tests/data/scripts/no-cache/no-prompt-try25
-rw-r--r--tests/data/scripts/no-cache/no-prompt-use25
-rw-r--r--tests/data/scripts/no-cache/prompt25
-rw-r--r--tests/data/scripts/no-cache/prompt-expose25
-rw-r--r--tests/data/scripts/no-cache/prompt-fail25
-rw-r--r--tests/data/scripts/no-cache/prompt-fail-debug36
-rw-r--r--tests/data/scripts/no-cache/prompt-principal26
-rw-r--r--tests/data/scripts/no-cache/try-first25
-rw-r--r--tests/data/scripts/no-cache/use-first25
-rw-r--r--tests/data/scripts/pam-user/no-update20
-rw-r--r--tests/data/scripts/pam-user/update20
-rw-r--r--tests/data/scripts/password/authtok21
-rw-r--r--tests/data/scripts/password/authtok-force18
-rw-r--r--tests/data/scripts/password/authtok-too-long17
-rw-r--r--tests/data/scripts/password/authtok-too-long-debug23
-rw-r--r--tests/data/scripts/password/banner23
-rw-r--r--tests/data/scripts/password/banner-expose23
-rw-r--r--tests/data/scripts/password/basic20
-rw-r--r--tests/data/scripts/password/basic-debug28
-rw-r--r--tests/data/scripts/password/expose23
-rw-r--r--tests/data/scripts/password/ignore18
-rw-r--r--tests/data/scripts/password/no-banner23
-rw-r--r--tests/data/scripts/password/no-banner-expose23
-rw-r--r--tests/data/scripts/password/prompt-principal24
-rw-r--r--tests/data/scripts/password/too-long15
-rw-r--r--tests/data/scripts/password/too-long-debug24
-rw-r--r--tests/data/scripts/pkinit/basic22
-rw-r--r--tests/data/scripts/pkinit/basic-debug30
-rw-r--r--tests/data/scripts/pkinit/no-use-pkinit18
-rw-r--r--tests/data/scripts/pkinit/pin-mit20
-rw-r--r--tests/data/scripts/pkinit/preauth-opt-mit17
-rw-r--r--tests/data/scripts/pkinit/prompt-try20
-rw-r--r--tests/data/scripts/pkinit/prompt-use20
-rw-r--r--tests/data/scripts/pkinit/try-pkinit17
-rw-r--r--tests/data/scripts/pkinit/try-pkinit-debug19
-rw-r--r--tests/data/scripts/pkinit/try-pkinit-debug-mit20
-rw-r--r--tests/data/scripts/realm/fail-bad-user-realm17
-rw-r--r--tests/data/scripts/realm/fail-no-realm17
-rw-r--r--tests/data/scripts/realm/fail-no-realm-debug21
-rw-r--r--tests/data/scripts/realm/fail-realm17
-rw-r--r--tests/data/scripts/realm/fail-user-realm18
-rw-r--r--tests/data/scripts/realm/pass-realm17
-rw-r--r--tests/data/scripts/realm/pass-user-realm17
-rw-r--r--tests/data/scripts/stacked/auth-only18
-rw-r--r--tests/data/scripts/stacked/basic22
-rw-r--r--tests/data/scripts/stacked/prompt25
-rw-r--r--tests/data/scripts/stacked/prompt-principal25
-rw-r--r--tests/data/scripts/stacked/try-first22
-rw-r--r--tests/data/scripts/stacked/use-first22
-rw-r--r--tests/data/scripts/trace/supported58
-rw-r--r--tests/data/scripts/trace/unsupported52
-rw-r--r--tests/data/valgrind.supp242
-rwxr-xr-xtests/docs/pod-spelling-t55
-rwxr-xr-xtests/docs/pod-t56
-rwxr-xr-xtests/docs/spdx-license-t149
-rw-r--r--tests/fakepam/README276
-rw-r--r--tests/fakepam/config.c766
-rw-r--r--tests/fakepam/data.c356
-rw-r--r--tests/fakepam/general.c151
-rw-r--r--tests/fakepam/internal.h119
-rw-r--r--tests/fakepam/kuserok.c119
-rw-r--r--tests/fakepam/logging.c183
-rw-r--r--tests/fakepam/pam.h101
-rw-r--r--tests/fakepam/script.c411
-rw-r--r--tests/fakepam/script.h82
-rw-r--r--tests/module/alt-auth-t.c117
-rw-r--r--tests/module/bad-authtok-t.c53
-rw-r--r--tests/module/basic-t.c67
-rw-r--r--tests/module/cache-cleanup-t.c104
-rw-r--r--tests/module/cache-t.c210
-rw-r--r--tests/module/expired-t.c175
-rw-r--r--tests/module/fast-anon-t.c108
-rw-r--r--tests/module/fast-t.c57
-rw-r--r--tests/module/long-t.c46
-rw-r--r--tests/module/no-cache-t.c47
-rw-r--r--tests/module/pam-user-t.c80
-rw-r--r--tests/module/password-t.c152
-rw-r--r--tests/module/pkinit-t.c98
-rw-r--r--tests/module/realm-t.c87
-rw-r--r--tests/module/stacked-t.c50
-rw-r--r--tests/module/trace-t.c48
-rw-r--r--tests/pam-util/args-t.c86
-rw-r--r--tests/pam-util/fakepam-t.c121
-rw-r--r--tests/pam-util/logging-t.c146
-rw-r--r--tests/pam-util/options-t.c458
-rw-r--r--tests/pam-util/vector-t.c149
-rw-r--r--tests/portable/asprintf-t.c69
-rw-r--r--tests/portable/asprintf.c2
-rw-r--r--tests/portable/mkstemp-t.c81
-rw-r--r--tests/portable/mkstemp.c2
-rw-r--r--tests/portable/strndup-t.c60
-rw-r--r--tests/portable/strndup.c2
-rw-r--r--tests/runtests.c1782
-rwxr-xr-xtests/style/obsolete-strings-t104
-rw-r--r--tests/tap/basic.c1029
-rw-r--r--tests/tap/basic.h192
-rw-r--r--tests/tap/kadmin.c138
-rw-r--r--tests/tap/kadmin.h58
-rw-r--r--tests/tap/kerberos.c544
-rw-r--r--tests/tap/kerberos.h135
-rw-r--r--tests/tap/libtap.sh248
-rw-r--r--tests/tap/macros.h99
-rw-r--r--tests/tap/perl/Test/RRA.pm324
-rw-r--r--tests/tap/perl/Test/RRA/Automake.pm487
-rw-r--r--tests/tap/perl/Test/RRA/Config.pm224
-rw-r--r--tests/tap/process.c532
-rw-r--r--tests/tap/process.h95
-rw-r--r--tests/tap/string.c67
-rw-r--r--tests/tap/string.h51
-rwxr-xr-xtests/valgrind/logs-t83
254 files changed, 29790 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 000000000000..da1e4e8030d3
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,30 @@
+# Configuration for clang-format automated reformatting. -*- yaml -*-
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Copyright 2020-2021 Russ Allbery <eagle@eyrie.org>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+#
+# SPDX-License-Identifier: FSFAP
+
+---
+Language: Cpp
+BasedOnStyle: LLVM
+AlignConsecutiveMacros: true
+AlignEscapedNewlines: Left
+AllowShortEnumsOnASingleLine: false
+AlwaysBreakAfterReturnType: AllDefinitions
+BreakBeforeBinaryOperators: NonAssignment
+BreakBeforeBraces: WebKit
+ColumnLimit: 79
+IndentPPDirectives: AfterHash
+IndentWidth: 4
+IndentWrappedFunctionNames: false
+MaxEmptyLinesToKeep: 2
+SpaceAfterCStyleCast: true
+---
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000000..5ace4600a1f2
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 000000000000..6120a6cf8f58
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,44 @@
+name: build
+
+on:
+ push:
+ branches-ignore:
+ - "debian/**"
+ - "pristine-tar"
+ - "ubuntu/**"
+ - "upstream/**"
+ tags:
+ - "release/*"
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ env:
+ AUTHOR_TESTING: 1
+ C_TAP_VERBOSE: 1
+
+ strategy:
+ fail-fast: false
+ matrix:
+ kerberos:
+ - "mit"
+ - "heimdal"
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: install
+ run: sudo ci/install
+ - name: kdc-setup-mit
+ run: sudo ci/kdc-setup-mit
+ if: matrix.kerberos == 'mit'
+ - name: kdc-setup-heimdal
+ run: sudo ci/kdc-setup-heimdal
+ if: matrix.kerberos == 'heimdal'
+ - name: test
+ run: ci/test
+ env:
+ KERBEROS: ${{ matrix.kerberos }}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000000..9c3abee30255
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,344 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Comment: This file documents the copyright statements and licenses for
+ every file in this package in a machine-readable format. For a less
+ detailed, higher-level overview, see README.
+ .
+ For any copyright year range specified as YYYY-ZZZZ in this file, the
+ range specifies every single year in that closed interval.
+
+Files: *
+Copyright: 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ 2005 Andres Salomon <dilinger@debian.org>
+ 2005-2010, 2014-2015, 2017, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ 2008-2014 The Board of Trustees of the Leland Stanford Junior University
+License: BSD-3-clause or GPL-1+
+
+Files: .clang-format docs/pam_krb5.5 docs/pam_krb5.pod pam-util/vector.c
+ pam-util/vector.h portable/asprintf.c portable/dummy.c
+ portable/issetugid.c portable/kadmin.h portable/krb5-extra.c
+ portable/krb5.h portable/macros.h portable/mkstemp.c portable/pam.h
+ portable/pam_syslog.c portable/pam_vsyslog.c portable/reallocarray.c
+ portable/stdbool.h portable/strndup.c portable/system.h tests/README
+ tests/TESTS tests/config/README tests/data/cppcheck.supp
+ tests/fakepam/README tests/pam-util/vector-t.c tests/portable/asprintf-t.c
+ tests/portable/mkstemp-t.c tests/portable/strndup-t.c
+Copyright: 2005-2012, 2014-2021 Russ Allbery <eagle@eyrie.org>
+ 2006-2014 The Board of Trustees of the Leland Stanford Junior University
+License: all-permissive
+ Copying and distribution of this file, with or without modification, are
+ permitted in any medium without royalty provided the copyright notice and
+ this notice are preserved. This file is offered as-is, without any
+ warranty.
+
+Files: Makefile.in
+Copyright: 1994-2021 Free Software Foundation, Inc.
+ 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ 2005 Andres Salomon <dilinger@debian.org>
+ 2005-2007, 2014, 2017, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ 2009, 2011-2012
+ The Board of Trustees of the Leland Stanford Junior University
+License: FSF-unlimited, and BSD-3-clause or GPL-1+
+
+Files: aclocal.m4 m4/ltoptions.m4 m4/ltsugar.m4 m4/ltversion.m4
+ m4/lt~obsolete.m4
+Copyright: 1996-2021 Free Software Foundation, Inc.
+License: FSF-unlimited
+
+Files: build-aux/ar-lib build-aux/compile build-aux/depcomp
+ build-aux/missing
+Copyright: 1996-2021 Free Software Foundation, Inc.
+License: GPL-2+ with Autoconf exception or BSD-3-clause or GPL-1+
+
+Files: build-aux/config.guess build-aux/config.sub
+Copyright: 1992-2018 Free Software Foundation, Inc.
+License: GPL-3+ with Autoconf exception or BSD-3-clause or GPL-1+
+
+Files: build-aux/install-sh
+Copyright: 1994 X Consortium
+License: X11
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to permit
+ persons to whom the Software is furnished to do so, subject to the
+ following conditions:
+ .
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+ .
+ Except as contained in this notice, the name of the X Consortium shall
+ not be used in advertising or otherwise to promote the sale, use or other
+ dealings in this Software without prior written authorization from the X
+ Consortium.
+
+Files: build-aux/ltmain.sh
+Copyright: 1996-2015 Free Software Foundation, Inc.
+License: GPL-2+ with Libtool exception or BSD-3-clause or GPL-1+, and GPL-3+ with Libtool exception or BSD-3-clause or GPL-1+, and GPL-3+
+
+Files: ci/install ci/kdc-setup-heimdal ci/kdc-setup-mit ci/test
+ pam-util/args.c pam-util/args.h pam-util/logging.c pam-util/logging.h
+ pam-util/options.c pam-util/options.h tests/data/generate-krb5-conf
+ tests/data/valgrind.supp tests/docs/pod-spelling-t tests/docs/pod-t
+ tests/docs/spdx-license-t tests/fakepam/config.c tests/fakepam/data.c
+ tests/fakepam/general.c tests/fakepam/internal.h tests/fakepam/kuserok.c
+ tests/fakepam/logging.c tests/fakepam/pam.h tests/fakepam/script.c
+ tests/fakepam/script.h tests/pam-util/args-t.c tests/pam-util/fakepam-t.c
+ tests/pam-util/logging-t.c tests/pam-util/options-t.c tests/runtests.c
+ tests/style/obsolete-strings-t tests/tap/basic.c tests/tap/basic.h
+ tests/tap/kadmin.c tests/tap/kadmin.h tests/tap/kerberos.c
+ tests/tap/kerberos.h tests/tap/libtap.sh tests/tap/macros.h
+ tests/tap/perl/Test/RRA.pm tests/tap/perl/Test/RRA/Automake.pm
+ tests/tap/perl/Test/RRA/Config.pm tests/tap/process.c tests/tap/process.h
+ tests/tap/string.c tests/tap/string.h tests/valgrind/logs-t
+Copyright: 2000-2002, 2004-2021 Russ Allbery <eagle@eyrie.org>
+ 2001-2002, 2004-2014
+ The Board of Trustees of the Leland Stanford Junior University
+License: Expat
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to permit
+ persons to whom the Software is furnished to do so, subject to the
+ following conditions:
+ .
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
+ OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+ THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Files: configure
+Copyright: 1992-1996, 1998-2017, 2020-2021 Free Software Foundation, Inc.
+License: FSF-configure, and GPL-2+ with Libtool exception or BSD-3-clause or GPL-1+
+
+Files: m4/cc-flags.m4
+Copyright: 2006, 2009, 2016 Internet Systems Consortium, Inc.
+ 2016-2021 Russ Allbery <eagle@eyrie.org>
+License: ISC
+ Permission to use, copy, modify, and distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY
+ SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+Files: m4/clang.m4 m4/kadm5clnt.m4 m4/krb5-config.m4 m4/krb5-pkinit.m4
+ m4/krb5.m4 m4/ld-version.m4 m4/lib-depends.m4 m4/lib-helper.m4
+ m4/lib-pathname.m4 m4/pam-const.m4
+Copyright: 2005-2014
+ The Board of Trustees of the Leland Stanford Junior University
+ 2007, 2015, 2018, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ 2007-2008 Markus Moeller
+ 2008-2010 Free Software Foundation, Inc.
+License: unlimited
+ This file is free software; the authors give unlimited permission to copy
+ and/or distribute it, with or without modifications, as long as this
+ notice is preserved.
+
+Files: m4/libtool.m4
+Copyright: 1996-2001, 2003-2015 Free Software Foundation, Inc.
+License: FSF-unlimited, and GPL-2+ with Libtool exception or BSD-3-clause or GPL-1+
+
+Files: portable/krb5-profile.c
+Copyright: 1985-2005 the Massachusetts Institute of Technology
+License: MIT-Kerberos
+ Export of this software from the United States of America may require
+ a specific license from the United States Government. It is the
+ responsibility of any person or organization contemplating export to
+ obtain such a license before exporting.
+ .
+ WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
+ distribute this software and its documentation for any purpose and
+ without fee is hereby granted, provided that the above copyright
+ notice appear in all copies and that both that copyright notice and
+ this permission notice appear in supporting documentation, and that
+ the name of M.I.T. not be used in advertising or publicity pertaining
+ to distribution of the software without specific, written prior
+ permission. Furthermore if you modify this software you must label
+ your software as modified software and not distribute it in such a
+ fashion that it might be confused with the original MIT software.
+ M.I.T. makes no representations about the suitability of this software
+ for any purpose. It is provided "as is" without express or implied
+ warranty.
+ .
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+ .
+ Individual source code files are copyright MIT, Cygnus Support,
+ OpenVision, Oracle, Sun Soft, FundsXpress, and others.
+ .
+ Project Athena, Athena, Athena MUSE, Discuss, Hesiod, Kerberos, Moira,
+ and Zephyr are trademarks of the Massachusetts Institute of Technology
+ (MIT). No commercial use of these trademarks may be made without
+ prior written permission of MIT.
+ .
+ "Commercial use" means use of a name in a product or other for-profit
+ manner. It does NOT prevent a commercial firm from referring to the
+ MIT trademarks in order to convey information (although in doing so,
+ recognition of their trademark status should be given).
+
+License: BSD-3-clause or GPL-1+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+ .
+ 1. Redistributions of source code must retain the above copyright
+ notice, and the entire permission notice in its entirety, including
+ the disclaimer of warranties.
+ .
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ .
+ 3. The name of the author may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+ .
+ ALTERNATIVELY, this product may be distributed under the terms of the
+ GNU General Public License, in which case the provisions of the GPL
+ are required INSTEAD OF the above restrictions. (This clause is
+ necessary due to a potential bad interaction between the GPL and the
+ restrictions contained in a BSD-style copyright.)
+ .
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGE.
+
+License: FSF-configure
+ This script is free software; the Free Software Foundation gives unlimited
+ permission to copy, distribute and modify it.
+
+License: FSF-unlimited
+ This file is free software; the Free Software Foundation gives unlimited
+ permission to copy and/or distribute it, with or without modifications, as
+ long as this notice is preserved.
+ .
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY, to the extent permitted by law; without even the
+ implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+License: GPL-2+ with Autoconf exception
+ This file is free software; you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by the
+ Free Software Foundation; either version 2 of the License, or (at your
+ option) any later version.
+ .
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
+ .
+ As a special exception to the GNU General Public License, if you
+ distribute this file as part of a program that contains a configuration
+ script generated by Autoconf, you may include it under the same
+ distribution terms that you use for the rest of that program.
+Comment: The option described in the license has been accepted and these
+ files are distributed under the same terms as the package as a whole, as
+ described at the top of this file.
+
+License: GPL-2+ with Libtool exception
+ This file is part of GNU Libtool.
+ .
+ GNU Libtool is free software; you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by the
+ Free Software Foundation; either version 2 of the License, or (at your
+ option) any later version.
+ .
+ As a special exception to the GNU General Public License, if you
+ distribute this file as part of a program or library that is built using
+ GNU Libtool, you may include this file under the same distribution terms
+ that you use for the rest of that program.
+ .
+ GNU Libtool is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ Public License for more details.
+Comment: The option described in the license has been accepted and these
+ files are distributed under the same terms as the package as a whole, as
+ described at the top of this file.
+
+License: GPL-3+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ .
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+License: GPL-3+ with Autoconf exception
+ This file is free software; you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by the
+ Free Software Foundation; either version 3 of the License, or (at your
+ option) any later version.
+ .
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, see <https://www.gnu.org/licenses/>.
+ .
+ As a special exception to the GNU General Public License, if you
+ distribute this file as part of a program that contains a configuration
+ script generated by Autoconf, you may include it under the same
+ distribution terms that you use for the rest of that program. This
+ Exception is an additional permission under section 7 of the GNU General
+ Public License, version 3 ("GPLv3").
+Comment: The option described in the license has been accepted and these
+ files are distributed under the same terms as the package as a whole, as
+ described at the top of this file.
+
+License: GPL-3+ with Libtool exception
+ GNU Libtool is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+ .
+ As a special exception to the GNU General Public License, if you
+ distribute this file as part of a program or library that is built
+ using GNU Libtool, you may include this file under the same
+ distribution terms that you use for the rest of that program.
+ .
+ GNU Libtool is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+Comment: The option described in the license has been accepted and these
+ files are distributed under the same terms as the package as a whole, as
+ described at the top of this file.
+
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 000000000000..ef28c36ad045
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,210 @@
+# Automake makefile for pam-krb5.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2005-2007, 2014, 2017, 2020-2021 Russ Allbery <eagle@eyrie.org>
+# Copyright 2009, 2011-2012
+# The Board of Trustees of the Leland Stanford Junior University
+# Copyright 2005 Andres Salomon <dilinger@debian.org>
+# Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+ACLOCAL_AMFLAGS = -I m4
+EXTRA_DIST = .clang-format .gitignore .github LICENSE README.md bootstrap \
+ ci/README.md ci/files/heimdal/heimdal-kdc \
+ ci/files/heimdal/kadmind.acl ci/files/heimdal/kdc.conf \
+ ci/files/heimdal/krb5.conf ci/files/heimdal/pki-mapping \
+ ci/files/mit/extensions.client ci/files/mit/extensions.kdc \
+ ci/files/mit/kadm5.acl ci/files/mit/kdc.conf ci/files/mit/krb5.conf \
+ ci/kdc-setup-heimdal ci/kdc-setup-mit ci/install ci/test \
+ docs/docknot.yaml docs/pam_krb5.pod module/pam_krb5.map \
+ module/pam_krb5.sym tests/README tests/TESTS tests/config/README \
+ tests/data/cppcheck.supp tests/data/generate-krb5-conf \
+ tests/data/krb5-pam.conf tests/data/krb5.conf tests/data/perl.conf \
+ tests/data/scripts tests/data/valgrind.supp \
+ tests/docs/pod-spelling-t tests/docs/pod-t \
+ tests/docs/spdx-license-t tests/fakepam/README tests/tap/libtap.sh \
+ tests/tap/perl/Test/RRA.pm tests/tap/perl/Test/RRA/Automake.pm \
+ tests/tap/perl/Test/RRA/Config.pm tests/style/obsolete-strings-t \
+ tests/valgrind/logs-t
+
+# Everything we build needs the Kerbeors headers and library flags.
+AM_CPPFLAGS = $(KRB5_CPPFLAGS)
+AM_LDFLAGS = $(KRB5_LDFLAGS)
+
+noinst_LTLIBRARIES = pam-util/libpamutil.la portable/libportable.la
+portable_libportable_la_SOURCES = portable/dummy.c portable/kadmin.h \
+ portable/krb5.h portable/macros.h portable/pam.h portable/stdbool.h \
+ portable/system.h
+portable_libportable_la_LIBADD = $(LTLIBOBJS)
+pam_util_libpamutil_la_SOURCES = pam-util/args.c pam-util/args.h \
+ pam-util/logging.c pam-util/logging.h pam-util/options.c \
+ pam-util/options.h pam-util/vector.c pam-util/vector.h
+
+if HAVE_LD_VERSION_SCRIPT
+ VERSION_LDFLAGS = -Wl,--version-script=${srcdir}/module/pam_krb5.map
+else
+ VERSION_LDFLAGS = -export-symbols ${srcdir}/module/pam_krb5.sym
+endif
+
+pamdir = $(libdir)/security
+pam_LTLIBRARIES = module/pam_krb5.la
+module_pam_krb5_la_SOURCES = module/account.c module/alt-auth.c \
+ module/auth.c module/cache.c module/context.c module/fast.c \
+ module/internal.h module/options.c module/password.c \
+ module/prompting.c module/public.c module/setcred.c \
+ module/support.c
+module_pam_krb5_la_LDFLAGS = -module -shared \
+ -avoid-version $(VERSION_LDFLAGS) $(AM_LDFLAGS)
+module_pam_krb5_la_LIBADD = pam-util/libpamutil.la portable/libportable.la \
+ $(KRB5_LIBS)
+dist_man_MANS = docs/pam_krb5.5
+
+# The manual page is normally generated by the bootstrap script, but add a
+# Makefile rule to regenerate it if it is modified.
+docs/pam_krb5.5: $(srcdir)/docs/pam_krb5.pod
+ pod2man --release="$(VERSION)" --center=pam-krb5 -s 5 \
+ $(srcdir)/docs/pam_krb5.pod > $@
+
+# Work around the GNU Coding Standards, which leave all the Autoconf and
+# Automake stuff around after make maintainer-clean, thus making that command
+# mostly worthless.
+DISTCLEANFILES = config.h.in~ configure~
+MAINTAINERCLEANFILES = Makefile.in aclocal.m4 build-aux/compile \
+ build-aux/config.guess build-aux/config.sub build-aux/depcomp \
+ build-aux/install-sh build-aux/ltmain.sh build-aux/missing \
+ config.h.in configure docs/pam_krb5.5 m4/libtool.m4 m4/ltoptions.m4 \
+ m4/ltsugar.m4 m4/ltversion.m4 m4/lt~obsolete.m4
+
+# Separate target for a human to request building everything with as many
+# compiler warnings enabled as possible.
+warnings:
+ $(MAKE) V=0 AM_CFLAGS='$(WARNINGS_CFLAGS) $(AM_CFLAGS)' \
+ KRB5_CPPFLAGS='$(KRB5_CPPFLAGS_WARNINGS)'
+ $(MAKE) V=0 AM_CFLAGS='$(WARNINGS_CFLAGS) $(AM_CFLAGS)' \
+ KRB5_CPPFLAGS='$(KRB5_CPPFLAGS_WARNINGS)' $(check_PROGRAMS)
+
+# The bits below are for the test suite, not for the main package.
+check_PROGRAMS = tests/runtests tests/module/alt-auth-t \
+ tests/module/bad-authtok-t tests/module/basic-t \
+ tests/module/cache-cleanup-t tests/module/cache-t \
+ tests/module/expired-t tests/module/fast-anon-t tests/module/fast-t \
+ tests/module/long-t tests/module/no-cache-t tests/module/pam-user-t \
+ tests/module/password-t tests/module/pkinit-t tests/module/realm-t \
+ tests/module/stacked-t tests/module/trace-t tests/pam-util/args-t \
+ tests/pam-util/fakepam-t tests/pam-util/logging-t \
+ tests/pam-util/options-t tests/pam-util/vector-t \
+ tests/portable/asprintf-t tests/portable/mkstemp-t \
+ tests/portable/strndup-t
+tests_runtests_CPPFLAGS = -DC_TAP_SOURCE='"$(abs_top_srcdir)/tests"' \
+ -DC_TAP_BUILD='"$(abs_top_builddir)/tests"'
+check_LIBRARIES = tests/fakepam/libfakepam.a tests/tap/libtap.a
+tests_fakepam_libfakepam_a_SOURCES = tests/fakepam/config.c \
+ tests/fakepam/data.c tests/fakepam/general.c \
+ tests/fakepam/internal.h tests/fakepam/kuserok.c \
+ tests/fakepam/logging.c tests/fakepam/pam.h tests/fakepam/script.c \
+ tests/fakepam/script.h
+tests_tap_libtap_a_CPPFLAGS = $(KADM5CLNT_CPPFLAGS) $(AM_CPPFLAGS)
+tests_tap_libtap_a_SOURCES = tests/tap/basic.c tests/tap/basic.h \
+ tests/tap/kadmin.c tests/tap/kadmin.h tests/tap/kerberos.c \
+ tests/tap/kerberos.h tests/tap/macros.h tests/tap/process.c \
+ tests/tap/process.h tests/tap/string.c tests/tap/string.h
+
+# The list of objects and libraries used for module testing by programs that
+# link with the fake PAM library or with both it and the module.
+MODULE_OBJECTS = module/account.lo module/alt-auth.lo module/auth.lo \
+ module/cache.lo module/context.lo module/fast.lo module/options.lo \
+ module/password.lo module/prompting.lo module/public.lo \
+ module/setcred.lo module/support.lo pam-util/libpamutil.la \
+ tests/fakepam/libfakepam.a
+
+# The test programs themselves.
+tests_module_alt_auth_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_bad_authtok_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_basic_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_cache_cleanup_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_cache_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_expired_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KADM5CLNT_LDFLAGS) $(KADM5CLNT_LIBS) \
+ $(KRB5_LIBS)
+tests_module_fast_anon_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_fast_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_long_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_no_cache_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_pam_user_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_password_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_pkinit_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_realm_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_stacked_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_module_trace_t_LDADD = $(MODULE_OBJECTS) tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_pam_util_args_t_LDADD = pam-util/libpamutil.la \
+ tests/fakepam/libfakepam.a tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_pam_util_fakepam_t_LDADD = tests/fakepam/libfakepam.a \
+ tests/tap/libtap.a portable/libportable.la
+tests_pam_util_logging_t_LDADD = pam-util/libpamutil.la \
+ tests/fakepam/libfakepam.a tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_pam_util_options_t_LDADD = pam-util/libpamutil.la \
+ tests/fakepam/libfakepam.a tests/tap/libtap.a \
+ portable/libportable.la $(KRB5_LIBS)
+tests_pam_util_vector_t_LDADD = pam-util/libpamutil.la \
+ tests/fakepam/libfakepam.a tests/tap/libtap.a \
+ portable/libportable.la
+tests_portable_asprintf_t_SOURCES = tests/portable/asprintf-t.c \
+ tests/portable/asprintf.c
+tests_portable_asprintf_t_LDADD = tests/tap/libtap.a portable/libportable.la
+tests_portable_mkstemp_t_SOURCES = tests/portable/mkstemp-t.c \
+ tests/portable/mkstemp.c
+tests_portable_mkstemp_t_LDADD = tests/tap/libtap.a portable/libportable.la
+tests_portable_strndup_t_SOURCES = tests/portable/strndup-t.c \
+ tests/portable/strndup.c
+tests_portable_strndup_t_LDADD = tests/tap/libtap.a portable/libportable.la
+
+check-local: $(check_PROGRAMS)
+ cd tests && ./runtests -l '$(abs_top_srcdir)/tests/TESTS'
+
+# Used by maintainers to check the source code with cppcheck.
+check-cppcheck:
+ cd $(abs_top_srcdir) && \
+ find . -name .git -prune -o -name '*.[ch]' -print \
+ | cppcheck -q --force --error-exitcode=2 --file-list=- \
+ --suppressions-list=tests/data/cppcheck.supp \
+ --enable=warning,performance,portability,style
+
+# The full path to valgrind and its options, used when doing valgrind
+# testing.
+VALGRIND_COMMAND = $(PATH_VALGRIND) --leak-check=full \
+ --trace-children=yes \
+ --trace-children-skip=/bin/sh,*/generate-krb5-conf \
+ --suppressions=$(abs_top_srcdir)/tests/data/valgrind.supp \
+ --log-file=$(abs_top_builddir)/tests/tmp/valgrind/log.%p
+
+# Used by maintainers to run the main test suite under valgrind.
+check-valgrind: $(check_PROGRAMS)
+ rm -rf $(abs_top_builddir)/tests/tmp
+ mkdir $(abs_top_builddir)/tests/tmp
+ mkdir $(abs_top_builddir)/tests/tmp/valgrind
+ C_TAP_VALGRIND="$(VALGRIND_COMMAND)" tests/runtests \
+ -l '$(abs_top_srcdir)/tests/TESTS'
+
+# Used by maintainers to reformat all source code using clang-format and
+# excluding some files.
+reformat:
+ find . -name '*.[ch]' \! -name krb5-profile.c -print \
+ | xargs clang-format -style=file -i
diff --git a/NEWS b/NEWS
new file mode 100644
index 000000000000..b5c007ef9359
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,1215 @@
+ User-Visible pam-krb5 Changes
+
+pam-krb5 4.11 (2021-10-17)
+
+ Properly support calling pam_end with PAM_DATA_SILENT by not deleting
+ the underlying ticket cache. This flag is used when the application
+ is closing the PAM session after a fork to free memory resources, but
+ doesn't intend to free resources external to the process because
+ another process may still depend on them. Thanks to Andrew G. Morgan
+ for the report. (GitHub #21)
+
+ Stop attempting to guess the correct PAM module installation path on
+ Linux systems when --prefix is set to /usr and instead document that
+ --libdir will probably need to be set explicitly. The previous logic
+ is now broken on Debian usrmerge systems and the guesswork seems too
+ fragile to maintain.
+
+ Update to rra-c-util 10.0:
+
+ * Support Autoconf 2.71 without warnings.
+ * Tests written in Perl now require Perl 5.10 or later.
+
+pam-krb5 4.10 (2021-03-20)
+
+ When re-retrieving the authenticated principal from the current cache,
+ ensure the stored principal in the authentication context is always
+ either valid or NULL. Otherwise, a failure of krb5_cc_get_principal
+ could result in a double free. Thanks to Michael Muehle for the
+ report.
+
+ Update to rra-c-util 9.0:
+
+ * Check that at least one Kerberos header file was found and works.
+ * Use AS_ECHO in all Autoconf macros in preference to echo.
+ * Fix portability of reallocarray on NetBSD systems.
+ * Stop providing a replacement for a broken snprintf.
+
+ Update to C TAP Harness 4.7:
+
+ * Fix warnings with GCC 10.
+
+pam-krb5 4.9 (2020-03-30)
+
+ SECURITY: All previous versions of this module could overflow the
+ buffer provided by the underlying Kerberos library for the response to
+ a prompt by writing a single nul character past the end of the buffer.
+ (CVE-2020-10595)
+
+ Support use_pkinit with MIT Kerberos. (Debian Bug#871699)
+
+ Reject passwords as long or longer than PAM_MAX_RESP_SIZE (normally
+ 512 octets), since extremely long passwords can be used for a denial
+ of service attack via the Kerberos string to key function. Thanks to
+ Florian Best for pointing out this issue and suggesting a good fix.
+
+ Use explicit_bzero instead of memset, where available, to overwrite
+ the memory used by PAM responses before freeing. This reduces the
+ lifetime of passwords and other secrets in memory.
+
+ Return more accurate errors from the Kerberos prompter function if it
+ was unable to prompt for the password. This may translate into better
+ debug log messages and, in some situations, returning the slightly
+ more accurate PAM_AUTHINFO_UNAVAIL instead of PAM_AUTH_ERR.
+
+ Fix an edge-case memory leak in pam_chauthtok when prompting for a new
+ password for an ignored user.
+
+ Ensure the module/basic test will run properly when the system
+ krb5.conf file does not specify a default realm. Reported by TBK.
+
+ Update to rra-c-util 8.2:
+
+ * Fix support for configuring the test suite with a krb5.conf file.
+ * Drop support for Perl 5.6.
+ * Reformat all C source using clang-format 10.
+ * Remove bogus snprintf tests.
+ * Fix misplaced va_end in the pam-util putil_log_failure function.
+ * Skip checking for krb5-config on the path if a prefix was given.
+ * Add SPDX-License-Identifier headers to all substantial source files.
+
+ Update to C TAP Harness 4.6:
+
+ * Fixed malloc error checking in bstrndup.
+ * Fix (harmless) allocation error in runtests driver.
+ * Add support for valgrind testing via test list options.
+ * Report test failures as left and right, not wanted and seen.
+ * Fix is_string comparisons involving NULL pointers and "(null)".
+ * Add SPDX-License-Identifier headers to all substantial source files.
+
+pam-krb5 4.8 (2017-12-30)
+
+ When verifying that an expired password can still be used to get
+ kadmin/changepw credentials, correctly set the credential options for
+ getting password change credentials, not for getting initial
+ credentials. This should fix password change issues when, for
+ example, krb5.conf requests that all tickets be proxiable but
+ kadmin/changepw doesn't allow proxiable credentials. Thanks to
+ Florian Best for the bug report.
+
+ When built against recent versions of Heimdal with richer status codes
+ from PKINIT attempts, report to the user the reason for a PKINIT
+ failure. Based on work by Henry Jacques.
+
+ Document the test suite configuration files required to run the PKINIT
+ tests.
+
+ Fix expired password tests to work with Heimdal 7.0.1 and later.
+
+ Better document that the default Kerberos library ticket cache
+ location is not used (and why), and how to set configuration
+ parameters in krb5.conf. Thanks, Matthew Gabeler-Lee. (Debian
+ Bug#872943)
+
+ Compile cleanly under GCC 7 and Clang warnings and Clang's static
+ analyzer.
+
+ Rename the script to bootstrap from a Git checkout to bootstrap,
+ matching the emerging consensus in the Autoconf world.
+
+ Update to rra-c-util 7.0:
+
+ * Fix new warnings in GCC 7.
+ * Support a warning build under Clang.
+ * Avoid zero-length allocations in reallocarray and vector.
+ * Probe for warning flags instead of hard-coding a list.
+ * New test for obsolete URLs and email addresses.
+ * Remove unused portable replacements for strlcpy and strlcat.
+ * Use C_TAP_SOURCE and C_TAP_BUILD environment variables in tests.
+ * Fix portability defines for anonymous principal strings.
+ * Clear errno on pam_modutil_getpwnam to improve other testing.
+ * Add portability defines for macOS's PAM implementation.
+ * Add new Autoconf macro to probe for pam_strerror const usage.
+ * Support Solaris 10's included Kerberos.
+
+ Update to C TAP Harness 4.2:
+
+ * Avoid zero-length allocations in breallocarray.
+ * Add is_blob and is_bool functions.
+ * Use C_TAP_SOURCE and C_TAP_BUILD environment variables in tests.
+ * Fix segfault in runtests with an empty test list.
+ * Display verbose test results with -v or C_TAP_VERBOSE.
+ * Test infrastructure builds cleanly with Clang warnings.
+
+pam-krb5 4.7 (2014-12-25)
+
+ Add a no_update_user option that disables the normal update of the
+ PAM_USER PAM variable after canonicalization of the username. When
+ this is set, pam-krb5 will not convert full principal names to local
+ usernames where possible for the rest of the PAM stack.
+
+ Suppress spurious password prompt from Heimdal when authenticating
+ with PKINIT.
+
+ Map unknown realm errors from the Kerberos libraries to the PAM error
+ code PAM_AUTHINFO_UNAVAIL instead of PAM_AUTH_ERR.
+
+ Treat an KRB5_GET_IN_TKT_LOOP error as an incorrect password. Heimdal
+ KDCs sometimes return it, and Heimdal kinit treats it this way.
+ Similarly, treat a KRB5_BAD_ENCTYPE error as an incorrect password,
+ since this error is returned by a Heimdal 1.6-rc2 KDC for incorrect
+ preauth from a MIT Kerberos 1.12.1 client.
+
+ Add the version number at which each module option was added with its
+ current meaning to the documentatation.
+
+ Update to rra-c-util 5.6:
+
+ * Suppress warnings from Kerberos headers in non-system paths.
+ * Fix probing for Heimdal's libroken to work with older versions.
+ * Fix Kerberos header detection if root or include paths are given.
+ * Pass --deps to krb5-config in the non-reduced-dependencies case.
+ * Provide a reallocarray replacement for platforms without it.
+ * Use reallocarray where appropriate.
+ * Drop checks for NULL before freeing pointers.
+ * Drop explicit pointer initialization to NULL and rely on calloc.
+ * Check the return status of snprintf and vsnprintf properly.
+ * Preserve errno if snprintf fails in vasprintf replacement.
+ * Suppress a dummy symbol in the client library that could leak.
+ * Fix syntax errors when building with a C++ compiler.
+ * Avoid test suite failures where tested functions are macros.
+
+ Update to C TAP Harness 3.2:
+
+ * Reopen standard input to /dev/null when running a test list.
+ * Don't leak extraneous file descriptors to tests.
+ * Suppress lazy plans and test summaries if the test failed with bail.
+ * bail and sysbail now exit with status 255 to match Test::More.
+ * runtests now treats the command line as a list of tests by default.
+ * The full test executable path can now be passed to runtests -o.
+ * Improved harness output for tests with lazy plans.
+ * Improved harness output to a terminal for some abort cases.
+ * Flush harness output after each test even when not on a terminal.
+
+pam-krb5 4.6 (2012-06-02)
+
+ Add an anon_fast option that attempts anonymous authentication
+ (generally implemented via anonymous PKINIT inside the Kerberos
+ library) and then, if successful, uses those credentials for FAST
+ armor. If fast_ccache and anon_fast are both specified, anonymous
+ authentication will be used as a fallback if the specified FAST ticket
+ cache doesn't exist. Based on patches from Yair Yarom.
+
+ Add a user_realm option to only set the realm for unqualified user
+ principals. This differs from the existing realm option in that realm
+ also changes the default realm for authorization decisions and for
+ verification of credentials. Update the realm option documentation to
+ clarify the differences and remove incorrect information. Patch from
+ Roland C. Dowdeswell.
+
+ Add a no_prompt option to suppress the PAM module's prompt for the
+ user's password and defer all prompting to the Kerberos library. This
+ allows the Kerberos library to have complete control of the prompting
+ process, which may be desirable if authentication mechanisms other
+ than password are in use. Be aware that, with this option set, the
+ PAM module has no control over the contents of the prompt and cannot
+ store the user's password in the PAM data. Based on a patch by Yair
+ Yarom.
+
+ Add a silent option to force the module to behave as if the
+ application had passed in PAM_SILENT and suppress text messages and
+ errors from the Kerberos library. Patch from Yair Yarom.
+
+ Add preliminary support for Kerberos trace logging via a trace option
+ that enables trace logging if supported by the underlying Kerberos
+ library. The option takes as an argument the file name to which to
+ log trace output. This option does not yet work with any released
+ version of Kerberos, but may work with the next release of MIT
+ Kerberos.
+
+ MIT Kerberos does not add a colon and space to its password prompts,
+ but Heimdal does. pam-krb5 previously unconditionally added a colon
+ and space, resulting in doubled colons with Heimdal. Work around this
+ inconsistency by not adding the colon and space if already present.
+
+ Fix alt_auth_map support to preserve the realm of the authentication
+ identity when forming the alternate authentication principal, matching
+ the documentation.
+
+ Document that the alt_auth_map format may contain a realm to force all
+ mapped principals to be in that realm. In that case, don't add the
+ realm of the authentication identity. Note that this can be used as a
+ simple way to attempt authentication in an alternate realm first and
+ then fall back to the local realm, although any complex attempt at
+ authentication in multiple realms should instead run the module
+ multiple times with different realm settings.
+
+ Avoid a NULL pointer dereference if krb5_init_context fails.
+
+ Fix initialization of time values in the module configuration on
+ platforms (like S/390X) where krb5_deltat is not equivalent to long.
+
+ Close a memory leak when search_k5login is set but the user has no
+ .k5login file.
+
+ Close several memory leaks in alt_auth_map support.
+
+ Suppress bogus error messages about unknown option for the realm
+ option. The option was being parsed and honored despite the error.
+
+ Retry authentication under try_first_pass on several other errors in
+ addition to decrypt integrity check errors to handle a wider array of
+ possible "password incorrect" error messages from the KDC.
+
+ Update to rra-c-util 4.4:
+
+ * Replacement strndup now works with non-nul-terminated strings.
+ * New Kerberos test setup that simplifies writing tests.
+ * Add -D_FORTIFY_SOURCE=2 to the make warnings flags.
+ * Use --deps flag to krb5-config by default.
+ * Suppress __alloc_size__ attribute with older versions of gcc.
+ * Suppress attribute warnings for non-gcc compilers.
+
+ Update to C TAP Harness 1.12:
+
+ * Add bstrndup to the basic C TAP library.
+ * Only use feature-test macros when requested or built with gcc -ansi.
+ * New tests/tap/macros.h header with some common definitions.
+ * Drop is_double from the C TAP library to avoid requiring -lm.
+ * Avoid using local in the shell libtap.sh library.
+
+pam-krb5 4.5 (2011-12-24)
+
+ Suppress the notice that the password is being changed because it's
+ expired if force_first_pass or use_first_pass is set in the password
+ stack, indicating that it's stacked with another module that's also
+ doing password changes. This is arguable, but without this change the
+ notification message of why the password is being changed shows up
+ confusingly in the middle of the password change interaction. Based
+ on a patch by William Yang.
+
+ Some old versions of Heimdal (0.7.2 in OpenBSD 4.9, specifically)
+ reportedly return KRB5KDC_ERR_KEY_EXP for accounts with expired
+ keys even if the supplied password is wrong. Work around this by
+ confirming that the PAM module can obtain tickets for kadmin/changepw
+ before returning a password expiration error instead of an invalid
+ password error. Based on a patch by William Yang.
+
+ The location of the temporary root-owned ticket cache created during
+ the authentication process is now also controlled by the ccache_dir
+ option (but not the ccache option) rather than forced to be in /tmp.
+ This will allow system administrators to configure an alternative
+ cache directory so that pam-krb5 can continue working when /tmp is
+ full.
+
+ Report more specific errors in syslog if authorization checks (such as
+ .k5login checks) fail.
+
+ Pass a NULL principal to krb5_set_password with MIT client libraries
+ to prefer the older change password protocol for compatibility with
+ older KDCs. This is not necessary on Heimdal since Heimdal's
+ krb5_set_password tries both protocols.
+
+ Improve logging and authorization checks when defer_pwchange is set
+ and a user authenticates with an expired password.
+
+ When probing for Kerberos libraries, always add any supplemental
+ libraries found to that point to the link command. This will fix
+ configure failures on platforms without working transitive shared
+ library dependencies.
+
+ Close some memory leaks where unparsed Kerberos principal names were
+ never freed.
+
+ Restructure the code to work with OpenPAM's default PAM build
+ machinery, which exports a struct containing module entry points
+ rather than public pam_sm_* functions. Thanks to Fredrik Pettai for
+ the information.
+
+ In debug logging, report symbolic names for PAM flags on PAM function
+ entry rather than the numeric PAM flags. This helps with automated
+ testing and with debugging PAM problems on different operating
+ systems.
+
+ Include <krb5/krb5.h> if <krb5.h> is missing, which permits finding
+ the header file on NetBSD systems. Thanks to Fredrik Pettai for the
+ report.
+
+ Replace the Kerberos compatibility layer with equivalent but
+ better-structured code from rra-c-util 4.0.
+
+ Avoid krb5-config and use manual library probing if --with-krb5-lib or
+ --with-krb5-include were given to configure. This avoids having to
+ point configure at a nonexistent krb5-config to override its results.
+
+ Use PATH_KRB5_CONFIG instead of KRB5_CONFIG to locate krb5-config in
+ configure, to avoid a conflict with the variable used by the Kerberos
+ libraries to find krb5.conf.
+
+ Change references to Kerberos v5 to just Kerberos in the
+ documentation. Kerberos v5 has been the default version of Kerberos
+ for over ten years now.
+
+ Update to rra-c-util 4.0:
+
+ * Add notices to all files copied over from rra-c-util.
+ * Include strings.h for additional POSIX functions where found.
+ * Fix detection of whether PAM uses const on FreeBSD.
+ * Update warning flags for make warnings for GCC 4.6.1.
+ * Limit symbol exports even on systems without GNU ld.
+ * Fix replacement mkstemp to use long long where available.
+ * Improve stripping of /usr/include from krb5-config results.
+ * Use issetugid where available, not the misnamed issetuidgid.
+
+ Update to C TAP Harness 1.9:
+
+ * Add bmalloc, bcalloc, brealloc, and bstrdup TAP library functions.
+ * Fix runtests to honor -s even if BUILD and -b aren't given.
+ * Add test_tmpdir and test_tmpdir_free to TAP library.
+ * runtests now frees all allocated resources on exit.
+
+pam-krb5 4.4 (2010-12-31)
+
+ Do not prompt for a password when try_pkinit is set and the module is
+ built against MIT Kerberos. This fixes a spurious password prompt
+ introduced in 4.1, but partly reintroduces the bug fixed in 4.1 where
+ the user's password is not saved in the PAM data if the authentication
+ falls back to password when PKINIT fails. This requires more work
+ to fix and will be addressed in a subsequent release. Thanks to
+ Бранко Мајић (Branko Majic) for the report.
+
+ Reorganize the configuration section of the pam_krb5 man page to
+ divide the many PAM module options into sections.
+
+ When probing for <ibm_svc/krb5_svc.h> (part of AIX's bundled Kerberos
+ implementation), include <krb5.h> before attempting to include that
+ header to quiet confusing Autoconf warnings. Reported by Wilfried
+ Weiss.
+
+ Update to rra-c-util 3.0:
+
+ * Fix compilation of the replacement snprintf for old systems.
+ * Look for krb5-config in /usr/kerberos/bin for Red Hat systems.
+ * Fix compilation with OpenBSD's Heimdal without separate libroken.
+
+pam-krb5 4.3 (2010-06-09)
+
+ Add a fast_ccache option that, if set, points to a Kerberos ticket
+ cache used for Flexible Authentication Secure Tunneling (FAST) to
+ protect the authentication. FAST is a mechanism to protect Kerberos
+ against password guessing attacks and provide other security
+ improvements. This option is only available when built against
+ Kerberos libraries with FAST support (currently only MIT Kerberos 1.7
+ or later). Patch from Sam Hartman.
+
+ Fix error in freeing a previous alt_auth_map setting when parsing
+ configuration options. Patch from Sam Hartman.
+
+ Fix the linker flags for Solaris with the native compiler. Thanks,
+ Kevin Sumner.
+
+pam-krb5 4.2 (2009-11-25)
+
+ Add a new fail_pwchange option, which suppresses password changes for
+ expired passwords and treats expired passwords the same as incorrect
+ passwords.
+
+ Include all the new header files from the portability code so that
+ it will actually compile on non-Linux platforms.
+
+pam-krb5 4.1 (2009-11-20)
+
+ Return PAM_SUCCESS, not PAM_USER_UNKNOWN, for ignored users in
+ pam_setcred. It's safe to return success when doing nothing in
+ pam_setcred because the stack has already been frozen after the
+ authentication step, and returning an error causes the stack to fail
+ on some other Linux PAM implementations. Thanks, Ian Ward Comfort.
+
+ In the second pass through the password group, prompt for the new
+ password and store it in the PAM data even if the user is being
+ ignored. This is required to allow this module to be stacked with
+ another module that uses use_authtok. Without this behavior, the
+ second module won't be able to work for any ignored user since it will
+ see no saved password and use_authtok will reject the password change.
+
+ Fix return status from pam_sm_acct_mgmt if we were unable to retrieve
+ PAM_USER.
+
+ Log successful authentications to syslog with priority LOG_INFO,
+ including the Kerberos principal used for authentication.
+
+ Log failed authentication to syslog with priority LOG_NOTICE,
+ including roughly the same additional information that the Linux PAM
+ pam_unix logs by default.
+
+ Use pam_syslog for logging where available. This means pam-krb5 log
+ messages will look like all other log messages for Linux PAM modules
+ on Linux. Change the format of log messages on all platforms to
+ hopefully be somewhat clearer.
+
+ Rationalize logging. The module should now follow the recommendations
+ of the Linux PAM Module Writers' Guide for log levels. More errors
+ are logged at LOG_ERR instead of LOG_DEBUG, and system resource errors
+ are now logged at LOG_CRIT instead of LOG_ERR.
+
+ Add additional error and debug logging in places where significant
+ actions or failures may happen without previously being logged. Also
+ add failure information from PAM or Kerberos libraries to messages
+ where appropriate.
+
+ Add replacement snprintf, vsnprintf, and mkstemp functions for
+ pointless portability to ancient systems.
+
+pam-krb5 4.0 (2009-11-13)
+
+ UPGRADE WARNING: If you were using pam_krb5 with the use_authtok
+ parameter in the password group, you will need to add use_first_pass
+ to your configuration to keep the same behavior. See below for
+ details.
+
+ UPGRADE WARNING: If you used the use_authtok parameter in the
+ authentication group, you should change it to force_first_pass.
+
+ Previous versions of this module incorrectly implemented the standard
+ use_authtok parameter. use_authtok applies only to the password group
+ and says to use the new password stored in the PAM data rather than
+ prompting for a new password. It doesn't imply anything about where
+ to obtain the old password, but it was implemented as requiring both
+ the old and new password be in the PAM stack already. This doesn't
+ work when stacked with pam_cracklib. Change use_authtok to have the
+ correct meaning, which means that password group configurations may
+ need to add use_first_pass to use_authtok to get the desired behavior.
+
+ use_first_pass and try_first_pass no longer affect how the new
+ password is obtained during password changes. To use a password
+ obtained by a previous module, use use_authtok instead.
+
+ A new option, force_first_pass, is now supported for both the
+ authentication and password groups. It tells the module to always get
+ the user's current password from the PAM data and fail without
+ prompting if it isn't already set. This is the meaning that
+ use_authtok previously had for the current password.
+
+ use_authtok no longer has any meaning for the authentication stack.
+ Use force_first_pass instead, which does the same as use_authtok used
+ to do. use_authtok will be temporarily converted to force_first_pass
+ in the authentication group and log a diagnostic, but this will be
+ removed in the future.
+
+ Stop returning PAM_IGNORE from pam_setcred if the user is ignored or
+ didn't log in via Kerberos and instead return PAM_USER_UNKNOWN. This
+ fixes problems with the Linux PAM library where returning PAM_IGNORE
+ would cause pam_setcred to fail even if other modules succeeded.
+ Since pam_authenticate never returned PAM_IGNORE, this change should
+ not cause any differences in behavior.
+
+ Do not use issetugid on Solaris to determine when to avoid refreshing
+ the ticket cache named in KRB5CCNAME during pam_setcred. Instead,
+ compare effective and real UID and GID and permit KRB5CCNAME to be
+ trusted if they match. This allows setuid screensavers on Solaris to
+ refresh ticket caches and makes behavior on Solaris match other
+ platforms. Using issetugid is arguably safer since it protects
+ programs that switch users via setuid to a user other than the calling
+ user but still should not trust the original environment, but such
+ programs are rare in the PAM context and should not be calling
+ pam_setcred anyway unless the calling user is permitted to generally
+ act as the target user. Thanks, William Yang.
+
+ Do the same logging in pam_sm_open_session and pam_sm_close_session as
+ we do with the other functions. This will mean pam_sm_open_session
+ calls will be logged as pam_sm_open_session, not as pam_sm_setcred as
+ before.
+
+ pam-krb5 is now built using Automake and Libtool to bring it more in
+ line with other software packages. This means that it now relies on
+ Libtool to know how to generate a loadable module rather than
+ hand-configured linker rules. This may improve portability on some
+ platforms and may hurt it on other platforms.
+
+ If configured with a prefix of /usr on Linux, use /lib, /lib32, or
+ /lib64 as an installation path based on the size of an integer in the
+ compilation environment rather than based on known 64-bit Linux
+ variants.
+
+ Update to rra-c-util 2.0:
+
+ * Sanity-check the results of krb5-config before proceeding.
+ * Fall back on manual probing if krb5-config results don't work.
+ * Don't break if the user clobbers CPPFLAGS at build time.
+
+pam-krb5 3.15 (2009-07-21)
+
+ Fix a segfault (null pointer dereference) if pam-krb5 is configured
+ with use_first_pass or use_authtok and there is no password stored in
+ the PAM stack. Thanks to Jonathan Guthrie for the bug report.
+
+pam-krb5 3.14 (2009-07-18)
+
+ Return PAM_IGNORE instead of PAM_PERM_DENIED from pam_chauthtok for
+ ignored users. This allows making the Kerberos PAM module mandatory
+ for password changes and still falling back to other PAM modules for
+ ignored users. Thanks, Steve Langasek.
+
+ Always treat the empty password as an authentication failure rather
+ than passing it to the Kerberos libraries. The Kerberos libraries
+ may treat it as equivalent to no password and prompt for a password
+ without our knowledge, leading to the user authenticating with a
+ different password than the one stored in the PAM stack. This could
+ cause unexpected problems with some PAM configurations. It's safer
+ to make the assumption that the empty password is always invalid and
+ reject it outside of the Kerberos libraries. Thanks, Sanjay Sha.
+
+ Fix error handling if ticket cache initialization fails.
+ Authentication will still fail, but this avoids a segfault from a
+ double-free of the ticket cache structure. The most common cause of
+ this problem was having the attempt to initialize the ticket cache
+ be blocked by AppArmor. Thanks to Alex Mauer for the report.
+
+ Call krb5_free_error_string correctly, fixing a portability issue
+ when building against Heimdal. Thanks, Andrew Drake.
+
+ Work around a deficiency in pam_putenv on FreeBSD 7.2 that doesn't
+ allow deleting environment variables, only setting them to empty
+ values. Thanks, Andrew Elble.
+
+pam-krb5 3.13 (2009-02-11)
+
+ SECURITY: When built against MIT Kerberos, if pam_krb5 is called in a
+ setuid context (effective UID or GID doesn't match the real UID or
+ GID), use krb5_init_secure_context instead of krb5_init_context. This
+ ignores environment variable settings for the local Kerberos
+ configuration and keytab. Previous versions could allow a local
+ attacker to point a setuid program that used PAM authentication at a
+ different Kerberos configuration under the attacker's control,
+ possibly resulting in privilege escalation. Heimdal handles this
+ logic within the Kerberos libraries and therefore was not affected.
+ (CVE-2009-0360)
+
+ SECURITY: Disable pam_setcred(PAM_REINITIALIZE_CREDS) for setuid
+ applications. If pam_krb5 detects this call in a setuid context, it
+ now logs an error and returns success without doing anything. Solaris
+ su calls pam_setcred with that option rather than PAM_ESTABLISH_CREDS
+ after authentication and without wiping the environment, leading
+ previous versions of pam_krb5 to trust the KRB5CCNAME environment
+ variable for the ticket cache location. This permitted an attacker to
+ use previous versions of pam_krb5 to overwrite arbitrary files with
+ Kerberos credential caches that were left owned by the attacker.
+ Setuid screen lock programs may also be affected. Discovered by Derek
+ Chan and reported by Steven Luo. Thanks to Sam Hartman and Jeffrey
+ Hutzelman for additional analysis. (CVE-2009-0361)
+
+ If a prefix of /usr is requested at configure time, install the PAM
+ module into /lib/security or /lib64/security on Linux, matching the
+ standard Linux-PAM module location. Use lib64 instead of lib on
+ 64-bit SPARC, PowerPC, and S390 Linux as well as x86_64. Patch from
+ Peter Breitenlohner.
+
+ Fix a build problem when builddir != srcdir introduced in 3.11. Patch
+ from Peter Breitenlohner.
+
+ Add support for the old Heimdal krb5_get_error_string interface.
+ Thanks, Chaskiel Grundman.
+
+ Add --with-krb5-include and --with-krb5-lib configure options to allow
+ more specific setting of paths if necessary.
+
+ If krb5-config isn't available, attempt to determine if the library
+ directory for the Kerberos libraries is lib32 or lib64 instead of lib
+ and set LDFLAGS accordingly. Based on an idea from the CMU Autoconf
+ macros.
+
+pam-krb5 3.12 (2008-11-13)
+
+ Add alt_auth_map configuration option, which allows mapping of
+ usernames to alternative Kerberos principals, useful primarily for
+ using particular instances for access to a given PAM-authenticated
+ service. Also added force_alt_auth and only_alt_auth options to
+ control when alternative Kerberos principals are used. Patch from
+ Booker Bense.
+
+ Fix incorrect error handling for bad .k5login ownership when
+ search_k5login is set, leading to a NULL pointer dereference and a
+ segfault. Thanks, Andrew Deason.
+
+ Fix double-free of the ticket cache structure if creation of the
+ ticket cache in the session module fails. Thanks, Jens Jorgensen.
+
+ Log all syslog messages to LOG_AUTHPRIV, or LOG_AUTH if the system
+ doesn't define LOG_AUTHPRIV. Thanks, Mark Painter.
+
+ Fix portability to AIX's bundled Kerberos. Thanks, Markus Moeller.
+
+ When debugging is enabled, log an exit status of PAM_IGNORE as ignore
+ rather than failure.
+
+ Document that pam-krb5 must be listed in the session group as well as
+ the auth group for interactive logins or OpenSSH won't set up the
+ user's credential cache properly.
+
+ Document adding ignore=ignore to complex [] action configuration for
+ the session and account groups since the module now returns PAM_IGNORE
+ instead of PAM_SUCCESS for accounts that didn't use Kerberos.
+
+pam-krb5 3.11 (2008-07-10)
+
+ pam_setcred, pam_open_session, and pam_acct_mgmt now return PAM_IGNORE
+ for ignored users or non-Kerberos logins rather than PAM_SUCCESS.
+ This return code tells the PAM library to continue as if the module
+ were not present in the configuration and allows sufficient to be
+ meaningful for pam-krb5 in account and session groups.
+ pam_authenticate continues to return failure for ignored users;
+ PAM_IGNORE would arguably be more correct, but increases the risk of
+ security holes through incorrect configuration.
+
+ Support correct password expiration handling according to the PAM
+ standard (returning success from pam_authenticate and an error from
+ pam_acct_mgmt and completing the authentication after pam_chauthotk).
+ This is not the default since it opens security holes with broken
+ applications that don't call pam_acct_mgmt or ignore its exit status.
+ To enable it, set the PAM option defer_pwchange for applications known
+ to make the correct PAM calls and check return codes.
+
+ Add a new option to attempt change of expired passwords during
+ pam_authenticate if Kerberos authentication returns a password expired
+ error. Normally, the Kerberos library will do this for you, but some
+ Kerberos libraries (notably Solaris) disable that code. This option
+ allows simulation of the normal Kerberos library behavior on those
+ platforms.
+
+ Work around an apparent Heimdal bug when krb5_free_cred_contents is
+ called on an all-zero credential structure. It's not clear what's
+ going on here and the Heimdal code looks correct, but avoiding the
+ call fixes the problem.
+
+ Warn if more than one of use_authtok, use_first_pass, and
+ try_first_pass is set and use the strongest of the one set.
+
+ Remove the workaround for versions of MIT Kerberos that didn't
+ initialize a krb5_get_init_creds_opt structure on opt_alloc. This bug
+ was only present in early versions of 1.6; the correct fix is to
+ upgrade.
+
+ Add an additional header check for AIX's bundled Kerberos.
+
+ If KRB5_CONFIG was explicitly set in the environment, don't use a
+ different krb5-config based on --with-krb5. If krb5-config isn't
+ executable, don't use it. This allows one to force library probing by
+ setting KRB5_CONFIG to point to a nonexistent file.
+
+ Sanity-check the results of krb5-config before proceeding and error
+ out in configure if they don't work.
+
+ For Kerberos libraries without krb5-config, also check for networking
+ libraries (-lsocket and friends) before checking for Kerberos
+ libraries in case shared library dependencies are broken.
+
+ Fix Autoconf syntax error when probing for libkrb5support. Thanks,
+ Mike Garrison.
+
+ Set an explicit visibility of hidden for all internal functions at
+ compile time if gcc is used to permit better optimization. Hide all
+ functions except the official interfaces using a version script on
+ Linux. This protects against leaking symbols into the application
+ namespace and provides some mild optimization benefit.
+
+ Fix the probing of PAM headers for const on Mac OS X. This will
+ suppress some harmless compiler warnings there. Thanks, Markus
+ Moeller.
+
+pam-krb5 3.10 (2007-12-28)
+
+ The workaround for krb5_get_init_creds_opt_alloc problems in MIT
+ Kerberos 1.6 broke PKINIT support with Heimdal. Only apply that
+ workaround when building against the MIT Kerberos libraries. Thanks
+ to Jaakko Pero for the detailed report.
+
+ If no_ccache is set, always exit successfully from pam_setcred or
+ pam_open_session, even if we couldn't retrieve module data. Thanks,
+ Markus Moeller.
+
+ When keytab is set, properly handle failure to create a keytab cursor
+ and don't assume that the cursor is valid. Thanks, Markus Moeller.
+
+ Define _ALL_SOURCE on AIX to get prototypes for snprintf.
+
+ Add additional portability glue and Autoconf probes to support
+ building against the version of Kerberos bundled with AIX. Support
+ for this should be considered alpha in this release. Thanks to Markus
+ Moeller for the initial patch.
+
+pam-krb5 3.9 (2007-11-12)
+
+ If use_authtok is set, fail even if we can retrieve the stored PAM
+ password if that password is set to NULL. Apparently that can happen
+ in some cases, such as with pam_cracklib. Thanks to Christian Holler
+ for the diagnosis and a patch.
+
+ Add a new clear_on_fail option for the password group. If set, when a
+ password change fails, set PAM_AUTHTOK to NULL so that subsequent
+ modules in the PAM stack with use_authtok set will also fail. Just
+ returning failure doesn't abort the stack on the second pass when
+ actual password changes are made. This is not the default since it
+ interferes with other desirable PAM configurations. It's useful
+ primarily when using the PAM stack to synchronize passwords between
+ multiple environments. Thanks to Christian Holler and Tomas Mraz for
+ the analysis.
+
+ Fix portability issues with Heimdal, versions of PAM that don't
+ provide pam_modutil_getpwnam, and compiler warnings when building
+ PKINIT support. Thanks, Martin von Gagern.
+
+ Fix parsing of the keytab PAM option. Thanks, Markus Moeller.
+
+ Return PAM_AUTHINFO_UNAVAIL instead of PAM_AUTH_ERR when unable to
+ resolve the Kerberos realm. Thanks, Frank Cornelissen.
+
+ Add a new debugging section to the README.
+
+pam-krb5 3.8 (2007-09-30)
+
+ krb5_get_init_creds_opt_alloc doesn't initialize the returned
+ structure with the default flags in MIT Kerberos 1.6, which meant that
+ users with expired passwords were not being prompted to change their
+ password but just rejected. Fixed by always calling _init before
+ setting the credential flags, regardless of the provenance of the opt
+ structure. Thanks, Michael Richters.
+
+ Fix configure and Makefile glue so that Mac OS X and HP-UX have a
+ chance of working (still untested).
+
+ Add a make warnings target with aggressive gcc warning options. Treat
+ negative minimum UIDs as zero so that UID comparisons can always be
+ done unsigned. Add casts and unused attributes as needed.
+
+pam-krb5 3.7 (2007-09-29)
+
+ If given an explicit keytab path to use for credential verification,
+ use the first principal found in that keytab as the principal for
+ verification rather than the library default (which is normally the
+ host/* principal for the local system and may not be found in that
+ keytab).
+
+ When authenticating, don't store our context data until after
+ authentication has succeeded. Otherwise, we may destroy the ticket
+ cache of a previous successful authentication. This bug would only
+ affect configurations where pam_krb5 was run multiple times with
+ different settings, such as multiple realms. Thanks to Dave Botsch
+ for the report.
+
+ Use pam_modutil_getpwnam instead of getpwnam if available for better
+ thread safety.
+
+ Don't store PAM data unless we're saving a ticket cache. All other
+ calls use it for is to find the ticket cache, so without a cache it's
+ pointless and means we run the risk of stomping on ourselves in
+ multithreaded programs.
+
+ Still canonicalize the PAM user before returning when not saving a
+ ticket cache.
+
+ Fix determination of linker flags on non-x86_64 Linux. Always link
+ with -fPIC when using GCC, just in case.
+
+ Add compilation options for Mac OS X and HP-UX (untested).
+
+ Use pam_krb5 instead of ctx for our PAM data name to reduce the
+ chances of collision.
+
+pam-krb5 3.6 (2007-09-18)
+
+ When the local user doesn't exist and search_k5login is enabled, fall
+ back to simple Kerberos authentication just as if the account existed
+ with no .k5login file. This avoids trying to verify an all-zero
+ credentials structure, leading to non-expoloitable segfaults on x86_64
+ systems. Be more careful in general about setting error codes in the
+ search_k5login implementation.
+
+ Explicitly clear the forwardable and proxiable options and don't ask
+ for renewable tickets when getting a ticket for the password changing
+ service. Otherwise, system-wide defaults and PAM configuration will
+ apply to those tickets as well and the resulting ticket request may be
+ rejected based on KDC configuration. Based on a patch by Sergio
+ Gelato.
+
+ Do username canonicalization earlier so that .k5login checking and
+ similar work uses the correct username but only change the PAM
+ username if authentication succeeds. Document that username
+ canonicalization won't work with unmodified OpenSSH and with several
+ common PAM modules. Thanks to R. Scott Bailey for the bug report and
+ analysis.
+
+ Add a prompt_principal option which, if set, causes the PAM module to
+ prompt the user for the Kerberos principal to use for authentication
+ before prompting for the password.
+
+ Try to determine whether the PAM headers use const in the prototypes
+ of such things as pam_get_item and adjust accordingly. This should
+ address most compiler warnings on Solaris. Thanks, Markus Moeller.
+
+ Change lib to lib64 on x86_64 Linux to allow for the magical $ISA
+ parameter in Red Hat's PAM configuration. Hopefully this won't cause
+ problems elsewhere.
+
+ Support DESTDIR for make install.
+
+pam-krb5 3.5 (2007-04-10)
+
+ Don't try to chown non-FILE ticket caches, which among other things
+ breaks using pam-krb5 with Heimdal KCM caches. Thanks, Jeremy
+ Jackson.
+
+ When logging session deletion via pam_setcred or pam_close_session,
+ don't look for the username in the PAM context after it's been freed.
+ Thanks, Markus Moeller.
+
+ Map more Kerberos status codes to PAM status codes for authentication
+ errors.
+
+pam-krb5 3.4 (2007-01-28)
+
+ More compilation fixes for Heimdal 0.7, which has a pkinit function
+ but takes a different number of arguments. Thanks, Morgan LEFIEUX.
+
+ Never call error_message directly on Heimdal. krb5_get_err_text can
+ cope with a NULL context and krb5-config on Heimdal doesn't include
+ -lcom_err.
+
+ Handle a NULL return from krb5_get_error_message, since that seems
+ possible in some edge cases.
+
+ Call krb5_get_error_message on Heimdal as well if it's available,
+ since it's supported by the 0.8 release candidates.
+
+pam-krb5 3.3 (2007-01-24)
+
+ Support the new MIT Kerberos error message functions.
+
+ Fix compilation errors in the Heimdal PKINIT support and don't be
+ confused by a similar function in the MIT Kerberos PKINIT branch.
+ Thanks to Douglas E. Engert for the testing and patch.
+
+ Fix compilation errors with Heimdal 0.7, which has some of the PKINIT
+ functions but doesn't define the same error codes. Thanks, Morgan
+ LEFIEUX.
+
+ Initial support for the MIT Kerberos PKINIT branch, which uses a
+ different mechanism for configuring PKINIT support than Heimdal. Also
+ support configuration of general preauth parameters for the MIT
+ preauth plugin system via the preauth_opt option. Thanks to Douglas
+ E. Engert for the initial patch.
+
+ If use_pkinit is set in the PAM configuration and PKINIT isn't
+ available or cannot be forced, always fail authentication.
+
+pam-krb5 3.2 (2007-01-16)
+
+ This release fixes numerous bugs all identified by Douglas E. Engert
+ while testing with Heimdal and PKINIT support. Thank you!
+
+ Rewrite the code to drop the credlist data structure since we only
+ ever have one set of credentials, allocate new krb5_creds objects, and
+ do proper memory management, which should plug some memory leaks of
+ the contents of krb5_creds objects.
+
+ Probe for the correct Heimdal function to set default initial
+ credential options.
+
+ Prefix the default cache path with "FILE:" to make the cache type
+ explicit.
+
+ Fix installation of the manual page when building from a different
+ directory than the source directory.
+
+ Fix several compilation errors with the PKINIT support with Heimdal
+ 0.8rc1 or later. This code should still be considered alpha-quality.
+
+pam-krb5 3.1 (2007-01-03)
+
+ Fix an infinite loop with failed Kerberos authentication and a doubled
+ colon that causes a syntax error with some compilers. Thanks, Markus
+ Moeller.
+
+ Move the check for users we should ignore to pam_sm_authenticate
+ from pamk5_password_auth so that it's consistently done in the API
+ function. This also avoids bogus log messages when authenticating as
+ an ignored user with debug enabled.
+
+pam-krb5 3.0 (2006-12-18)
+
+ Add preliminary PKINIT support, contributed by Douglas E. Engert.
+ I reorganized and refactored the code extensively and it therefore may
+ not compile; until it has received more testing, it should be
+ considered alpha-quality. Currently, PKINIT support requires Heimdal
+ 0.8rc1 or later.
+
+ Add a keytab configuration option to use a different keytab for
+ initial credential validation.
+
+ Add a ticket_lifetime configuration option to set the lifetime of
+ obtained credentials.
+
+ Add the banner and expose_account configuration options, which control
+ the prompts for authentication and password changing. Provide more
+ informative prompts when changing passwords.
+
+ Work around a bug in MIT Kerberos prior to 1.4 causing the library to
+ cache the default realm and assume a particular realm even if the
+ default realm is later changed. This bug prevented running two
+ instances of pam-krb5 with different realm settings in the same PAM
+ stack. Thanks, Dave Botsch.
+
+ Honor PAM_SILENT when the Kerberos library prompts for more
+ information, passing to the application only prompts.
+
+ If PAM_USER is set to a fully-qualified principal that the Kerberos
+ library can map to a local account name, reset PAM_USER to that local
+ account name after authentication.
+
+ Avoid memory leaks in the Kerberos prompter by freeing the PAM
+ response strings. We were already doing this elsewhere and the world
+ didn't end, so assume that it's safe for the PAM module to do this.
+ Also avoid memory leaks in some unusual error conditions.
+
+ Return unknown user rather than internal error when attempting
+ authentication of a user we're supposed to ignore.
+
+ When debug is enabled, report the principal for which we're attempting
+ authentication to help catch realm configuration errors.
+
+ Document the broken behavior of old versions of OpenSSH, which tell
+ PAM to refresh credentials rather than opening a session. Thanks,
+ Michael C. Garrison.
+
+ Add a link to the distribution page to the pam-krb5 man page.
+
+ Extensive refactoring and reorganization of the code.
+
+pam-krb5 2.6 (2006-11-28)
+
+ Don't assume the pointer set by pam_get_user is usable over the life
+ of the PAM module; instead, save a local copy.
+
+ Avoid a use of already freed memory when debugging is enabled.
+
+ Use __func__ instead of __FUNCTION__ and provide a fallback for older
+ versions of gcc and for systems that support neither. Should fix
+ compilation issues with Sun's C compiler.
+
+ On platforms where we know the appropriate compiler flags, try to
+ build the module so that symbols are resolved within the module in
+ preference to any externally available symbols. Also add the
+ hopefully correct compiler flags for Sun's C compiler.
+
+pam-krb5 2.5 (2006-11-03)
+
+ Don't free the results of pam_get_item(PAM_AUTHTOK) when changing
+ passwords. Thanks, Arne Nordmark.
+
+ Be a bit more thorough when checking authorization in
+ pam_sm_acct_mgmt. Re-retrieve the value of user in case the
+ application changed it, and if we have a ticket cache (we may not even
+ after a successful authentication if no_ccache was specified),
+ retrieve the principal from it rather than using the principal from
+ the context.
+
+ Overwrite passwords with 0 before freeing them, just out of paranoia
+ (and because PAM also does this internally).
+
+pam-krb5 2.4 (2006-10-05)
+
+ Fix compilation problems with Heimdal. Thanks, Matthijs Mohlmann and
+ Douglas Engert.
+
+ Check for memory allocation failures when parsing PAM options rather
+ than segfaulting.
+
+ Fix several places where an uninitialized context could have been
+ passed into the argument parsing function.
+
+ Refactor the code to read configuration from krb5.conf to be easier
+ to read and understand. Parse renew_lifetime immediately and always
+ report an error rather than deferring time parsing until acquiring
+ tickets.
+
+ Log errors (not just authentication failures) at the LOG_ERR level
+ to match (some of) the recommendations of the Linux PAM documentation.
+
+ Log an error when an unknown option is passed via the PAM
+ configuration.
+
+pam-krb5 2.3 (2006-09-03)
+
+ Fix the interface between the Kerberos prompting function and the
+ PAM conversation function on Linux. Prior to this fix, the PAM module
+ would only work on Solaris if Kerberos passed multiple prompts, which
+ happens when an account requires a password change. Solaris and Linux
+ PAM implementations expect a different structure of pam_message
+ structs in the conversation function; use a workaround to cater to
+ both of them. Based on a patch by Joachim Keltsch.
+
+ Implement retain_after_close, which specifies that the PAM module
+ should never destroy the user's ticket cache, even on session end.
+
+ Adjust for the differences in Solaris's PAM libraries: Include
+ pam_appl.h everywhere for structure and type definitions, and add
+ portability workarounds for the return statuses missing from the
+ Solaris implementation.
+
+pam-krb5 2.2 (2006-08-28)
+
+ Allow the default realm to be overridden in the PAM options.
+
+ Use the realm, default or otherwise, when reading options from
+ krb5.conf so that realm-specific sections in [appdefaults] work
+ correctly.
+
+ Update the build and installation documentation for the new
+ Autoconf-based build system. This should have been in the last
+ release but was missed.
+
+ Initialize ticket options correctly when built with Heimdal.
+
+ Fix a typo that caused the Heimdal support not to compile. Thanks,
+ Matthijs Mohlmann.
+
+pam-krb5 2.1 (2006-08-26)
+
+ Strip off a FILE: prefix from the cache path before creating it in
+ case the user set ccache or ccache_dir with a cache type prefix.
+ Thanks to Björn Torkelsson for the patch.
+
+ Added an Autoconf script to distinguish between Heimdal and MIT
+ Kerberos and take care of other portability issues. Rewrote the
+ Makefile accordingly.
+
+ Added portability and error reporting fixes for Heimdal, thanks to
+ Matthijs Mohlmann.
+
+pam-krb5 2.0 (2006-08-11)
+
+ Always use a disk cache for temporary storage of credentials between
+ authentication and setcred or session initialization. This allows the
+ module to work correctly with OpenSSH ChallengeResponseAuthentication.
+
+ Add support for some PAM options that were supported by the
+ Sourceforge K5 PAM module, most notably minimum_uid and
+ renew_lifetime.
+
+ Support setting many PAM options from krb5.conf as well as on the PAM
+ command line, using the same application section as the Sourceforge
+ PAM module. Use the profile reading functions provided by the
+ Kerberos libraries.
+
+ Add support for use_authtok, which is like use_first_pass except that
+ it will never prompt even if no password is currently set.
+
+ Add a search_k5login option to check the user's password against every
+ principal listed in .k5login, to support use of this module to
+ authenticate user access to shared accounts.
+
+ Add an ignore_k5login option that bypasses all checks of .k5login
+ files entirely and relies solely on krb5_aname_to_localname checks.
+
+ Re-add the ccache option to specify the exact file name of the ticket
+ cache, and allow for randomization using mkstemp even when this option
+ is used.
+
+ Only call krb5_kuserok (the .k5login check) when the account to which
+ the user is authenticating is a local account. It's up to the
+ application to handle authorization checks for non-local accounts.
+
+ Support preliminary checks for password changing by using that to
+ obtain the user's current credentials. Correctly handle saved
+ passwords from previous authentications or password changes when
+ changing passwords, and correctly set the saved passwords for
+ subsequent password changes in the PAM stack.
+
+ Only initialize the ticket cache once, no matter how many times
+ setcred is called. This saves duplicate work and works around a bug
+ in X.org xdm that otherwise causes it to lose the PAM environment.
+
+ When reinitializing a ticket cache, never reinitialize the temporary
+ cache created by the authentication call. Instead, fall back to the
+ default ticket cache name if KRB5CCNAME isn't set.
+
+ Improve support for no_ccache. Now, it doesn't even generate a
+ temporary ticket cache during authentication but only uses an
+ in-memory credential list.
+
+ Do user ticket validation using the standard Kerberos library call
+ rather than rolling our own code. This means that the user can now
+ set options in krb5.conf to control whether that call should fail if
+ the local keytab isn't readable or contains no usable keys.
+
+ Completely rewrite the man page. Clean it up and make it more
+ readable and fully document all of the options. Also rewrite the
+ README file and clean up the rest of the package documentation.
+
+ Don't create a ticket cache until after successful authentication.
+
+ Understand the FILE: prefix to Kerberos ticket cache names and compare
+ and chown ticket caches properly with that prefix.
+
+ Add a trailing nul to the password in the Kerberos prompter function,
+ since some code relies on it being there.
+
+ Review the return status of each PAM function and ensure that we only
+ return failure statuses that are supported for that function.
+
+ Rename all internal functions with a pamk5_* prefix to avoid
+ conflicting with any application or system library functions.
+
+ Eliminate global variables in the PAM module and do a better job at
+ cleaning up memory usage. There are still a few places where the PAM
+ conversation functions may leak memory due to an incomplete
+ specification in the PAM API on who should free what memory.
+
+ The logging messages produced when debug is set should now be more
+ consistent and more complete.
+
+pam-krb5 1.2 (2005-09-27)
+
+ Don't reinitialize the ticket cache if the old and new cache have the
+ same name, since otherwise we end up destroying it.
+
+ Always set KRB5CCNAME, even when reinitializing.
+
+ When reinitializing, look for the ticket cache in the saved context
+ even if KRB5CCNAME isn't set. OpenSSH calls it this way.
+
+ Drop the ccache option and add ccache_dir instead, which only
+ specifies the directory for ticket caches and is therefore easier to
+ implement.
+
+pam-krb5 1.1 (2005-08-31)
+
+ Add support for reinitialization/refreshing of credentials in
+ pam_sm_setcred.
+
+ Set PAM_AUTHTOK and PAM_OLDAUTHTOK when authenticating to better
+ support stacking this module with others.
+
+ Add an ignore_root option to not do anything when the account to which
+ the user is authenticating is root. This allows one to log in via
+ console as root even when the network is down (thereby breaking the
+ PAM module in ways that login doesn't like due to timeouts in the
+ Kerberos libraries).
+
+ Store the entire context structure in PAM's memory rather than just
+ the name of the ticket cache so that we can pass around more data to
+ ourself.
+
+ Bring errors more in line with the official PAM specification.
+
+ Move prompt generation into the PAM module rather than letting the
+ Kerberos library generate the prompt. This way we don't leak
+ principal information to the caller, and the non-standard prompt also
+ broke some applications like gksudo.
+
+ Support session management and destruction of the ticket cache on
+ close of session.
+
+ Don't require that the user have a local account on the system.
+
+ Include the user UID in the default ticket cache name so that rpc.gssd
+ and similar programs can find it.
diff --git a/README b/README
new file mode 100644
index 000000000000..3b7cb5c886dc
--- /dev/null
+++ b/README
@@ -0,0 +1,641 @@
+ pam-krb5 4.11
+ (PAM module for Kerberos authentication)
+ Maintained by Russ Allbery <eagle@eyrie.org>
+
+ Copyright 2005-2010, 2014-2015, 2017, 2020-2021 Russ Allbery
+ <eagle@eyrie.org>. Copyright 2009-2011 The Board of Trustees of the
+ Leland Stanford Junior University. Copyright 2005 Andres Salomon
+ <dilinger@debian.org>. Copyright 1999-2000 Frank Cusack
+ <fcusack@fcusack.com>. This software is distributed under a BSD-style
+ license. Please see the section LICENSE below for more information.
+
+BLURB
+
+ pam-krb5 is a Kerberos PAM module for either MIT Kerberos or Heimdal.
+ It supports ticket refreshing by screen savers, configurable
+ authorization handling, authentication of non-local accounts for network
+ services, password changing, and password expiration, as well as all the
+ standard expected PAM features. It works correctly with OpenSSH, even
+ with ChallengeResponseAuthentication and PrivilegeSeparation enabled,
+ and supports extensive configuration either by PAM options or in
+ krb5.conf or both. PKINIT is supported with recent versions of both MIT
+ Kerberos and Heimdal and FAST is supported with recent MIT Kerberos.
+
+DESCRIPTION
+
+ pam-krb5 provides a Kerberos PAM module that supports authentication,
+ user ticket cache handling, simple authorization (via .k5login or
+ checking Kerberos principals against local usernames), and password
+ changing. It can be configured through either options in the PAM
+ configuration itself or through entries in the system krb5.conf file,
+ and it tries to work around PAM implementation flaws in commonly-used
+ PAM-enabled applications such as OpenSSH and xdm. It supports both
+ PKINIT and FAST to the extent that the underlying Kerberos libraries
+ support these features.
+
+ This is not the Kerberos PAM module maintained on Sourceforge and used
+ on Red Hat systems. It is an independent implementation that, if it
+ ever shared any common code, diverged long ago. It supports some
+ features that the Sourceforge module does not (particularly around
+ authorization), and does not support some options (particularly ones not
+ directly related to Kerberos) that it does. This module will never
+ support Kerberos v4 or AFS. For an AFS session module that works with
+ this module (or any other Kerberos PAM module), see pam-afs-session [1].
+
+ [1] https://www.eyrie.org/~eagle/software/pam-afs-session/
+
+ If there are other options besides AFS and Kerberos v4 support from the
+ Sourceforge PAM module that you're missing in this module, please let me
+ know.
+
+REQUIREMENTS
+
+ Either MIT Kerberos (or Kerberos implementations based on it) or Heimdal
+ are supported. MIT Keberos 1.3 or later may be required; this module
+ has not been tested with earlier versions.
+
+ For PKINIT support, Heimdal 0.8rc1 or later or MIT Kerberos 1.6.3 or
+ later are required. Earlier MIT Kerberos 1.6 releases have a bug in
+ their handling of PKINIT options. MIT Kerberos 1.12 or later is
+ required to use the use_pkinit PAM option.
+
+ For FAST (Flexible Authentication Secure Tunneling) support, MIT
+ Kerberos 1.7 or higher is required. For anonymous FAST support,
+ anonymous authentication (generally anonymous PKINIT) support is
+ required in both the Kerberos libraries and in the local KDC.
+
+ This module should work on Linux and build with gcc or clang. It may
+ still work on Solaris and build with the Sun C compiler, but I have only
+ tested it on Linux recently. There is beta-quality support for the AIX
+ NAS Kerberos implementation that has not been tested in years. Other
+ PAM implementations will probably require some porting, although
+ untested build system support is present for FreeBSD, Mac OS X, and
+ HP-UX. I personally can only test on Linux and rely on others to report
+ problems on other operating systems.
+
+ Old versions of OpenSSH are known to call pam_authenticate followed by
+ pam_setcred(PAM_REINITIALIZE_CRED) without first calling
+ pam_open_session, thereby requesting that an existing ticket cache be
+ renewed (similar to what a screensaver would want) rather than
+ requesting a new ticket cache be created. Since this behavior is
+ indistinguishable at the PAM level from a screensaver, pam-krb5 when
+ used with these old versions of OpenSSH will refresh the ticket cache of
+ the OpenSSH daemon rather than setting up a new ticket cache for the
+ user. The resulting ticket cache will have the correct permissions
+ (this is not a security concern), but will not be named correctly or
+ referenced in the user's environment and will be overwritten by the next
+ user login. The best solution to this problem is to upgrade OpenSSH.
+ I'm not sure exactly when this problem was fixed, but at the very least
+ OpenSSH 4.3 and later do not exhibit it.
+
+ To bootstrap from a Git checkout, or if you change the Automake files
+ and need to regenerate Makefile.in, you will need Automake 1.11 or
+ later. For bootstrap or if you change configure.ac or any of the m4
+ files it includes and need to regenerate configure or config.h.in, you
+ will need Autoconf 2.64 or later. Perl is also required to generate
+ manual pages from a fresh Git checkout.
+
+BUILDING AND INSTALLATION
+
+ You can build and install pam-krb5 with the standard commands:
+
+ ./configure
+ make
+ make install
+
+ If you are building from a Git clone, first run ./bootstrap in the
+ source directory to generate the build files. make install will
+ probably have to be done as root. Building outside of the source
+ directory is also supported, if you wish, by creating an empty directory
+ and then running configure with the correct relative path.
+
+ The module will be installed in /usr/local/lib/security by default, but
+ expect to have to override this using --libdir. The correct
+ installation path for PAM modules varies considerably between systems.
+ The module will always be installed in a subdirectory named security
+ under the specified value of --libdir. On Red Hat Linux, for example,
+ --libdir=/usr/lib64 is appropriate to install the module into the system
+ PAM directory. On Debian's amd64 architecture,
+ --libdir=/usr/lib/x86_64-linux-gnu would be correct.
+
+ Normally, configure will use krb5-config to determine the flags to use
+ to compile with your Kerberos libraries. To specify a particular
+ krb5-config script to use, either set the PATH_KRB5_CONFIG environment
+ variable or pass it to configure like:
+
+ ./configure PATH_KRB5_CONFIG=/path/to/krb5-config
+
+ If krb5-config isn't found, configure will look for the standard
+ Kerberos libraries in locations already searched by your compiler. If
+ the the krb5-config script first in your path is not the one
+ corresponding to the Kerberos libraries you want to use, or if your
+ Kerberos libraries and includes aren't in a location searched by default
+ by your compiler, you need to specify a different Kerberos installation
+ root via --with-krb5=PATH. For example:
+
+ ./configure --with-krb5=/usr/pubsw
+
+ You can also individually set the paths to the include directory and the
+ library directory with --with-krb5-include and --with-krb5-lib. You may
+ need to do this if Autoconf can't figure out whether to use lib, lib32,
+ or lib64 on your platform.
+
+ To not use krb5-config and force library probing even if there is a
+ krb5-config script on your path, set PATH_KRB5_CONFIG to a nonexistent
+ path:
+
+ ./configure PATH_KRB5_CONFIG=/nonexistent
+
+ krb5-config is not used and library probing is always done if either
+ --with-krb5-include or --with-krb5-lib are given.
+
+ Pass --enable-silent-rules to configure for a quieter build (similar to
+ the Linux kernel). Use make warnings instead of make to build with full
+ compiler warnings (requires either GCC or Clang and may require a
+ relatively current version of the compiler).
+
+ You can pass the --enable-reduced-depends flag to configure to try to
+ minimize the shared library dependencies encoded in the binaries. This
+ omits from the link line all the libraries included solely because other
+ libraries depend on them and instead links the programs only against
+ libraries whose APIs are called directly. This will only work with
+ shared libraries and will only work on platforms where shared libraries
+ properly encode their own dependencies (this includes most modern
+ platforms such as all Linux). It is intended primarily for building
+ packages for Linux distributions to avoid encoding unnecessary shared
+ library dependencies that make shared library migrations more difficult.
+ If none of the above made any sense to you, don't bother with this flag.
+
+TESTING
+
+ pam-krb5 comes with a comprehensive test suite, but it requires some
+ configuration in order to test anything other than low-level utility
+ functions. For the full test suite, you will need to have a running KDC
+ in which you can create two test accounts, one with admin access to the
+ other. Using a test KDC environment, if you have one, is recommended.
+
+ Follow the instructions in tests/config/README to configure the test
+ suite.
+
+ Now, you can run the test suite with:
+
+ make check
+
+ If a test fails, you can run a single test with verbose output via:
+
+ tests/runtests -o <name-of-test>
+
+ Do this instead of running the test program directly since it will
+ ensure that necessary environment variables are set up.
+
+ The default libkadm5clnt library on the system must match the
+ implementation of your KDC for the module/expired test to work, since
+ the two kadmin protocols are not compatible. If you use the MIT library
+ against a Heimdal server, the test will be skipped; if you use the
+ Heimdal library against an MIT server, the test suite may hang.
+
+ Several module/expired tests are expected to fail with Heimdal 1.5 due
+ to a bug in Heimdal with reauthenticating immediately after a
+ library-mediated password change of an expired password. This is fixed
+ in later releases of Heimdal.
+
+ To run the full test suite, Perl 5.10 or later is required. The
+ following additional Perl modules will be used if present:
+
+ * Test::Pod
+ * Test::Spelling
+
+ All are available on CPAN. Those tests will be skipped if the modules
+ are not available.
+
+ To enable tests that don't detect functionality problems but are used to
+ sanity-check the release, set the environment variable RELEASE_TESTING
+ to a true value. To enable tests that may be sensitive to the local
+ environment or that produce a lot of false positives without uncovering
+ many problems, set the environment variable AUTHOR_TESTING to a true
+ value.
+
+CONFIGURING
+
+ Just installing the module does not enable it or change anything about
+ your system authentication configuration. To use the module for all
+ system authentication on Debian systems, put something like:
+
+ auth sufficient pam_krb5.so minimum_uid=1000
+ auth required pam_unix.so try_first_pass nullok_secure
+
+ in /etc/pam.d/common-auth, something like:
+
+ session optional pam_krb5.so minimum_uid=1000
+ session required pam_unix.so
+
+ in /etc/pam.d/common-session, and something like:
+
+ account required pam_krb5.so minimum_uid=1000
+ account required pam_unix.so
+
+ in /etc/pam.d/common-account. The minimum_uid setting tells the PAM
+ module to pass on any users with a UID lower than 1000, thereby
+ bypassing Kerberos authentication for the root account and any system
+ accounts. You normally want to do this since otherwise, if the network
+ is down, the Kerberos authentication can time out and make it difficult
+ to log in as root and fix matters. This also avoids problems with
+ Kerberos principals that happen to match system accounts accidentally
+ getting access to those accounts.
+
+ Be sure to include the module in the session group as well as the auth
+ group. Without the session entry, the user's ticket cache will not be
+ created properly for ssh logins (among possibly others).
+
+ If your users should normally all use Kerberos passwords exclusively,
+ putting something like:
+
+ password sufficient pam_krb5.so minimum_uid=1000
+ password required pam_unix.so try_first_pass obscure md5
+
+ in /etc/pam.d/common-password will change users' passwords in Kerberos
+ by default and then only fall back on Unix if that doesn't work. (You
+ can make this tighter by using the more complex new-style PAM
+ configuration.) If you instead want to synchronize local and Kerberos
+ passwords and change them both at the same time, you can do something
+ like:
+
+ password required pam_unix.so obscure sha512
+ password required pam_krb5.so use_authtok minimum_uid=1000
+
+ If you have multiple environments that you want to synchronize and you
+ don't want password changes to continue if the Kerberos password change
+ fails, use the clear_on_fail option. For example:
+
+ password required pam_krb5.so clear_on_fail minimum_uid=1000
+ password required pam_unix.so use_authtok obscure sha512
+ password required pam_smbpass.so use_authtok
+
+ In this case, if pam_krb5 cannot change the password (due to password
+ strength rules on the KDC, for example), it will clear the stored
+ password (because of the clear_on_fail option), and since pam_unix and
+ pam_smbpass are both configured with use_authtok, they will both fail.
+ clear_on_fail is not the default because it would interfere with the
+ more common pattern of falling back to local passwords if the user
+ doesn't exist in Kerberos.
+
+ If you use a more complex configuration with the Linux PAM [] syntax for
+ the session and account groups, note that pam_krb5 returns a status of
+ ignore, not success, if the user didn't log on with Kerberos. You may
+ need to handle that explicitly with ignore=ignore in your action list.
+
+ There are many, many other possibilities. See the Linux PAM
+ documentation for all the configuration options.
+
+ On Red Hat systems, modify /etc/pam.d/system-auth instead, which
+ contains all of the configuration for the different stacks.
+
+ You can also use pam-krb5 only for specific services. In that case,
+ modify the files in /etc/pam.d for that particular service to use
+ pam_krb5.so for authentication. For services that are using passwords
+ over TLS to authenticate users, you may want to use the ignore_k5login
+ and no_ccache options to the authenticate module. .k5login
+ authorization is only meaningful for local accounts and ticket caches
+ are usually (although not always) only useful for interactive sessions.
+
+ Configuring the module for Solaris is both simpler and less flexible,
+ since Solaris (at least Solaris 8 and 9, which are the last versions of
+ Solaris with which this module was extensively tested) use a single
+ /etc/pam.conf file that contains configuration for all programs. For
+ console login on Solaris, try something like:
+
+ login auth sufficient /usr/local/lib/security/pam_krb5.so minimum_uid=100
+ login auth required /usr/lib/security/pam_unix_auth.so.1 use_first_pass
+ login account required /usr/local/lib/security/pam_krb5.so minimum_uid=100
+ login account required /usr/lib/security/pam_unix_account.so.1
+ login session required /usr/local/lib/security/pam_krb5.so retain_after_close minimum_uid=100
+ login session required /usr/lib/security/pam_unix_session.so.1
+
+ A similar configuration could be used for other services, such as ssh.
+ See the pam.conf(5) man page for more information. When using this
+ module with Solaris login (at least on Solaris 8 and 9), you will
+ probably also need to add retain_after_close to the PAM configuration to
+ avoid having the user's credentials deleted before they are logged in.
+
+ The Solaris Kerberos library reportedly does not support prompting for a
+ password change of an expired account during authentication. Supporting
+ password change for expired accounts on Solaris with native Kerberos may
+ therefore require setting the defer_pwchange or force_pwchange option
+ for selected login applications. See the description and warnings about
+ that option in the pam_krb5(5) man page.
+
+ Some configuration options may be put in the krb5.conf file used by your
+ Kerberos libraries (usually /etc/krb5.conf or /usr/local/etc/krb5.conf)
+ instead or in addition to the PAM configuration. See the man page for
+ more details.
+
+ The Kerberos library, via pam-krb5, will prompt the user to change their
+ password if their password is expired, but when using OpenSSH, this will
+ only work when ChallengeResponseAuthentication is enabled. Unless this
+ option is enabled, OpenSSH doesn't pass PAM messages to the user and can
+ only respond to a simple password prompt.
+
+ If you are using MIT Kerberos, be aware that users whose passwords are
+ expired will not be prompted to change their password unless the KDC
+ configuration for your realm in [realms] in krb5.conf contains a
+ master_kdc setting or, if using DNS SRV records, you have a DNS entry
+ for _kerberos-master as well as _kerberos.
+
+DEBUGGING
+
+ The first step when debugging any problems with this module is to add
+ debug to the PAM options for the module (either in the PAM configuration
+ or in krb5.conf). This will significantly increase the logging from the
+ module and should provide a trace of exactly what failed and any
+ available error information.
+
+ Many Kerberos authentication problems are due to configuration issues in
+ krb5.conf. If pam-krb5 doesn't work, first check that kinit works on
+ the same system. That will test your basic Kerberos configuration. If
+ the system has a keytab file installed that's readable by the process
+ doing authentication via PAM, make sure that the keytab is current and
+ contains a key for host/<system> where <system> is the fully-qualified
+ hostname. pam-krb5 prevents KDC spoofing by checking the user's
+ credentials when possible, but this means that if a keytab is present it
+ must be correct or authentication will fail. You can check the keytab
+ with klist -k and kinit -k.
+
+ Be sure that all libraries and modules, including PAM modules, loaded by
+ a program use the same Kerberos libraries. Sometimes programs that use
+ PAM, such as current versions of OpenSSH, also link against Kerberos
+ directly. If your sshd is linked against one set of Kerberos libraries
+ and pam-krb5 is linked against a different set of Kerberos libraries,
+ this will often cause problems (such as segmentation faults, bus errors,
+ assertions, or other strange behavior). Similar issues apply to the
+ com_err library or any other library used by both modules and shared
+ libraries and by the application that loads them. If your OS ships
+ Kerberos libraries, it's usually best if possible to build all Kerberos
+ software on the system against those libraries.
+
+IMPLEMENTATION NOTES
+
+ The normal sequence of actions taken for a user login is:
+
+ pam_authenticate
+ pam_setcred(PAM_ESTABLISH_CRED)
+ pam_open_session
+ pam_acct_mgmt
+
+ and then at logout:
+
+ pam_close_session
+
+ followed by closing the open PAM session. The corresponding pam_sm_*
+ functions in this module are called when an application calls those
+ public interface functions. Not all applications call all of those
+ functions, or in particularly that order, although pam_authenticate is
+ always first and has to be.
+
+ When pam_authenticate is called, pam-krb5 creates a temporary ticket
+ cache in /tmp and sets the PAM environment variable PAM_KRB5CCNAME to
+ point to it. This ticket cache will be automatically destroyed when the
+ PAM session is closed and is there only to pass the initial credentials
+ to the call to pam_setcred. The module would use a memory cache, but
+ memory caches will only work if the application preserves the PAM
+ environment between the calls to pam_authenticate and pam_setcred. Most
+ do, but OpenSSH notoriously does not and calls pam_authenticate in a
+ subprocess, so this method is used to pass the tickets to the
+ pam_setcred call in a different process.
+
+ pam_authenticate does a complete authentication, including checking the
+ resulting TGT by obtaining a service ticket for the local host if
+ possible, but this requires read access to the system keytab. If the
+ keytab doesn't exist, can't be read, or doesn't include the appropriate
+ credentials, the default is to accept the authentication. This can be
+ controlled by setting verify_ap_req_nofail to true in [libdefaults] in
+ /etc/krb5.conf. pam_authenticate also does a basic authorization check,
+ by default calling krb5_kuserok (which uses ~/.k5login if available and
+ falls back to checking that the principal corresponds to the account
+ name). This can be customized with several options documented in the
+ pam_krb5(5) man page.
+
+ pam-krb5 treats pam_open_session and pam_setcred(PAM_ESTABLISH_CRED) as
+ synonymous, as some applications call one and some call the other. Both
+ copy the initial credentials from the temporary cache into a permanent
+ cache for this session and set KRB5CCNAME in the environment. It will
+ remember when the credential cache has been established and then avoid
+ doing any duplicate work afterwards, since some applications call
+ pam_setcred or pam_open_session multiple times (most notably X.Org 7 and
+ earlier xdm, which also throws away the module settings the last time it
+ calls them).
+
+ pam_acct_mgmt finds the ticket cache, reads it in to obtain the
+ authenticated principal, and then does is another authorization check
+ against .k5login or the local account name as described above.
+
+ After the call to pam_setcred or pam_open_session, the ticket cache will
+ be destroyed whenever the calling application either destroys the PAM
+ environment or calls pam_close_session, which it should do on user
+ logout.
+
+ The normal sequence of events when refreshing a ticket cache (such as
+ inside a screensaver) is:
+
+ pam_authenticate
+ pam_setcred(PAM_REINITIALIZE_CRED)
+ pam_acct_mgmt
+
+ (PAM_REFRESH_CRED may be used instead.) Authentication proceeds as
+ above. At the pam_setcred stage, rather than creating a new ticket
+ cache, the module instead finds the current ticket cache (from the
+ KRB5CCNAME environment variable or the default ticket cache location
+ from the Kerberos library) and then reinitializes it with the
+ credentials from the temporary pam_authenticate ticket cache. When
+ refreshing a ticket cache, the application should not open a session.
+ Calling pam_acct_mgmt is optional; pam-krb5 doesn't do anything
+ different when it's called in this case.
+
+ If pam_authenticate apparently didn't succeed, or if an account was
+ configured to be ignored via ignore_root or minimum_uid, pam_setcred
+ (and therefore pam_open_session) and pam_acct_mgmt return PAM_IGNORE,
+ which tells the PAM library to proceed as if that module wasn't listed
+ in the PAM configuration at all. pam_authenticate, however, returns
+ failure in the ignored user case by default, since otherwise a
+ configuration using ignore_root with pam-krb5 as the only PAM module
+ would allow anyone to log in as root without a password. There doesn't
+ appear to be a case where returning PAM_IGNORE instead would improve the
+ module's behavior, but if you know of a case, please let me know.
+
+ By default, pam_authenticate intentionally does not follow the PAM
+ standard for handling expired accounts and instead returns failure from
+ pam_authenticate unless the Kerberos libraries are able to change the
+ account password during authentication. Too many applications either do
+ not call pam_acct_mgmt or ignore its exit status. The fully correct PAM
+ behavior (returning success from pam_authenticate and
+ PAM_NEW_AUTHTOK_REQD from pam_acct_mgmt) can be enabled with the
+ defer_pwchange option.
+
+ The defer_pwchange option is unfortunately somewhat tricky to implement.
+ In this case, the calling sequence is:
+
+ pam_authenticate
+ pam_acct_mgmt
+ pam_chauthtok
+ pam_setcred
+ pam_open_session
+
+ During the first pam_authenticate, we can't obtain credentials and
+ therefore a ticket cache since the password is expired. But
+ pam_authenticate isn't called again after pam_chauthtok, so
+ pam_chauthtok has to create a ticket cache. We however don't want it to
+ do this for the normal password change (passwd) case.
+
+ What we do is set a flag in our PAM data structure saying that we're
+ processing an expired password, and pam_chauthtok, if it sees that flag,
+ redoes the authentication with password prompting disabled after it
+ finishes changing the password.
+
+ Unfortunately, when handling password changes this way, pam_chauthtok
+ will always have to prompt the user for their current password again
+ even though they just typed it. This is because the saved
+ authentication tokens are cleared after pam_authenticate returns, for
+ security reasons. We could hack around this by saving the password in
+ our PAM data structure, but this would let the application gain access
+ to it (exactly what the clearing is intended to prevent) and breaks a
+ PAM library guarantee. We could also work around this by having
+ pam_authenticate get the kadmin/changepw authenticator in the expired
+ password case and store it for pam_chauthtok, but it doesn't seem worth
+ the hassle.
+
+HISTORY AND ACKNOWLEDGEMENTS
+
+ Originally written by Frank Cusack <fcusack@fcusack.com>, with the
+ following acknowledgement:
+
+ Thanks to Naomaru Itoi <itoi@eecs.umich.edu>, Curtis King
+ <curtis.king@cul.ca>, and Derrick Brashear <shadow@dementia.org>, all
+ of whom have written and made available Kerberos 4/5 modules.
+ Although no code in this module is directly from these author's
+ modules, (except the get_user_info() routine in support.c; derived
+ from whichever of these authors originally wrote the first module the
+ other 2 copied from), it was extremely helpful to look over their code
+ which aided in my design.
+
+ The module was then patched for the FreeBSD ports collection with
+ additional modifications by unknown maintainers and then was modified by
+ Joel Kociolek <joko@logidee.com> to be usable with Debian GNU/Linux.
+
+ It was packaged by Sam Hartman as the Kerberos v5 PAM module for Debian
+ and improved and modified by him and later by Russ Allbery to fix bugs
+ and add additional features. It was then adopted by Andres Salomon, who
+ added support for refreshing credentials.
+
+ The current distribution is maintained by Russ Allbery, who also added
+ support for reading configuration from krb5.conf, added many features
+ for compatibility with the Sourceforge module, commented and
+ standardized the formatting of the code, and overhauled the
+ documentation.
+
+ Thanks to Douglas E. Engert for the initial implementation of PKINIT
+ support. I have since modified and reworked it extensively, so any bugs
+ or compilation problems are my fault.
+
+ Thanks to Markus Moeller for lots of debugging and multiple patches and
+ suggestions for improved portability.
+
+ Thanks to Booker Bense for the implementation of the alt_auth_map
+ option.
+
+ Thanks to Sam Hartman for the FAST support implementation.
+
+SUPPORT
+
+ The pam-krb5 web page at:
+
+ https://www.eyrie.org/~eagle/software/pam-krb5/
+
+ will always have the current version of this package, the current
+ documentation, and pointers to any additional resources.
+
+ For bug tracking, use the issue tracker on GitHub:
+
+ https://github.com/rra/pam-krb5/issues
+
+ However, please be aware that I tend to be extremely busy and work
+ projects often take priority. I'll save your report and get to it as
+ soon as I can, but it may take me a couple of months.
+
+SOURCE REPOSITORY
+
+ pam-krb5 is maintained using Git. You can access the current source on
+ GitHub at:
+
+ https://github.com/rra/pam-krb5
+
+ or by cloning the repository at:
+
+ https://git.eyrie.org/git/kerberos/pam-krb5.git
+
+ or view the repository via the web at:
+
+ https://git.eyrie.org/?p=kerberos/pam-krb5.git
+
+ The eyrie.org repository is the canonical one, maintained by the author,
+ but using GitHub is probably more convenient for most purposes. Pull
+ requests are gratefully reviewed and normally accepted.
+
+LICENSE
+
+ The pam-krb5 package as a whole is covered by the following copyright
+ statement and license:
+
+ Copyright 2005-2010, 2014-2015, 2017, 2020-2021
+ Russ Allbery <eagle@eyrie.org>
+ Copyright 2009-2011
+ The Board of Trustees of the Leland Stanford Junior University
+ Copyright 2005 Andres Salomon <dilinger@debian.org>
+ Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, and the entire permission notice in its entirety, including
+ the disclaimer of warranties.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the
+ distribution.
+
+ 3. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ ALTERNATIVELY, this product may be distributed under the terms of the
+ GNU General Public License, in which case the provisions of the GPL
+ are required INSTEAD OF the above restrictions. (This clause is
+ necessary due to a potential bad interaction between the GPL and the
+ restrictions contained in a BSD-style copyright.)
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGE.
+
+ Some files in this distribution are individually released under
+ different licenses, all of which are compatible with the above general
+ package license but which may require preservation of additional
+ notices. All required notices, and detailed information about the
+ licensing of each file, are recorded in the LICENSE file.
+
+ Files covered by a license with an assigned SPDX License Identifier
+ include SPDX-License-Identifier tags to enable automated processing of
+ license information. See https://spdx.org/licenses/ for more
+ information.
+
+ For any copyright range specified by files in this package as YYYY-ZZZZ,
+ the range specifies every single year in that closed interval.
diff --git a/README.md b/README.md
new file mode 100644
index 000000000000..e74b6751ceb4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,665 @@
+# pam-krb5
+
+[![Build
+status](https://github.com/rra/pam-krb5/workflows/build/badge.svg)](https://github.com/rra/pam-krb5/actions)
+[![Debian
+package](https://img.shields.io/debian/v/libpam-krb5/unstable)](https://tracker.debian.org/pkg/libpam-krb5)
+
+Copyright 2005-2010, 2014-2015, 2017, 2020-2021 Russ Allbery
+<eagle@eyrie.org>. Copyright 2009-2011 The Board of Trustees of the
+Leland Stanford Junior University. Copyright 2005 Andres Salomon
+<dilinger@debian.org>. Copyright 1999-2000 Frank Cusack
+<fcusack@fcusack.com>. This software is distributed under a BSD-style
+license. Please see the section [License](#license) below for more
+information.
+
+## Blurb
+
+pam-krb5 is a Kerberos PAM module for either MIT Kerberos or Heimdal. It
+supports ticket refreshing by screen savers, configurable authorization
+handling, authentication of non-local accounts for network services,
+password changing, and password expiration, as well as all the standard
+expected PAM features. It works correctly with OpenSSH, even with
+ChallengeResponseAuthentication and PrivilegeSeparation enabled, and
+supports extensive configuration either by PAM options or in krb5.conf or
+both. PKINIT is supported with recent versions of both MIT Kerberos and
+Heimdal and FAST is supported with recent MIT Kerberos.
+
+## Description
+
+pam-krb5 provides a Kerberos PAM module that supports authentication, user
+ticket cache handling, simple authorization (via .k5login or checking
+Kerberos principals against local usernames), and password changing. It
+can be configured through either options in the PAM configuration itself
+or through entries in the system krb5.conf file, and it tries to work
+around PAM implementation flaws in commonly-used PAM-enabled applications
+such as OpenSSH and xdm. It supports both PKINIT and FAST to the extent
+that the underlying Kerberos libraries support these features.
+
+This is not the Kerberos PAM module maintained on Sourceforge and used on
+Red Hat systems. It is an independent implementation that, if it ever
+shared any common code, diverged long ago. It supports some features that
+the Sourceforge module does not (particularly around authorization), and
+does not support some options (particularly ones not directly related to
+Kerberos) that it does. This module will never support Kerberos v4 or
+AFS. For an AFS session module that works with this module (or any other
+Kerberos PAM module), see
+[pam-afs-session](https://www.eyrie.org/~eagle/software/pam-afs-session/).
+
+If there are other options besides AFS and Kerberos v4 support from the
+Sourceforge PAM module that you're missing in this module, please let me
+know.
+
+## Requirements
+
+Either MIT Kerberos (or Kerberos implementations based on it) or Heimdal
+are supported. MIT Keberos 1.3 or later may be required; this module has
+not been tested with earlier versions.
+
+For PKINIT support, Heimdal 0.8rc1 or later or MIT Kerberos 1.6.3 or later
+are required. Earlier MIT Kerberos 1.6 releases have a bug in their
+handling of PKINIT options. MIT Kerberos 1.12 or later is required to use
+the use_pkinit PAM option.
+
+For FAST (Flexible Authentication Secure Tunneling) support, MIT Kerberos
+1.7 or higher is required. For anonymous FAST support, anonymous
+authentication (generally anonymous PKINIT) support is required in both
+the Kerberos libraries and in the local KDC.
+
+This module should work on Linux and build with gcc or clang. It may
+still work on Solaris and build with the Sun C compiler, but I have only
+tested it on Linux recently. There is beta-quality support for the AIX
+NAS Kerberos implementation that has not been tested in years. Other PAM
+implementations will probably require some porting, although untested
+build system support is present for FreeBSD, Mac OS X, and HP-UX. I
+personally can only test on Linux and rely on others to report problems on
+other operating systems.
+
+Old versions of OpenSSH are known to call `pam_authenticate` followed by
+`pam_setcred(PAM_REINITIALIZE_CRED)` without first calling
+`pam_open_session`, thereby requesting that an existing ticket cache be
+renewed (similar to what a screensaver would want) rather than requesting
+a new ticket cache be created. Since this behavior is indistinguishable
+at the PAM level from a screensaver, pam-krb5 when used with these old
+versions of OpenSSH will refresh the ticket cache of the OpenSSH daemon
+rather than setting up a new ticket cache for the user. The resulting
+ticket cache will have the correct permissions (this is not a security
+concern), but will not be named correctly or referenced in the user's
+environment and will be overwritten by the next user login. The best
+solution to this problem is to upgrade OpenSSH. I'm not sure exactly when
+this problem was fixed, but at the very least OpenSSH 4.3 and later do not
+exhibit it.
+
+To bootstrap from a Git checkout, or if you change the Automake files and
+need to regenerate Makefile.in, you will need Automake 1.11 or later. For
+bootstrap or if you change configure.ac or any of the m4 files it includes
+and need to regenerate configure or config.h.in, you will need Autoconf
+2.64 or later. Perl is also required to generate manual pages from a
+fresh Git checkout.
+
+## Building and Installation
+
+You can build and install pam-krb5 with the standard commands:
+
+```
+ ./configure
+ make
+ make install
+```
+
+If you are building from a Git clone, first run `./bootstrap` in the
+source directory to generate the build files. `make install` will
+probably have to be done as root. Building outside of the source
+directory is also supported, if you wish, by creating an empty directory
+and then running configure with the correct relative path.
+
+The module will be installed in `/usr/local/lib/security` by default, but
+expect to have to override this using `--libdir`. The correct
+installation path for PAM modules varies considerably between systems.
+The module will always be installed in a subdirectory named `security`
+under the specified value of `--libdir`. On Red Hat Linux, for example,
+`--libdir=/usr/lib64` is appropriate to install the module into the system
+PAM directory. On Debian's amd64 architecture,
+`--libdir=/usr/lib/x86_64-linux-gnu` would be correct.
+
+Normally, configure will use `krb5-config` to determine the flags to use
+to compile with your Kerberos libraries. To specify a particular
+`krb5-config` script to use, either set the `PATH_KRB5_CONFIG` environment
+variable or pass it to configure like:
+
+```
+ ./configure PATH_KRB5_CONFIG=/path/to/krb5-config
+```
+
+If `krb5-config` isn't found, configure will look for the standard
+Kerberos libraries in locations already searched by your compiler. If the
+the `krb5-config` script first in your path is not the one corresponding
+to the Kerberos libraries you want to use, or if your Kerberos libraries
+and includes aren't in a location searched by default by your compiler,
+you need to specify a different Kerberos installation root via
+`--with-krb5=PATH`. For example:
+
+```
+ ./configure --with-krb5=/usr/pubsw
+```
+
+You can also individually set the paths to the include directory and the
+library directory with `--with-krb5-include` and `--with-krb5-lib`. You
+may need to do this if Autoconf can't figure out whether to use `lib`,
+`lib32`, or `lib64` on your platform.
+
+To not use `krb5-config` and force library probing even if there is a
+`krb5-config` script on your path, set `PATH_KRB5_CONFIG` to a nonexistent
+path:
+
+```
+ ./configure PATH_KRB5_CONFIG=/nonexistent
+```
+
+`krb5-config` is not used and library probing is always done if either
+`--with-krb5-include` or `--with-krb5-lib` are given.
+
+Pass `--enable-silent-rules` to configure for a quieter build (similar to
+the Linux kernel). Use `make warnings` instead of `make` to build with
+full GCC compiler warnings (requires either GCC or Clang and may require a
+relatively current version of the compiler).
+
+You can pass the `--enable-reduced-depends` flag to configure to try to
+minimize the shared library dependencies encoded in the binaries. This
+omits from the link line all the libraries included solely because other
+libraries depend on them and instead links the programs only against
+libraries whose APIs are called directly. This will only work with shared
+libraries and will only work on platforms where shared libraries properly
+encode their own dependencies (this includes most modern platforms such as
+all Linux). It is intended primarily for building packages for Linux
+distributions to avoid encoding unnecessary shared library dependencies
+that make shared library migrations more difficult. If none of the above
+made any sense to you, don't bother with this flag.
+
+## Testing
+
+pam-krb5 comes with a comprehensive test suite, but it requires some
+configuration in order to test anything other than low-level utility
+functions. For the full test suite, you will need to have a running KDC
+in which you can create two test accounts, one with admin access to the
+other. Using a test KDC environment, if you have one, is recommended.
+
+Follow the instructions in `tests/config/README` to configure the test
+suite.
+
+Now, you can run the test suite with:
+
+```
+ make check
+```
+
+If a test fails, you can run a single test with verbose output via:
+
+```
+ tests/runtests -o <name-of-test>
+```
+
+Do this instead of running the test program directly since it will ensure
+that necessary environment variables are set up.
+
+The default libkadm5clnt library on the system must match the
+implementation of your KDC for the module/expired test to work, since the
+two kadmin protocols are not compatible. If you use the MIT library
+against a Heimdal server, the test will be skipped; if you use the Heimdal
+library against an MIT server, the test suite may hang.
+
+Several `module/expired` tests are expected to fail with Heimdal 1.5 due
+to a bug in Heimdal with reauthenticating immediately after a
+library-mediated password change of an expired password. This is fixed in
+later releases of Heimdal.
+
+To run the full test suite, Perl 5.10 or later is required. The following
+additional Perl modules will be used if present:
+
+* Test::Pod
+* Test::Spelling
+
+All are available on CPAN. Those tests will be skipped if the modules are
+not available.
+
+To enable tests that don't detect functionality problems but are used to
+sanity-check the release, set the environment variable `RELEASE_TESTING`
+to a true value. To enable tests that may be sensitive to the local
+environment or that produce a lot of false positives without uncovering
+many problems, set the environment variable `AUTHOR_TESTING` to a true
+value.
+
+## Configuring
+
+Just installing the module does not enable it or change anything about
+your system authentication configuration. To use the module for all
+system authentication on Debian systems, put something like:
+
+```
+ auth sufficient pam_krb5.so minimum_uid=1000
+ auth required pam_unix.so try_first_pass nullok_secure
+```
+
+in `/etc/pam.d/common-auth`, something like:
+
+```
+ session optional pam_krb5.so minimum_uid=1000
+ session required pam_unix.so
+```
+
+in `/etc/pam.d/common-session`, and something like:
+
+```
+ account required pam_krb5.so minimum_uid=1000
+ account required pam_unix.so
+```
+
+in `/etc/pam.d/common-account`. The `minimum_uid` setting tells the PAM
+module to pass on any users with a UID lower than 1000, thereby bypassing
+Kerberos authentication for the root account and any system accounts. You
+normally want to do this since otherwise, if the network is down, the
+Kerberos authentication can time out and make it difficult to log in as
+root and fix matters. This also avoids problems with Kerberos principals
+that happen to match system accounts accidentally getting access to those
+accounts.
+
+Be sure to include the module in the session group as well as the auth
+group. Without the session entry, the user's ticket cache will not be
+created properly for ssh logins (among possibly others).
+
+If your users should normally all use Kerberos passwords exclusively,
+putting something like:
+
+```
+ password sufficient pam_krb5.so minimum_uid=1000
+ password required pam_unix.so try_first_pass obscure md5
+```
+
+in `/etc/pam.d/common-password` will change users' passwords in Kerberos
+by default and then only fall back on Unix if that doesn't work. (You can
+make this tighter by using the more complex new-style PAM configuration.)
+If you instead want to synchronize local and Kerberos passwords and change
+them both at the same time, you can do something like:
+
+```
+ password required pam_unix.so obscure sha512
+ password required pam_krb5.so use_authtok minimum_uid=1000
+```
+
+If you have multiple environments that you want to synchronize and you
+don't want password changes to continue if the Kerberos password change
+fails, use the `clear_on_fail` option. For example:
+
+```
+ password required pam_krb5.so clear_on_fail minimum_uid=1000
+ password required pam_unix.so use_authtok obscure sha512
+ password required pam_smbpass.so use_authtok
+```
+
+In this case, if `pam_krb5` cannot change the password (due to password
+strength rules on the KDC, for example), it will clear the stored password
+(because of the `clear_on_fail` option), and since `pam_unix` and
+`pam_smbpass` are both configured with `use_authtok`, they will both fail.
+`clear_on_fail` is not the default because it would interfere with the
+more common pattern of falling back to local passwords if the user doesn't
+exist in Kerberos.
+
+If you use a more complex configuration with the Linux PAM `[]` syntax for
+the session and account groups, note that `pam_krb5` returns a status of
+ignore, not success, if the user didn't log on with Kerberos. You may
+need to handle that explicitly with `ignore=ignore` in your action list.
+
+There are many, many other possibilities. See the Linux PAM documentation
+for all the configuration options.
+
+On Red Hat systems, modify `/etc/pam.d/system-auth` instead, which
+contains all of the configuration for the different stacks.
+
+You can also use pam-krb5 only for specific services. In that case,
+modify the files in `/etc/pam.d` for that particular service to use
+`pam_krb5.so` for authentication. For services that are using passwords
+over TLS to authenticate users, you may want to use the `ignore_k5login`
+and `no_ccache` options to the authenticate module. `.k5login`
+authorization is only meaningful for local accounts and ticket caches are
+usually (although not always) only useful for interactive sessions.
+
+Configuring the module for Solaris is both simpler and less flexible,
+since Solaris (at least Solaris 8 and 9, which are the last versions of
+Solaris with which this module was extensively tested) use a single
+`/etc/pam.conf` file that contains configuration for all programs. For
+console login on Solaris, try something like:
+
+```
+ login auth sufficient /usr/local/lib/security/pam_krb5.so minimum_uid=100
+ login auth required /usr/lib/security/pam_unix_auth.so.1 use_first_pass
+ login account required /usr/local/lib/security/pam_krb5.so minimum_uid=100
+ login account required /usr/lib/security/pam_unix_account.so.1
+ login session required /usr/local/lib/security/pam_krb5.so retain_after_close minimum_uid=100
+ login session required /usr/lib/security/pam_unix_session.so.1
+```
+
+A similar configuration could be used for other services, such as ssh.
+See the pam.conf(5) man page for more information. When using this module
+with Solaris login (at least on Solaris 8 and 9), you will probably also
+need to add `retain_after_close` to the PAM configuration to avoid having
+the user's credentials deleted before they are logged in.
+
+The Solaris Kerberos library reportedly does not support prompting for a
+password change of an expired account during authentication. Supporting
+password change for expired accounts on Solaris with native Kerberos may
+therefore require setting the `defer_pwchange` or `force_pwchange` option
+for selected login applications. See the description and warnings about
+that option in the pam_krb5(5) man page.
+
+Some configuration options may be put in the `krb5.conf` file used by your
+Kerberos libraries (usually `/etc/krb5.conf` or
+`/usr/local/etc/krb5.conf`) instead or in addition to the PAM
+configuration. See the man page for more details.
+
+The Kerberos library, via pam-krb5, will prompt the user to change their
+password if their password is expired, but when using OpenSSH, this will
+only work when `ChallengeResponseAuthentication` is enabled. Unless this
+option is enabled, OpenSSH doesn't pass PAM messages to the user and can
+only respond to a simple password prompt.
+
+If you are using MIT Kerberos, be aware that users whose passwords are
+expired will not be prompted to change their password unless the KDC
+configuration for your realm in `[realms]` in `krb5.conf` contains a
+`master_kdc` setting or, if using DNS SRV records, you have a DNS entry
+for `_kerberos-master` as well as `_kerberos`.
+
+## Debugging
+
+The first step when debugging any problems with this module is to add
+`debug` to the PAM options for the module (either in the PAM configuration
+or in `krb5.conf`). This will significantly increase the logging from the
+module and should provide a trace of exactly what failed and any available
+error information.
+
+Many Kerberos authentication problems are due to configuration issues in
+`krb5.conf`. If pam-krb5 doesn't work, first check that `kinit` works on
+the same system. That will test your basic Kerberos configuration. If
+the system has a keytab file installed that's readable by the process
+doing authentication via PAM, make sure that the keytab is current and
+contains a key for `host/<system>` where <system> is the fully-qualified
+hostname. pam-krb5 prevents KDC spoofing by checking the user's
+credentials when possible, but this means that if a keytab is present it
+must be correct or authentication will fail. You can check the keytab
+with `klist -k` and `kinit -k`.
+
+Be sure that all libraries and modules, including PAM modules, loaded by a
+program use the same Kerberos libraries. Sometimes programs that use PAM,
+such as current versions of OpenSSH, also link against Kerberos directly.
+If your sshd is linked against one set of Kerberos libraries and pam-krb5
+is linked against a different set of Kerberos libraries, this will often
+cause problems (such as segmentation faults, bus errors, assertions, or
+other strange behavior). Similar issues apply to the com_err library or
+any other library used by both modules and shared libraries and by the
+application that loads them. If your OS ships Kerberos libraries, it's
+usually best if possible to build all Kerberos software on the system
+against those libraries.
+
+## Implementation Notes
+
+The normal sequence of actions taken for a user login is:
+
+```
+ pam_authenticate
+ pam_setcred(PAM_ESTABLISH_CRED)
+ pam_open_session
+ pam_acct_mgmt
+```
+
+and then at logout:
+
+```
+ pam_close_session
+```
+
+followed by closing the open PAM session. The corresponding `pam_sm_*`
+functions in this module are called when an application calls those public
+interface functions. Not all applications call all of those functions, or
+in particularly that order, although `pam_authenticate` is always first
+and has to be.
+
+When `pam_authenticate` is called, pam-krb5 creates a temporary ticket
+cache in `/tmp` and sets the PAM environment variable `PAM_KRB5CCNAME` to
+point to it. This ticket cache will be automatically destroyed when the
+PAM session is closed and is there only to pass the initial credentials to
+the call to `pam_setcred`. The module would use a memory cache, but
+memory caches will only work if the application preserves the PAM
+environment between the calls to `pam_authenticate` and `pam_setcred`.
+Most do, but OpenSSH notoriously does not and calls `pam_authenticate` in
+a subprocess, so this method is used to pass the tickets to the
+`pam_setcred` call in a different process.
+
+`pam_authenticate` does a complete authentication, including checking the
+resulting TGT by obtaining a service ticket for the local host if
+possible, but this requires read access to the system keytab. If the
+keytab doesn't exist, can't be read, or doesn't include the appropriate
+credentials, the default is to accept the authentication. This can be
+controlled by setting `verify_ap_req_nofail` to true in `[libdefaults]` in
+`/etc/krb5.conf`. `pam_authenticate` also does a basic authorization
+check, by default calling `krb5_kuserok` (which uses `~/.k5login` if
+available and falls back to checking that the principal corresponds to the
+account name). This can be customized with several options documented in
+the pam_krb5(5) man page.
+
+pam-krb5 treats `pam_open_session` and `pam_setcred(PAM_ESTABLISH_CRED)`
+as synonymous, as some applications call one and some call the other.
+Both copy the initial credentials from the temporary cache into a
+permanent cache for this session and set `KRB5CCNAME` in the environment.
+It will remember when the credential cache has been established and then
+avoid doing any duplicate work afterwards, since some applications call
+`pam_setcred` or `pam_open_session` multiple times (most notably X.Org 7
+and earlier xdm, which also throws away the module settings the last time
+it calls them).
+
+`pam_acct_mgmt` finds the ticket cache, reads it in to obtain the
+authenticated principal, and then does is another authorization check
+against `.k5login` or the local account name as described above.
+
+After the call to `pam_setcred` or `pam_open_session`, the ticket cache
+will be destroyed whenever the calling application either destroys the PAM
+environment or calls `pam_close_session`, which it should do on user
+logout.
+
+The normal sequence of events when refreshing a ticket cache (such as
+inside a screensaver) is:
+
+```
+ pam_authenticate
+ pam_setcred(PAM_REINITIALIZE_CRED)
+ pam_acct_mgmt
+```
+
+(`PAM_REFRESH_CRED` may be used instead.) Authentication proceeds as
+above. At the `pam_setcred` stage, rather than creating a new ticket
+cache, the module instead finds the current ticket cache (from the
+`KRB5CCNAME` environment variable or the default ticket cache location
+from the Kerberos library) and then reinitializes it with the credentials
+from the temporary `pam_authenticate` ticket cache. When refreshing a
+ticket cache, the application should not open a session. Calling
+`pam_acct_mgmt` is optional; pam-krb5 doesn't do anything different when
+it's called in this case.
+
+If `pam_authenticate` apparently didn't succeed, or if an account was
+configured to be ignored via `ignore_root` or `minimum_uid`, `pam_setcred`
+(and therefore `pam_open_session`) and `pam_acct_mgmt` return
+`PAM_IGNORE`, which tells the PAM library to proceed as if that module
+wasn't listed in the PAM configuration at all. `pam_authenticate`,
+however, returns failure in the ignored user case by default, since
+otherwise a configuration using `ignore_root` with pam-krb5 as the only
+PAM module would allow anyone to log in as root without a password. There
+doesn't appear to be a case where returning `PAM_IGNORE` instead would
+improve the module's behavior, but if you know of a case, please let me
+know.
+
+By default, `pam_authenticate` intentionally does not follow the PAM
+standard for handling expired accounts and instead returns failure from
+`pam_authenticate` unless the Kerberos libraries are able to change the
+account password during authentication. Too many applications either do
+not call `pam_acct_mgmt` or ignore its exit status. The fully correct PAM
+behavior (returning success from `pam_authenticate` and
+`PAM_NEW_AUTHTOK_REQD` from `pam_acct_mgmt`) can be enabled with the
+`defer_pwchange` option.
+
+The `defer_pwchange` option is unfortunately somewhat tricky to implement.
+In this case, the calling sequence is:
+
+```
+ pam_authenticate
+ pam_acct_mgmt
+ pam_chauthtok
+ pam_setcred
+ pam_open_session
+```
+
+During the first `pam_authenticate`, we can't obtain credentials and
+therefore a ticket cache since the password is expired. But
+`pam_authenticate` isn't called again after `pam_chauthtok`, so
+`pam_chauthtok` has to create a ticket cache. We however don't want it to
+do this for the normal password change (`passwd`) case.
+
+What we do is set a flag in our PAM data structure saying that we're
+processing an expired password, and `pam_chauthtok`, if it sees that flag,
+redoes the authentication with password prompting disabled after it
+finishes changing the password.
+
+Unfortunately, when handling password changes this way, `pam_chauthtok`
+will always have to prompt the user for their current password again even
+though they just typed it. This is because the saved authentication
+tokens are cleared after `pam_authenticate` returns, for security reasons.
+We could hack around this by saving the password in our PAM data
+structure, but this would let the application gain access to it (exactly
+what the clearing is intended to prevent) and breaks a PAM library
+guarantee. We could also work around this by having `pam_authenticate`
+get the `kadmin/changepw` authenticator in the expired password case and
+store it for `pam_chauthtok`, but it doesn't seem worth the hassle.
+
+## History and Acknowledgements
+
+Originally written by Frank Cusack <fcusack@fcusack.com>, with the
+following acknowledgement:
+
+> Thanks to Naomaru Itoi <itoi@eecs.umich.edu>, Curtis King
+> <curtis.king@cul.ca>, and Derrick Brashear <shadow@dementia.org>, all of
+> whom have written and made available Kerberos 4/5 modules. Although no
+> code in this module is directly from these author's modules, (except the
+> get_user_info() routine in support.c; derived from whichever of these
+> authors originally wrote the first module the other 2 copied from), it
+> was extremely helpful to look over their code which aided in my design.
+
+The module was then patched for the FreeBSD ports collection with
+additional modifications by unknown maintainers and then was modified by
+Joel Kociolek <joko@logidee.com> to be usable with Debian GNU/Linux.
+
+It was packaged by Sam Hartman as the Kerberos v5 PAM module for Debian
+and improved and modified by him and later by Russ Allbery to fix bugs and
+add additional features. It was then adopted by Andres Salomon, who added
+support for refreshing credentials.
+
+The current distribution is maintained by Russ Allbery, who also added
+support for reading configuration from `krb5.conf`, added many features
+for compatibility with the Sourceforge module, commented and standardized
+the formatting of the code, and overhauled the documentation.
+
+Thanks to Douglas E. Engert for the initial implementation of PKINIT
+support. I have since modified and reworked it extensively, so any bugs
+or compilation problems are my fault.
+
+Thanks to Markus Moeller for lots of debugging and multiple patches and
+suggestions for improved portability.
+
+Thanks to Booker Bense for the implementation of the `alt_auth_map`
+option.
+
+Thanks to Sam Hartman for the FAST support implementation.
+
+## Support
+
+The [pam-krb5 web page](https://www.eyrie.org/~eagle/software/pam-krb5/)
+will always have the current version of this package, the current
+documentation, and pointers to any additional resources.
+
+For bug tracking, use the [issue tracker on
+GitHub](https://github.com/rra/pam-krb5/issues). However, please be aware
+that I tend to be extremely busy and work projects often take priority.
+I'll save your report and get to it as soon as I can, but it may take me a
+couple of months.
+
+## Source Repository
+
+pam-krb5 is maintained using Git. You can access the current source on
+[GitHub](https://github.com/rra/pam-krb5) or by cloning the repository at:
+
+https://git.eyrie.org/git/kerberos/pam-krb5.git
+
+or [view the repository on the
+web](https://git.eyrie.org/?p=kerberos/pam-krb5.git).
+
+The eyrie.org repository is the canonical one, maintained by the author,
+but using GitHub is probably more convenient for most purposes. Pull
+requests are gratefully reviewed and normally accepted.
+
+## License
+
+The pam-krb5 package as a whole is covered by the following copyright
+statement and license:
+
+> Copyright 2005-2010, 2014-2015, 2017, 2020-2021
+> Russ Allbery <eagle@eyrie.org>
+>
+> Copyright 2009-2011
+> The Board of Trustees of the Leland Stanford Junior University
+>
+> Copyright 2005
+> Andres Salomon <dilinger@debian.org>
+>
+> Copyright 1999-2000
+> Frank Cusack <fcusack@fcusack.com>
+>
+> Redistribution and use in source and binary forms, with or without
+> modification, are permitted provided that the following conditions are
+> met:
+>
+> 1. Redistributions of source code must retain the above copyright
+> notice, and the entire permission notice in its entirety, including
+> the disclaimer of warranties.
+>
+> 2. Redistributions in binary form must reproduce the above copyright
+> notice, this list of conditions and the following disclaimer in the
+> documentation and/or other materials provided with the distribution.
+>
+> 3. The name of the author may not be used to endorse or promote products
+> derived from this software without specific prior written permission.
+>
+> ALTERNATIVELY, this product may be distributed under the terms of the GNU
+> General Public License, in which case the provisions of the GPL are
+> required INSTEAD OF the above restrictions. (This clause is necessary due
+> to a potential bad interaction between the GPL and the restrictions
+> contained in a BSD-style copyright.)
+>
+> THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+> INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+> AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+> THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+> EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+> PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+> PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+> LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+> NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Some files in this distribution are individually released under different
+licenses, all of which are compatible with the above general package
+license but which may require preservation of additional notices. All
+required notices, and detailed information about the licensing of each
+file, are recorded in the LICENSE file.
+
+Files covered by a license with an assigned SPDX License Identifier
+include SPDX-License-Identifier tags to enable automated processing of
+license information. See https://spdx.org/licenses/ for more information.
+
+For any copyright range specified by files in this package as YYYY-ZZZZ,
+the range specifies every single year in that closed interval.
diff --git a/TODO b/TODO
new file mode 100644
index 000000000000..876c5196a1bf
--- /dev/null
+++ b/TODO
@@ -0,0 +1,101 @@
+ pam-krb5 To-Do List
+
+PAM API:
+
+ * Support PAM_CHANGE_EXPIRED_AUTHTOK properly in pam_chauthtok. This
+ will require prompting for the current password (if it's not already
+ available in the PAM data) and trying a regular authentication first to
+ see if the account is expired.
+
+ * Tighter verification that all of our flags are valid might be a good
+ idea.
+
+ * For informational messages followed by a prompt, find a way to combine
+ these into one PAM conversation call for better GUI presentation
+ behavior.
+
+Functionality:
+
+ * Change the authentication flow so that both Heimdal and MIT use the
+ same logic for attempting PKINIT first and then falling back to
+ password. This will fix failure to store passwords in the PAM data
+ with try_pkinit and MIT Kerberos on password fallback and will allow
+ implementation of use_pkinit for MIT. Based on discussion with MIT
+ Kerberos upstream, the best approach is probably to configure a custom
+ prompter that refuses to reply to any prompt.
+
+ * Add a daemon that can be used to verify TGTs that can be used when
+ pam-krb5 is run as a non-root user and hence doesn't have access to the
+ system keytab. Jeff Hutzelman has a daemon and protocol for doing this
+ developed for a different PAM authentication module, and it would be
+ good to stay consistent with that protocol if possible. (Debian
+ Bug#399001)
+
+ * The alt_auth_map parsing to find realms doesn't take into account
+ escaped @-signs and doesn't do proper principal parsing.
+
+ * Fix password expiration handling for the search_k5login and
+ alt_auth_map cases. Right now, we may return expired password errors
+ that would trigger password expiration handling, which probably isn't
+ correct.
+
+ * Support authentication from a keytab.
+
+ * Support disabling of user canonicalization so that the PAM user is
+ retained even if the module did an aname to lname mapping.
+
+ * Use set_out_ccache to write the resulting ticket cache, if it is
+ available. This ensures the correct flags are set in the ticket cache.
+ This poses some challenges due to the two-step ticket cache mechanism
+ currently used. Perhaps there's a cache copying API?
+
+ * Use krb5_chpw_message to parse password change messages from Active
+ Directory.
+
+ * Consider exposing the Kerberos principal in the password prompt for a
+ password change. (Debian Bug#667928)
+
+Code Cleanup:
+
+ * The PKINIT code for Heimdal involves too many #ifdefs right now for my
+ taste. Find a way to restructure it to only wrap the main PKINIT
+ function for Heimdal.
+
+ * The current handling of error return codes is a mess. We need to find
+ a way to return a rich set of error codes from the underlying functions
+ and then map error codes appropriately in the interface functions.
+ Helpful for this would be improved documentation of what error codes
+ are permitted and where.
+
+ * Tracking when to free the Kerberos context and other things stored in
+ the PAM context is currently too complicated. It should be possible to
+ simplify it with a reference counting scheme.
+
+Documentation:
+
+ * Document PKINIT configuration with MIT in krb5.conf. It looks like the
+ library supports configuration in [realms] with similar names to the
+ PAM module configuration.
+
+Portability:
+
+ * If pam_modutil_getpwnam is not available but getpwnam_r is, roll our
+ own using getpwnam_r.
+
+Logging:
+
+ * Log the information that the Kerberos library asks us to display, or at
+ least the info and error messages.
+
+ * Log unknown PAM flags on module entry. Currently, only the symbolic
+ flags we know about will be logged.
+
+Test suite:
+
+ * Ensure that the test suite covers all possible PAM options.
+
+ * Figure out why the pin-mit script for module/pkinit prompts twice and
+ check if it's a bug in the module.
+
+ * Find a way of testing the PKINIT identity selection for MIT Kerberos
+ with use_pkinit enabled.
diff --git a/bootstrap b/bootstrap
new file mode 100755
index 000000000000..948aa1b9f02e
--- /dev/null
+++ b/bootstrap
@@ -0,0 +1,13 @@
+#!/bin/sh
+#
+# Run this shell script to bootstrap as necessary after a fresh checkout.
+
+set -e
+
+autoreconf -i --force
+rm -rf autom4te.cache
+
+# Generate manual pages.
+version=`grep '^pam-krb5' NEWS | head -1 | cut -d' ' -f2`
+pod2man --release="$version" --center=pam-krb5 -s 5 docs/pam_krb5.pod \
+ >docs/pam_krb5.5
diff --git a/ci/README.md b/ci/README.md
new file mode 100644
index 000000000000..fedd0d57fd08
--- /dev/null
+++ b/ci/README.md
@@ -0,0 +1,13 @@
+# Continuous Integration
+
+The files in this directory are used for continuous integration testing.
+`ci/install` installs the prerequisite packages (run as root on a Debian
+derivative), and `ci/test` runs the tests.
+
+Most tests will be skipped without a Kerberos configuration. The scripts
+`ci/kdc-setup-heimdal` and `ci/kdc-setup-mit` will (when run as root on a
+Debian derivative) set up a Heimdal or MIT Kerberos KDC, respectively, and
+generate the files required to run the complete test suite.
+
+Tests are run automatically via GitHub Actions workflows using these
+scripts and the configuration in the `.github/workflows` directory.
diff --git a/ci/files/heimdal/heimdal-kdc b/ci/files/heimdal/heimdal-kdc
new file mode 100644
index 000000000000..d7814631746d
--- /dev/null
+++ b/ci/files/heimdal/heimdal-kdc
@@ -0,0 +1,9 @@
+# Heimdal KDC init script setup. -*- sh -*-
+
+# KDC configuration.
+KDC_ENABLED=yes
+KDC_PARAMS='--config-file=/etc/heimdal-kdc/kdc.conf'
+
+# kpasswdd configuration.
+KPASSWDD_ENABLED=yes
+KPASSWDD_PARAMS='-r HEIMDAL.TEST'
diff --git a/ci/files/heimdal/kadmind.acl b/ci/files/heimdal/kadmind.acl
new file mode 100644
index 000000000000..ae74ad5598ad
--- /dev/null
+++ b/ci/files/heimdal/kadmind.acl
@@ -0,0 +1 @@
+test/admin@HEIMDAL.TEST all testuser@HEIMDAL.TEST
diff --git a/ci/files/heimdal/kdc.conf b/ci/files/heimdal/kdc.conf
new file mode 100644
index 000000000000..29ac52ebb947
--- /dev/null
+++ b/ci/files/heimdal/kdc.conf
@@ -0,0 +1,30 @@
+# Heimdal KDC configuration. -*- conf -*-
+
+[kadmin]
+ default_keys = aes256-cts-hmac-sha1-96:pw-salt
+
+[kdc]
+ acl_file = /etc/heimdal-kdc/kadmind.acl
+ check-ticket-addresses = false
+ logging = SYSLOG:NOTICE
+ ports = 88
+
+ # PKINIT configuration.
+ enable-pkinit = yes
+ pkinit_identity = FILE:/etc/heimdal-kdc/kdc.pem
+ pkinit_anchors = FILE:/etc/heimdal-kdc/ca/ca.pem
+ pkinit_mappings_file = /etc/heimdal-kdc/pki-mapping
+ pkinit_allow_proxy_certificate = no
+ pkinit_principal_in_certificate = no
+
+[libdefaults]
+ default_realm = HEIMDAL.TEST
+ dns_lookup_kdc = false
+ dns_lookup_realm = false
+
+[realms]
+ HEIMDAL.TEST.EYRIE.ORG = {
+ kdc = 127.0.0.1
+ master_kdc = 127.0.0.1
+ admin_server = 127.0.0.1
+ }
diff --git a/ci/files/heimdal/krb5.conf b/ci/files/heimdal/krb5.conf
new file mode 100644
index 000000000000..a2b22c2d54cd
--- /dev/null
+++ b/ci/files/heimdal/krb5.conf
@@ -0,0 +1,19 @@
+[libdefaults]
+ default_realm = HEIMDAL.TEST
+ dns_lookup_kdc = false
+ dns_lookup_realm = false
+ rdns = false
+ renew_lifetime = 7d
+ ticket_lifetime = 25h
+
+[realms]
+ HEIMDAL.TEST = {
+ kdc = 127.0.0.1
+ master_kdc = 127.0.0.1
+ admin_server = 127.0.0.1
+ pkinit_anchors = FILE:/etc/heimdal-kdc/ca/ca.pem
+ }
+
+[logging]
+ kdc = SYSLOG:NOTICE
+ default = SYSLOG:NOTICE
diff --git a/ci/files/heimdal/pki-mapping b/ci/files/heimdal/pki-mapping
new file mode 100644
index 000000000000..76dd6b87edb6
--- /dev/null
+++ b/ci/files/heimdal/pki-mapping
@@ -0,0 +1 @@
+testuser@HEIMDAL.TEST:UID=testuser,DC=HEIMDAL,DC=TEST
diff --git a/ci/files/mit/extensions.client b/ci/files/mit/extensions.client
new file mode 100644
index 000000000000..5a1bbc29bdec
--- /dev/null
+++ b/ci/files/mit/extensions.client
@@ -0,0 +1,19 @@
+[client_cert]
+basicConstraints=CA:FALSE
+keyUsage=digitalSignature,keyEncipherment,keyAgreement
+extendedKeyUsage=1.3.6.1.5.2.3.4
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid,issuer
+issuerAltName=issuer:copy
+subjectAltName=otherName:1.3.6.1.5.2.2;SEQUENCE:princ_name
+
+[princ_name]
+realm=EXP:0,GeneralString:${ENV::REALM}
+principal_name=EXP:1,SEQUENCE:principal_seq
+
+[principal_seq]
+name_type=EXP:0,INTEGER:1
+name_string=EXP:1,SEQUENCE:principals
+
+[principals]
+princ1=GeneralString:${ENV::CLIENT}
diff --git a/ci/files/mit/extensions.kdc b/ci/files/mit/extensions.kdc
new file mode 100644
index 000000000000..cbff73bef1ed
--- /dev/null
+++ b/ci/files/mit/extensions.kdc
@@ -0,0 +1,20 @@
+[kdc_cert]
+basicConstraints=CA:FALSE
+keyUsage=nonRepudiation,digitalSignature,keyEncipherment,keyAgreement
+extendedKeyUsage=1.3.6.1.5.2.3.5
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid,issuer
+issuerAltName=issuer:copy
+subjectAltName=otherName:1.3.6.1.5.2.2;SEQUENCE:kdc_princ_name
+
+[kdc_princ_name]
+realm=EXP:0,GeneralString:${ENV::REALM}
+principal_name=EXP:1,SEQUENCE:kdc_principal_seq
+
+[kdc_principal_seq]
+name_type=EXP:0,INTEGER:1
+name_string=EXP:1,SEQUENCE:kdc_principals
+
+[kdc_principals]
+princ1=GeneralString:krbtgt
+princ2=GeneralString:${ENV::REALM}
diff --git a/ci/files/mit/kadm5.acl b/ci/files/mit/kadm5.acl
new file mode 100644
index 000000000000..652bbecb84b2
--- /dev/null
+++ b/ci/files/mit/kadm5.acl
@@ -0,0 +1 @@
+test/admin@MIT.TEST mci testuser@MIT.TEST
diff --git a/ci/files/mit/kdc.conf b/ci/files/mit/kdc.conf
new file mode 100644
index 000000000000..7bf4e6a06e95
--- /dev/null
+++ b/ci/files/mit/kdc.conf
@@ -0,0 +1,19 @@
+[kdcdefaults]
+ kdc_ports = 88
+ kdc_tcp_ports = 88
+ restrict_anonymous_to_tgt = true
+
+[realms]
+ MIT.TEST = {
+ database_name = /var/lib/krb5kdc/principal
+ admin_keytab = /var/lib/krb5kdc/kadm5.keytab
+ acl_file = /etc/krb5kdc/kadm5.acl
+ key_stash_file = /var/lib/krb5kdc/stash
+ max_life = 1d 1h 0m 0s
+ max_renewable_life = 7d 0h 0m 0s
+ master_key_type = aes256-cts
+ supported_enctypes = aes256-cts:normal
+ default_principal_flags = +preauth
+ pkinit_identity = FILE:/var/lib/krb5kdc/kdc.pem,/var/lib/krb5kdc/kdckey.pem
+ pkinit_anchors = FILE:/etc/krb5kdc/cacert.pem
+ }
diff --git a/ci/files/mit/krb5.conf b/ci/files/mit/krb5.conf
new file mode 100644
index 000000000000..9b0d5ab9dbdf
--- /dev/null
+++ b/ci/files/mit/krb5.conf
@@ -0,0 +1,19 @@
+[libdefaults]
+ default_realm = MIT.TEST
+ dns_lookup_kdc = false
+ dns_lookup_realm = false
+ rdns = false
+ renew_lifetime = 7d
+ ticket_lifetime = 25h
+
+[realms]
+ MIT.TEST = {
+ kdc = 127.0.0.1
+ master_kdc = 127.0.0.1
+ admin_server = 127.0.0.1
+ pkinit_anchors = FILE:/etc/krb5kdc/cacert.pem
+ }
+
+[logging]
+ kdc = SYSLOG:NOTICE
+ default = SYSLOG:NOTICE
diff --git a/ci/install b/ci/install
new file mode 100755
index 000000000000..b53ac2957546
--- /dev/null
+++ b/ci/install
@@ -0,0 +1,18 @@
+#!/bin/sh
+#
+# Install packages for integration tests.
+#
+# This script is normally run via sudo in a test container or VM, such as via
+# GitHub Actions.
+#
+# Copyright 2015-2021 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: MIT
+
+set -eux
+
+# Install distribution packages.
+apt-get update -qq
+apt-get install aspell autoconf automake cppcheck heimdal-multidev \
+ krb5-config libkrb5-dev libpam0g-dev libtest-pod-perl \
+ libtest-spelling-perl libtool perl valgrind
diff --git a/ci/kdc-setup-heimdal b/ci/kdc-setup-heimdal
new file mode 100755
index 000000000000..9d15b1a4a6de
--- /dev/null
+++ b/ci/kdc-setup-heimdal
@@ -0,0 +1,105 @@
+#!/bin/sh
+#
+# Build a Kerberos test realm for Heimdal.
+#
+# This script automates the process of setting up a Kerberos test realm from
+# scratch suitable for testing pam-krb5. It is primarily intended to be run
+# from inside CI in a VM or container from the top of the pam-krb5 source
+# tree, and must be run as root. It expects to be operating on the Debian
+# Heimdal package.
+#
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: MIT
+
+set -eux
+
+# Install the KDC.
+apt-get install heimdal-kdc
+
+# Install its configuration files.
+cp ci/files/heimdal/heimdal-kdc /etc/default/heimdal-kdc
+cp ci/files/heimdal/kadmind.acl /etc/heimdal-kdc/kadmind.acl
+cp ci/files/heimdal/kdc.conf /etc/heimdal-kdc/kdc.conf
+cp ci/files/heimdal/krb5.conf /etc/krb5.conf
+cp ci/files/heimdal/pki-mapping /etc/heimdal-kdc/pki-mapping
+
+# Some versions of heimdal-kdc require this.
+ln -s /etc/heimdal-kdc/kadmind.acl /var/lib/heimdal-kdc/kadmind.acl
+
+# Add domain-realm mappings for the local host, since otherwise Heimdal and
+# MIT Kerberos may attempt to discover the realm of the local domain, and the
+# DNS server for GitHub Actions has a habit of just not responding and causing
+# the test to hang.
+cat <<EOF >>/etc/krb5.conf
+[domain_realm]
+ $(hostname -f) = HEIMDAL.TEST
+EOF
+cat <<EOF >>/etc/heimdal-kdc/kdc.conf
+[domain_realm]
+ $(hostname -f) = HEIMDAL.TEST
+EOF
+
+# Create the basic KDC.
+kstash --random-key
+kadmin -l init --realm-max-ticket-life='1 day 1 hour' \
+ --realm-max-renewable-life='1 week' HEIMDAL.TEST
+
+# Set default principal policies.
+kadmin -l modify --attributes=requires-pre-auth,disallow-svr \
+ default@HEIMDAL.TEST
+
+# Create and store the keytabs.
+kadmin -l add -r --use-defaults --attributes=requires-pre-auth \
+ test/admin@HEIMDAL.TEST
+kadmin -l ext_keytab -k tests/config/admin-keytab test/admin@HEIMDAL.TEST
+kadmin -l add -r --use-defaults --attributes=requires-pre-auth \
+ test/keytab@HEIMDAL.TEST
+kadmin -l ext_keytab -k tests/config/keytab test/keytab@HEIMDAL.TEST
+
+# Create a user principal with a known password.
+password="iceedKaicVevjunwiwyd"
+kadmin -l add --use-defaults --password="$password" testuser@HEIMDAL.TEST
+echo 'testuser@HEIMDAL.TEST' >tests/config/password
+echo "$password" >>tests/config/password
+
+# Create the root CA for PKINIT.
+mkdir -p /etc/heimdal-kdc/ca
+hxtool issue-certificate --self-signed --issue-ca --generate-key=rsa \
+ --subject=CN=CA,DC=HEIMDAL,DC=TEST --lifetime=10years \
+ --certificate=FILE:/etc/heimdal-kdc/ca/ca.pem
+chmod 644 /etc/heimdal-kdc/ca/ca.pem
+
+# Create the certificate for the Heimdal Kerberos KDC.
+hxtool issue-certificate --ca-certificate=FILE:/etc/heimdal-kdc/ca/ca.pem \
+ --generate-key=rsa --type=pkinit-kdc \
+ --pk-init-principal=krbtgt/HEIMDAL.TEST@HEIMDAL.TEST \
+ --subject=uid=kdc,DC=HEIMDAL,DC=TEST \
+ --certificate=FILE:/etc/heimdal-kdc/kdc.pem
+chmod 644 /etc/heimdal-kdc/kdc.pem
+
+# Create the certificate for the Heimdal client.
+hxtool issue-certificate --ca-certificate=FILE:/etc/heimdal-kdc/ca/ca.pem \
+ --generate-key=rsa --type=pkinit-client \
+ --pk-init-principal=testuser@HEIMDAL.TEST \
+ --subject=UID=testuser,DC=HEIMDAL,DC=TEST \
+ --certificate=FILE:tests/config/pkinit-cert
+echo 'testuser@HEIMDAL.TEST' >tests/config/pkinit-principal
+
+# Fix permissions on all the newly-created files.
+chmod 644 tests/config/*
+
+# Restart the Heimdal KDC and services.
+systemctl stop heimdal-kdc
+systemctl start heimdal-kdc
+
+# Ensure that the KDC is running.
+for n in $(seq 1 5); do
+ if echo "$password" \
+ | kinit --password-file=STDIN testuser@HEIMDAL.TEST; then
+ break
+ fi
+ sleep 1
+done
+klist
+kdestroy
diff --git a/ci/kdc-setup-mit b/ci/kdc-setup-mit
new file mode 100755
index 000000000000..0b3dfb60b64b
--- /dev/null
+++ b/ci/kdc-setup-mit
@@ -0,0 +1,102 @@
+#!/bin/sh
+#
+# Build a Kerberos test realm for MIT Kerberos
+#
+# This script automates the process of setting up a Kerberos test realm from
+# scratch suitable for testing pam-krb5. It is primarily intended to be run
+# from inside CI in a VM or container from the top of the pam-krb5 source
+# tree, and must be run as root. It expects to be operating on the Debian
+# MIT Kerberos package.
+#
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: MIT
+
+set -eux
+
+# Install the KDC and the OpenSSL command line tool.
+apt-get install krb5-admin-server krb5-kdc krb5-pkinit openssl
+
+# Install its configuration files.
+cp ci/files/mit/extensions.client /etc/krb5kdc/extensions.client
+cp ci/files/mit/extensions.kdc /etc/krb5kdc/extensions.kdc
+cp ci/files/mit/kadm5.acl /etc/krb5kdc/kadm5.acl
+cp ci/files/mit/kdc.conf /etc/krb5kdc/kdc.conf
+cp ci/files/mit/krb5.conf /etc/krb5.conf
+
+# Add domain-realm mappings for the local host, since otherwise Heimdal and
+# MIT Kerberos may attempt to discover the realm of the local domain, and the
+# DNS server for GitHub Actions has a habit of just not responding and causing
+# the test to hang.
+cat <<EOF >>/etc/krb5.conf
+[domain_realm]
+ $(hostname -f) = MIT.TEST
+EOF
+
+# Create the basic KDC.
+kdb5_util create -s -P 'this is a test master database password'
+
+# Create and store the keytabs.
+kadmin.local -q 'add_principal +requires_preauth -randkey test/admin@MIT.TEST'
+kadmin.local -q 'ktadd -k tests/config/admin-keytab test/admin@MIT.TEST'
+kadmin.local -q 'add_principal +requires_preauth -randkey test/keytab@MIT.TEST'
+kadmin.local -q 'ktadd -k tests/config/keytab test/keytab@MIT.TEST'
+
+# Enable anonymous PKINIT.
+kadmin.local -q 'addprinc -randkey WELLKNOWN/ANONYMOUS'
+
+# Create a user principal with a known password.
+password="iceedKaicVevjunwiwyd"
+kadmin.local -q \
+ "add_principal +requires_preauth -pw $password testuser@MIT.TEST"
+echo 'testuser@MIT.TEST' >tests/config/password
+echo "$password" >>tests/config/password
+
+# Create the root CA for PKINIT.
+openssl genrsa -out /etc/krb5kdc/cakey.pem 2048
+openssl req -key /etc/krb5kdc/cakey.pem -new -x509 \
+ -out /etc/krb5kdc/cacert.pem -subj "/CN=MIT.TEST CA" -days 3650
+chmod 755 /etc/krb5kdc
+chmod 644 /etc/krb5kdc/cacert.pem
+
+# Create the certificate for the MIT Kerberos KDC.
+openssl genrsa -out /var/lib/krb5kdc/kdckey.pem 2048
+openssl req -new -out /var/lib/krb5kdc/kdc.req \
+ -key /var/lib/krb5kdc/kdckey.pem -subj "/CN=MIT.TEST"
+REALM=MIT.TEST openssl x509 -req -in /var/lib/krb5kdc/kdc.req \
+ -CAkey /etc/krb5kdc/cakey.pem -CA /etc/krb5kdc/cacert.pem \
+ -out /var/lib/krb5kdc/kdc.pem -days 365 \
+ -extfile /etc/krb5kdc/extensions.kdc -extensions kdc_cert \
+ -CAcreateserial
+rm /var/lib/krb5kdc/kdc.req
+
+# Create the certificate for the MIT Kerberos client.
+openssl genrsa -out clientkey.pem 2048
+openssl req -new -key clientkey.pem -out client.req \
+ -subj "/CN=testuser@MIT.TEST"
+REALM="MIT.TEST" CLIENT="testuser" openssl x509 \
+ -CAkey /etc/krb5kdc/cakey.pem -CA /etc/krb5kdc/cacert.pem -req \
+ -in client.req -extensions client_cert \
+ -extfile /etc/krb5kdc/extensions.client -days 365 -out client.pem
+cat client.pem clientkey.pem >tests/config/pkinit-cert
+rm clientkey.pem client.pem client.req
+echo 'testuser@MIT.TEST' >tests/config/pkinit-principal
+
+# Fix permissions on all the newly-created files.
+chmod 644 tests/config/*
+
+# Restart the MIT Kerberos KDC and services.
+systemctl stop krb5-kdc krb5-admin-server
+systemctl start krb5-kdc krb5-admin-server
+
+# Ensure that the KDC is running.
+for n in $(seq 1 5); do
+ if echo "$password" | kinit testuser@MIT.TEST; then
+ break
+ fi
+ sleep 1
+done
+klist
+kdestroy
+kinit -n @MIT.TEST
+kinit -X X509_user_identity=FILE:tests/config/pkinit-cert testuser@MIT.TEST
diff --git a/ci/test b/ci/test
new file mode 100755
index 000000000000..b7844bdd75fe
--- /dev/null
+++ b/ci/test
@@ -0,0 +1,44 @@
+#!/bin/sh
+#
+# Run tests for continuous integration.
+#
+# This script is normally run in a test container or VM, such as via GitHub
+# Actions.
+#
+# Copyright 2015-2021 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: MIT
+
+set -eux
+
+# Normally, KERBEROS is set based on the CI matrix, but provide a default in
+# case someone runs this test by hand.
+KERBEROS="${KERBEROS:-mit}"
+
+# Generate Autotools files.
+./bootstrap
+
+# Build everything with Clang first, with warnings enabled.
+if [ "$KERBEROS" = 'heimdal' ]; then
+ ./configure CC=clang PATH_KRB5_CONFIG=/usr/bin/krb5-config.heimdal
+else
+ ./configure CC=clang
+fi
+make warnings
+
+# Then rebuild everything with GCC with warnings enabled.
+make distclean
+if [ "$KERBEROS" = 'heimdal' ]; then
+ ./configure CC=gcc PATH_KRB5_CONFIG=/usr/bin/krb5-config.heimdal
+else
+ ./configure CC=gcc
+fi
+make warnings
+
+# Run the tests with valgrind.
+make check-valgrind
+
+# Run additional style tests, but only in the MIT build.
+if [ "$KERBEROS" = "mit" ]; then
+ make check-cppcheck
+fi
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 000000000000..eddc6fd46559
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,145 @@
+dnl Autoconf configuration for pam-krb5.
+dnl
+dnl Written by Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2005-2009, 2014, 2017, 2020-2021 Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2009-2013
+dnl The Board of Trustees of the Leland Stanford Junior University
+dnl Copyright 2005 Andres Salomon <dilinger@debian.org>
+dnl Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+dnl
+dnl SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+AC_PREREQ([2.64])
+AC_INIT([pam-krb5], [4.11], [eagle@eyrie.org])
+AC_CONFIG_AUX_DIR([build-aux])
+AC_CONFIG_LIBOBJ_DIR([portable])
+AC_CONFIG_MACRO_DIR([m4])
+AM_INIT_AUTOMAKE([1.11 check-news dist-xz foreign silent-rules subdir-objects
+ -Wall -Werror])
+AM_MAINTAINER_MODE
+
+dnl Detect unexpanded macros.
+m4_pattern_forbid([^PKG_])
+m4_pattern_forbid([^_?RRA_])
+
+AC_PROG_CC
+AC_USE_SYSTEM_EXTENSIONS
+RRA_PROG_CC_WARNINGS_FLAGS
+AC_SYS_LARGEFILE
+AM_PROG_CC_C_O
+m4_ifdef([AM_PROG_AR], [AM_PROG_AR])
+AC_PROG_INSTALL
+LT_INIT([disable-static])
+AC_CANONICAL_HOST
+RRA_LD_VERSION_SCRIPT
+
+dnl Only used for the test suite.
+AC_ARG_VAR([PATH_OPENSSL], [Path to openssl for the test suite])
+AC_PATH_PROG([PATH_OPENSSL], [openssl])
+AS_IF([test x"$PATH_OPENSSL" != x],
+ [AC_DEFINE_UNQUOTED([PATH_OPENSSL], ["$PATH_OPENSSL"],
+ [Define to the full path to openssl for some tests.])])
+AC_ARG_VAR([PATH_VALGRIND], [Path to valgrind for the test suite])
+AC_PATH_PROG([PATH_VALGRIND], [valgrind])
+
+dnl Probe for the functionality of the PAM libraries and their include file
+dnl naming. Mac OS X puts them in pam/* instead of security/*.
+AC_SEARCH_LIBS([pam_set_data], [pam])
+AC_CHECK_FUNCS([pam_getenv pam_getenvlist pam_modutil_getpwnam])
+AC_REPLACE_FUNCS([pam_syslog pam_vsyslog])
+AC_CHECK_HEADERS([security/pam_modutil.h], [],
+ [AC_CHECK_HEADERS([pam/pam_modutil.h])])
+AC_CHECK_HEADERS([security/pam_appl.h], [],
+ [AC_CHECK_HEADERS([pam/pam_appl.h], [],
+ [AC_MSG_ERROR([No PAM header files found])])])
+AC_CHECK_HEADERS([security/pam_ext.h], [],
+ [AC_CHECK_HEADERS([pam/pam_ext.h])])
+RRA_HEADER_PAM_CONST
+RRA_HEADER_PAM_STRERROR_CONST
+AC_DEFINE([MODULE_NAME], ["pam_krb5"],
+ [The name of the PAM module, used by the pam_vsyslog replacement.])
+
+dnl Probe for the location and functionality of the Kerberos libraries.
+RRA_LIB_KRB5
+RRA_LIB_KRB5_SWITCH
+AC_CHECK_HEADERS([hx509_err.h])
+AC_CHECK_MEMBER([krb5_creds.session],
+ [AC_DEFINE([HAVE_KRB5_HEIMDAL], [1],
+ [Define if your Kerberos implementation is Heimdal.])],
+ [AC_DEFINE([HAVE_KRB5_MIT], [1],
+ [Define if your Kerberos implementation is MIT.])],
+ [RRA_INCLUDES_KRB5])
+AC_CHECK_TYPES([krb5_realm], [], [], [RRA_INCLUDES_KRB5])
+AC_CHECK_FUNCS([krb5_cc_get_full_name \
+ krb5_data_free \
+ krb5_free_default_realm \
+ krb5_free_string \
+ krb5_get_init_creds_opt_alloc \
+ krb5_get_init_creds_opt_set_anonymous \
+ krb5_get_init_creds_opt_set_change_password_prompt \
+ krb5_get_init_creds_opt_set_default_flags \
+ krb5_get_init_creds_opt_set_fast_ccache_name \
+ krb5_get_init_creds_opt_set_out_ccache \
+ krb5_get_init_creds_opt_set_pa \
+ krb5_get_prompt_types \
+ krb5_init_secure_context \
+ krb5_principal_get_realm \
+ krb5_principal_set_comp_string \
+ krb5_set_password \
+ krb5_set_trace_filename \
+ krb5_verify_init_creds_opt_init \
+ krb5_xfree])
+AC_CHECK_FUNCS([krb5_get_init_creds_opt_set_pkinit],
+ [RRA_FUNC_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT_ARGS])
+AC_CHECK_FUNCS([krb5_get_init_creds_opt_free],
+ [RRA_FUNC_KRB5_GET_INIT_CREDS_OPT_FREE_ARGS])
+AC_CHECK_DECLS([krb5_kt_free_entry], [], [], [RRA_INCLUDES_KRB5])
+AC_CHECK_FUNCS([krb5_appdefault_string], [],
+ [AC_CHECK_FUNCS([krb5_get_profile])
+ AC_CHECK_HEADERS([k5profile.h profile.h])
+ AC_LIBOBJ([krb5-profile])])
+AC_LIBOBJ([krb5-extra])
+RRA_LIB_KRB5_RESTORE
+
+dnl The kadmin client libraries are only used for the test suite.
+RRA_LIB_KADM5CLNT_OPTIONAL
+RRA_LIB_KADM5CLNT_SWITCH
+AC_CHECK_HEADERS([kadm5/kadm5_err.h])
+AC_CHECK_FUNCS([kadm5_init_krb5_context kadm5_init_with_skey_ctx])
+RRA_LIB_KADM5CLNT_RESTORE
+
+dnl Regex support is only used for the test suite.
+AC_CHECK_HEADER([regex.h], [AC_CHECK_FUNCS([regcomp])])
+
+dnl Other probes of the system libraries.
+AC_HEADER_STDBOOL
+AC_CHECK_HEADERS([strings.h sys/bittypes.h sys/select.h sys/time.h])
+AC_CHECK_DECLS([reallocarray])
+AC_TYPE_LONG_LONG_INT
+AC_CHECK_TYPES([ssize_t], [], [],
+ [#include <sys/types.h>])
+AC_CHECK_FUNCS([explicit_bzero])
+AC_REPLACE_FUNCS([asprintf issetugid mkstemp reallocarray strndup])
+
+dnl Try to specify the binding so that any references within the PAM module
+dnl are resolved to the functions in that module in preference to any external
+dnl function.
+dnl
+dnl More platforms could be handled here. Contributions welcome.
+AS_CASE([$host],
+ [*-hpux*],
+ [AS_IF([test x"$GCC" = x"yes"],
+ [AM_LDFLAGS="-Wl,-Bsymbolic $AM_LDFLAGS"],
+ [AM_LDFLAGS="-Wl,+vshlibunsats $AM_LDFLAGS"])],
+
+ [*-linux*],
+ [AM_LDFLAGS="-Wl,-z,defs -Wl,-Bsymbolic $AM_LDFLAGS"],
+
+ [*-solaris2*],
+ [AS_IF([test x"$GCC" = x"yes"],
+ [AM_LDFLAGS="-Wl,-Bsymbolic $AM_LDFLAGS"],
+ [AM_LDFLAGS="-Wl,-xldscope=symbolic $AM_LDFLAGS"])])
+
+AC_CONFIG_HEADERS([config.h])
+AC_CONFIG_FILES([Makefile])
+AC_OUTPUT
diff --git a/docs/docknot.yaml b/docs/docknot.yaml
new file mode 100644
index 000000000000..67e19f88d50a
--- /dev/null
+++ b/docs/docknot.yaml
@@ -0,0 +1,551 @@
+# Package metadata for pam-krb5.
+#
+# This file contains configuration for DocKnot used to generate
+# documentation files (like README.md) and web pages. Other documentation
+# in this package is generated automatically from these files as part of
+# the release process. For more information, see DocKnot's documentation.
+#
+# DocKnot is available from <https://www.eyrie.org/~eagle/software/docknot/>.
+#
+# Copyright 2017, 2020-2021 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+format: v1
+
+name: pam-krb5
+maintainer: Russ Allbery <eagle@eyrie.org>
+version: '4.11'
+synopsis: PAM module for Kerberos authentication
+
+license:
+ name: BSD-3-clause-or-GPL-1+
+copyrights:
+ - holder: Russ Allbery <eagle@eyrie.org>
+ years: 2005-2010, 2014-2015, 2017, 2020-2021
+ - holder: The Board of Trustees of the Leland Stanford Junior University
+ years: 2009-2011
+ - holder: Andres Salomon <dilinger@debian.org>
+ years: '2005'
+ - holder: Frank Cusack <fcusack@fcusack.com>
+ years: 1999-2000
+
+build:
+ autoconf: '2.64'
+ automake: '1.11'
+ autotools: true
+ kerberos: true
+ manpages: true
+ middle: |
+ The module will be installed in `/usr/local/lib/security` by default, but
+ expect to have to override this using `--libdir`. The correct
+ installation path for PAM modules varies considerably between systems.
+ The module will always be installed in a subdirectory named `security`
+ under the specified value of `--libdir`. On Red Hat Linux, for example,
+ `--libdir=/usr/lib64` is appropriate to install the module into the system
+ PAM directory. On Debian's amd64 architecture,
+ `--libdir=/usr/lib/x86_64-linux-gnu` would be correct.
+ reduced_depends: true
+ type: Autoconf
+ valgrind: true
+distribution:
+ packaging:
+ debian:
+ package: libpam-krb5
+ summary: |
+ Debian packages are available from Debian in Debian 4.0 (etch) and
+ later releases as libpam-krb5 and libpam-heimdal. The former packages
+ are built against the MIT Kerberos libraries and the latter against
+ the Heimdal libraries.
+ section: kerberos
+ tarname: pam-krb5
+ version: pam-krb5
+support:
+ email: eagle@eyrie.org
+ github: rra/pam-krb5
+ web: https://www.eyrie.org/~eagle/software/pam-krb5/
+vcs:
+ browse: https://git.eyrie.org/?p=kerberos/pam-krb5.git
+ github: rra/pam-krb5
+ openhub: https://www.openhub.net/p/pamkrb5
+ status:
+ workflow: build
+ type: Git
+ url: https://git.eyrie.org/git/kerberos/pam-krb5.git
+
+quote:
+ author: Joyce McGreevy
+ date: 2003-11-17
+ text: |
+ "You're always going to have some people who can't appreciate the thrill
+ of a tepid change for the somewhat better," explained one source.
+ title: '"Look, ma, no hands!"'
+ work: Salon
+advisories:
+ - date: 2020-03-30
+ threshold: '4.9'
+ versions: 4.8 and earlier
+ - date: 2009-02-11
+ threshold: '3.13'
+ versions: 3.12 and earlier
+docs:
+ user:
+ - name: pam-krb5
+ title: Manual page
+
+blurb: |
+ pam-krb5 is a Kerberos PAM module for either MIT Kerberos or Heimdal. It
+ supports ticket refreshing by screen savers, configurable authorization
+ handling, authentication of non-local accounts for network services,
+ password changing, and password expiration, as well as all the standard
+ expected PAM features. It works correctly with OpenSSH, even with
+ ChallengeResponseAuthentication and PrivilegeSeparation enabled, and
+ supports extensive configuration either by PAM options or in krb5.conf or
+ both. PKINIT is supported with recent versions of both MIT Kerberos and
+ Heimdal and FAST is supported with recent MIT Kerberos.
+
+description: |
+ pam-krb5 provides a Kerberos PAM module that supports authentication, user
+ ticket cache handling, simple authorization (via .k5login or checking
+ Kerberos principals against local usernames), and password changing. It can
+ be configured through either options in the PAM configuration itself or
+ through entries in the system krb5.conf file, and it tries to work around
+ PAM implementation flaws in commonly-used PAM-enabled applications such as
+ OpenSSH and xdm. It supports both PKINIT and FAST to the extent that the
+ underlying Kerberos libraries support these features.
+
+ This is not the Kerberos PAM module maintained on Sourceforge and used on
+ Red Hat systems. It is an independent implementation that, if it ever
+ shared any common code, diverged long ago. It supports some features that
+ the Sourceforge module does not (particularly around authorization), and
+ does not support some options (particularly ones not directly related to
+ Kerberos) that it does. This module will never support Kerberos v4 or AFS.
+ For an AFS session module that works with this module (or any other Kerberos
+ PAM module), see
+ [pam-afs-session](https://www.eyrie.org/~eagle/software/pam-afs-session/).
+
+ If there are other options besides AFS and Kerberos v4 support from the
+ Sourceforge PAM module that you're missing in this module, please let me
+ know.
+
+requirements: |
+ Either MIT Kerberos (or Kerberos implementations based on it) or Heimdal are
+ supported. MIT Keberos 1.3 or later may be required; this module has not
+ been tested with earlier versions.
+
+ For PKINIT support, Heimdal 0.8rc1 or later or MIT Kerberos 1.6.3 or later
+ are required. Earlier MIT Kerberos 1.6 releases have a bug in their
+ handling of PKINIT options. MIT Kerberos 1.12 or later is required to use
+ the use_pkinit PAM option.
+
+ For FAST (Flexible Authentication Secure Tunneling) support, MIT Kerberos
+ 1.7 or higher is required. For anonymous FAST support, anonymous
+ authentication (generally anonymous PKINIT) support is required in both the
+ Kerberos libraries and in the local KDC.
+
+ This module should work on Linux and build with gcc or clang. It may still
+ work on Solaris and build with the Sun C compiler, but I have only tested it
+ on Linux recently. There is beta-quality support for the AIX NAS Kerberos
+ implementation that has not been tested in years. Other PAM implementations
+ will probably require some porting, although untested build system support
+ is present for FreeBSD, Mac OS X, and HP-UX. I personally can only test on
+ Linux and rely on others to report problems on other operating systems.
+
+ Old versions of OpenSSH are known to call `pam_authenticate` followed by
+ `pam_setcred(PAM_REINITIALIZE_CRED)` without first calling
+ `pam_open_session`, thereby requesting that an existing ticket cache be
+ renewed (similar to what a screensaver would want) rather than requesting a
+ new ticket cache be created. Since this behavior is indistinguishable at
+ the PAM level from a screensaver, pam-krb5 when used with these old versions
+ of OpenSSH will refresh the ticket cache of the OpenSSH daemon rather than
+ setting up a new ticket cache for the user. The resulting ticket cache will
+ have the correct permissions (this is not a security concern), but will not
+ be named correctly or referenced in the user's environment and will be
+ overwritten by the next user login. The best solution to this problem is to
+ upgrade OpenSSH. I'm not sure exactly when this problem was fixed, but at
+ the very least OpenSSH 4.3 and later do not exhibit it.
+
+test:
+ lancaster: true
+ prefix: |
+ pam-krb5 comes with a comprehensive test suite, but it requires some
+ configuration in order to test anything other than low-level utility
+ functions. For the full test suite, you will need to have a running KDC
+ in which you can create two test accounts, one with admin access to the
+ other. Using a test KDC environment, if you have one, is recommended.
+
+ Follow the instructions in `tests/config/README` to configure the test
+ suite.
+
+ Now, you can run the test suite with:
+ suffix: |
+ The default libkadm5clnt library on the system must match the
+ implementation of your KDC for the module/expired test to work, since the
+ two kadmin protocols are not compatible. If you use the MIT library
+ against a Heimdal server, the test will be skipped; if you use the Heimdal
+ library against an MIT server, the test suite may hang.
+
+ Several `module/expired` tests are expected to fail with Heimdal 1.5 due
+ to a bug in Heimdal with reauthenticating immediately after a
+ library-mediated password change of an expired password. This is fixed in
+ later releases of Heimdal.
+
+ To run the full test suite, Perl 5.10 or later is required. The following
+ additional Perl modules will be used if present:
+
+ * Test::Pod
+ * Test::Spelling
+
+ All are available on CPAN. Those tests will be skipped if the modules are
+ not available.
+
+sections:
+ - title: Configuring
+ body: |
+ Just installing the module does not enable it or change anything about
+ your system authentication configuration. To use the module for all
+ system authentication on Debian systems, put something like:
+
+ ```
+ auth sufficient pam_krb5.so minimum_uid=1000
+ auth required pam_unix.so try_first_pass nullok_secure
+ ```
+
+ in `/etc/pam.d/common-auth`, something like:
+
+ ```
+ session optional pam_krb5.so minimum_uid=1000
+ session required pam_unix.so
+ ```
+
+ in `/etc/pam.d/common-session`, and something like:
+
+ ```
+ account required pam_krb5.so minimum_uid=1000
+ account required pam_unix.so
+ ```
+
+ in `/etc/pam.d/common-account`. The `minimum_uid` setting tells the PAM
+ module to pass on any users with a UID lower than 1000, thereby
+ bypassing Kerberos authentication for the root account and any system
+ accounts. You normally want to do this since otherwise, if the network
+ is down, the Kerberos authentication can time out and make it difficult
+ to log in as root and fix matters. This also avoids problems with
+ Kerberos principals that happen to match system accounts accidentally
+ getting access to those accounts.
+
+ Be sure to include the module in the session group as well as the auth
+ group. Without the session entry, the user's ticket cache will not be
+ created properly for ssh logins (among possibly others).
+
+ If your users should normally all use Kerberos passwords exclusively,
+ putting something like:
+
+ ```
+ password sufficient pam_krb5.so minimum_uid=1000
+ password required pam_unix.so try_first_pass obscure md5
+ ```
+
+ in `/etc/pam.d/common-password` will change users' passwords in Kerberos
+ by default and then only fall back on Unix if that doesn't work. (You
+ can make this tighter by using the more complex new-style PAM
+ configuration.) If you instead want to synchronize local and Kerberos
+ passwords and change them both at the same time, you can do something
+ like:
+
+ ```
+ password required pam_unix.so obscure sha512
+ password required pam_krb5.so use_authtok minimum_uid=1000
+ ```
+
+ If you have multiple environments that you want to synchronize and you
+ don't want password changes to continue if the Kerberos password change
+ fails, use the `clear_on_fail` option. For example:
+
+ ```
+ password required pam_krb5.so clear_on_fail minimum_uid=1000
+ password required pam_unix.so use_authtok obscure sha512
+ password required pam_smbpass.so use_authtok
+ ```
+
+ In this case, if `pam_krb5` cannot change the password (due to password
+ strength rules on the KDC, for example), it will clear the stored
+ password (because of the `clear_on_fail` option), and since `pam_unix`
+ and `pam_smbpass` are both configured with `use_authtok`, they will both
+ fail. `clear_on_fail` is not the default because it would interfere
+ with the more common pattern of falling back to local passwords if the
+ user doesn't exist in Kerberos.
+
+ If you use a more complex configuration with the Linux PAM `[]` syntax
+ for the session and account groups, note that `pam_krb5` returns a
+ status of ignore, not success, if the user didn't log on with Kerberos.
+ You may need to handle that explicitly with `ignore=ignore` in your
+ action list.
+
+ There are many, many other possibilities. See the Linux PAM
+ documentation for all the configuration options.
+
+ On Red Hat systems, modify `/etc/pam.d/system-auth` instead, which
+ contains all of the configuration for the different stacks.
+
+ You can also use pam-krb5 only for specific services. In that case,
+ modify the files in `/etc/pam.d` for that particular service to use
+ `pam_krb5.so` for authentication. For services that are using passwords
+ over TLS to authenticate users, you may want to use the `ignore_k5login`
+ and `no_ccache` options to the authenticate module. `.k5login`
+ authorization is only meaningful for local accounts and ticket caches
+ are usually (although not always) only useful for interactive sessions.
+
+ Configuring the module for Solaris is both simpler and less flexible,
+ since Solaris (at least Solaris 8 and 9, which are the last versions of
+ Solaris with which this module was extensively tested) use a single
+ `/etc/pam.conf` file that contains configuration for all programs. For
+ console login on Solaris, try something like:
+
+ ```
+ login auth sufficient /usr/local/lib/security/pam_krb5.so minimum_uid=100
+ login auth required /usr/lib/security/pam_unix_auth.so.1 use_first_pass
+ login account required /usr/local/lib/security/pam_krb5.so minimum_uid=100
+ login account required /usr/lib/security/pam_unix_account.so.1
+ login session required /usr/local/lib/security/pam_krb5.so retain_after_close minimum_uid=100
+ login session required /usr/lib/security/pam_unix_session.so.1
+ ```
+
+ A similar configuration could be used for other services, such as ssh.
+ See the pam.conf(5) man page for more information. When using this
+ module with Solaris login (at least on Solaris 8 and 9), you will
+ probably also need to add `retain_after_close` to the PAM configuration
+ to avoid having the user's credentials deleted before they are logged
+ in.
+
+ The Solaris Kerberos library reportedly does not support prompting for a
+ password change of an expired account during authentication. Supporting
+ password change for expired accounts on Solaris with native Kerberos may
+ therefore require setting the `defer_pwchange` or `force_pwchange`
+ option for selected login applications. See the description and
+ warnings about that option in the pam_krb5(5) man page.
+
+ Some configuration options may be put in the `krb5.conf` file used by
+ your Kerberos libraries (usually `/etc/krb5.conf` or
+ `/usr/local/etc/krb5.conf`) instead or in addition to the PAM
+ configuration. See the man page for more details.
+
+ The Kerberos library, via pam-krb5, will prompt the user to change their
+ password if their password is expired, but when using OpenSSH, this will
+ only work when `ChallengeResponseAuthentication` is enabled. Unless
+ this option is enabled, OpenSSH doesn't pass PAM messages to the user
+ and can only respond to a simple password prompt.
+
+ If you are using MIT Kerberos, be aware that users whose passwords are
+ expired will not be prompted to change their password unless the KDC
+ configuration for your realm in `[realms]` in `krb5.conf` contains a
+ `master_kdc` setting or, if using DNS SRV records, you have a DNS entry
+ for `_kerberos-master` as well as `_kerberos`.
+ - title: Debugging
+ body: |
+ The first step when debugging any problems with this module is to add
+ `debug` to the PAM options for the module (either in the PAM
+ configuration or in `krb5.conf`). This will significantly increase the
+ logging from the module and should provide a trace of exactly what
+ failed and any available error information.
+
+ Many Kerberos authentication problems are due to configuration issues in
+ `krb5.conf`. If pam-krb5 doesn't work, first check that `kinit` works
+ on the same system. That will test your basic Kerberos configuration.
+ If the system has a keytab file installed that's readable by the process
+ doing authentication via PAM, make sure that the keytab is current and
+ contains a key for `host/<system>` where <system> is the fully-qualified
+ hostname. pam-krb5 prevents KDC spoofing by checking the user's
+ credentials when possible, but this means that if a keytab is present it
+ must be correct or authentication will fail. You can check the keytab
+ with `klist -k` and `kinit -k`.
+
+ Be sure that all libraries and modules, including PAM modules, loaded by
+ a program use the same Kerberos libraries. Sometimes programs that use
+ PAM, such as current versions of OpenSSH, also link against Kerberos
+ directly. If your sshd is linked against one set of Kerberos libraries
+ and pam-krb5 is linked against a different set of Kerberos libraries,
+ this will often cause problems (such as segmentation faults, bus errors,
+ assertions, or other strange behavior). Similar issues apply to the
+ com_err library or any other library used by both modules and shared
+ libraries and by the application that loads them. If your OS ships
+ Kerberos libraries, it's usually best if possible to build all Kerberos
+ software on the system against those libraries.
+ - title: Implementation Notes
+ body: |
+ The normal sequence of actions taken for a user login is:
+
+ ```
+ pam_authenticate
+ pam_setcred(PAM_ESTABLISH_CRED)
+ pam_open_session
+ pam_acct_mgmt
+ ```
+
+ and then at logout:
+
+ ```
+ pam_close_session
+ ```
+
+ followed by closing the open PAM session. The corresponding `pam_sm_*`
+ functions in this module are called when an application calls those
+ public interface functions. Not all applications call all of those
+ functions, or in particularly that order, although `pam_authenticate` is
+ always first and has to be.
+
+ When `pam_authenticate` is called, pam-krb5 creates a temporary ticket
+ cache in `/tmp` and sets the PAM environment variable `PAM_KRB5CCNAME`
+ to point to it. This ticket cache will be automatically destroyed when
+ the PAM session is closed and is there only to pass the initial
+ credentials to the call to `pam_setcred`. The module would use a memory
+ cache, but memory caches will only work if the application preserves the
+ PAM environment between the calls to `pam_authenticate` and
+ `pam_setcred`. Most do, but OpenSSH notoriously does not and calls
+ `pam_authenticate` in a subprocess, so this method is used to pass the
+ tickets to the `pam_setcred` call in a different process.
+
+ `pam_authenticate` does a complete authentication, including checking
+ the resulting TGT by obtaining a service ticket for the local host if
+ possible, but this requires read access to the system keytab. If the
+ keytab doesn't exist, can't be read, or doesn't include the appropriate
+ credentials, the default is to accept the authentication. This can be
+ controlled by setting `verify_ap_req_nofail` to true in `[libdefaults]`
+ in `/etc/krb5.conf`. `pam_authenticate` also does a basic authorization
+ check, by default calling `krb5_kuserok` (which uses `~/.k5login` if
+ available and falls back to checking that the principal corresponds to
+ the account name). This can be customized with several options
+ documented in the pam_krb5(5) man page.
+
+ pam-krb5 treats `pam_open_session` and `pam_setcred(PAM_ESTABLISH_CRED)`
+ as synonymous, as some applications call one and some call the other.
+ Both copy the initial credentials from the temporary cache into a
+ permanent cache for this session and set `KRB5CCNAME` in the
+ environment. It will remember when the credential cache has been
+ established and then avoid doing any duplicate work afterwards, since
+ some applications call `pam_setcred` or `pam_open_session` multiple
+ times (most notably X.Org 7 and earlier xdm, which also throws away the
+ module settings the last time it calls them).
+
+ `pam_acct_mgmt` finds the ticket cache, reads it in to obtain the
+ authenticated principal, and then does is another authorization check
+ against `.k5login` or the local account name as described above.
+
+ After the call to `pam_setcred` or `pam_open_session`, the ticket cache
+ will be destroyed whenever the calling application either destroys the
+ PAM environment or calls `pam_close_session`, which it should do on user
+ logout.
+
+ The normal sequence of events when refreshing a ticket cache (such as
+ inside a screensaver) is:
+
+ ```
+ pam_authenticate
+ pam_setcred(PAM_REINITIALIZE_CRED)
+ pam_acct_mgmt
+ ```
+
+ (`PAM_REFRESH_CRED` may be used instead.) Authentication proceeds as
+ above. At the `pam_setcred` stage, rather than creating a new ticket
+ cache, the module instead finds the current ticket cache (from the
+ `KRB5CCNAME` environment variable or the default ticket cache location
+ from the Kerberos library) and then reinitializes it with the
+ credentials from the temporary `pam_authenticate` ticket cache. When
+ refreshing a ticket cache, the application should not open a session.
+ Calling `pam_acct_mgmt` is optional; pam-krb5 doesn't do anything
+ different when it's called in this case.
+
+ If `pam_authenticate` apparently didn't succeed, or if an account was
+ configured to be ignored via `ignore_root` or `minimum_uid`,
+ `pam_setcred` (and therefore `pam_open_session`) and `pam_acct_mgmt`
+ return `PAM_IGNORE`, which tells the PAM library to proceed as if that
+ module wasn't listed in the PAM configuration at all.
+ `pam_authenticate`, however, returns failure in the ignored user case by
+ default, since otherwise a configuration using `ignore_root` with
+ pam-krb5 as the only PAM module would allow anyone to log in as root
+ without a password. There doesn't appear to be a case where returning
+ `PAM_IGNORE` instead would improve the module's behavior, but if you
+ know of a case, please let me know.
+
+ By default, `pam_authenticate` intentionally does not follow the PAM
+ standard for handling expired accounts and instead returns failure from
+ `pam_authenticate` unless the Kerberos libraries are able to change the
+ account password during authentication. Too many applications either do
+ not call `pam_acct_mgmt` or ignore its exit status. The fully correct
+ PAM behavior (returning success from `pam_authenticate` and
+ `PAM_NEW_AUTHTOK_REQD` from `pam_acct_mgmt`) can be enabled with the
+ `defer_pwchange` option.
+
+ The `defer_pwchange` option is unfortunately somewhat tricky to
+ implement. In this case, the calling sequence is:
+
+ ```
+ pam_authenticate
+ pam_acct_mgmt
+ pam_chauthtok
+ pam_setcred
+ pam_open_session
+ ```
+
+ During the first `pam_authenticate`, we can't obtain credentials and
+ therefore a ticket cache since the password is expired. But
+ `pam_authenticate` isn't called again after `pam_chauthtok`, so
+ `pam_chauthtok` has to create a ticket cache. We however don't want it
+ to do this for the normal password change (`passwd`) case.
+
+ What we do is set a flag in our PAM data structure saying that we're
+ processing an expired password, and `pam_chauthtok`, if it sees that
+ flag, redoes the authentication with password prompting disabled after
+ it finishes changing the password.
+
+ Unfortunately, when handling password changes this way, `pam_chauthtok`
+ will always have to prompt the user for their current password again
+ even though they just typed it. This is because the saved
+ authentication tokens are cleared after `pam_authenticate` returns, for
+ security reasons. We could hack around this by saving the password in
+ our PAM data structure, but this would let the application gain access
+ to it (exactly what the clearing is intended to prevent) and breaks a
+ PAM library guarantee. We could also work around this by having
+ `pam_authenticate` get the `kadmin/changepw` authenticator in the
+ expired password case and store it for `pam_chauthtok`, but it doesn't
+ seem worth the hassle.
+ - title: History and Acknowledgements
+ body: |
+ Originally written by Frank Cusack <fcusack@fcusack.com>, with the
+ following acknowledgement:
+
+ > Thanks to Naomaru Itoi <itoi@eecs.umich.edu>, Curtis King
+ > <curtis.king@cul.ca>, and Derrick Brashear <shadow@dementia.org>, all
+ > of whom have written and made available Kerberos 4/5 modules.
+ > Although no code in this module is directly from these author's
+ > modules, (except the get_user_info() routine in support.c; derived
+ > from whichever of these authors originally wrote the first module the
+ > other 2 copied from), it was extremely helpful to look over their code
+ > which aided in my design.
+
+ The module was then patched for the FreeBSD ports collection with
+ additional modifications by unknown maintainers and then was modified by
+ Joel Kociolek <joko@logidee.com> to be usable with Debian GNU/Linux.
+
+ It was packaged by Sam Hartman as the Kerberos v5 PAM module for Debian
+ and improved and modified by him and later by Russ Allbery to fix bugs
+ and add additional features. It was then adopted by Andres Salomon, who
+ added support for refreshing credentials.
+
+ The current distribution is maintained by Russ Allbery, who also added
+ support for reading configuration from `krb5.conf`, added many features
+ for compatibility with the Sourceforge module, commented and
+ standardized the formatting of the code, and overhauled the
+ documentation.
+
+ Thanks to Douglas E. Engert for the initial implementation of PKINIT
+ support. I have since modified and reworked it extensively, so any bugs
+ or compilation problems are my fault.
+
+ Thanks to Markus Moeller for lots of debugging and multiple patches and
+ suggestions for improved portability.
+
+ Thanks to Booker Bense for the implementation of the `alt_auth_map`
+ option.
+
+ Thanks to Sam Hartman for the FAST support implementation.
diff --git a/docs/pam_krb5.pod b/docs/pam_krb5.pod
new file mode 100644
index 000000000000..024584dfd4cd
--- /dev/null
+++ b/docs/pam_krb5.pod
@@ -0,0 +1,1056 @@
+=for stopwords
+KRB5CCNAME ChallengeResponseAuthentication GSS-API Heimdal KDC PKINIT
+PasswordAuthentication SRV Solaris Sourceforge aname appdefaults auth
+canonicalized ccache krb5.conf forwardable kdestroy keytab libdefaults
+logout pam-krb5 preauth 0.8rc1 screensaver screensavers sshd localname
+krb5.conf. 0.8rc1. Allbery Cusack Salomon FSFAP SPDX-License-Identifier
+responder
+
+=head1 NAME
+
+pam_krb5 - Kerberos PAM module
+
+=head1 SYNOPSIS
+
+ auth sufficient pam_krb5.so minimum_uid=1000
+ session required pam_krb5.so minimum_uid=1000
+ account required pam_krb5.so minimum_uid=1000
+ password sufficient pam_krb5.so minimum_uid=1000
+
+=head1 DESCRIPTION
+
+The Kerberos service module for PAM, typically installed at
+F</lib/security/pam_krb5.so>, provides functionality for the four PAM
+operations: authentication, account management, session management, and
+password management. F<pam_krb5.so> is a shared object that is
+dynamically loaded by the PAM subsystem as necessary, based on the system
+PAM configuration. PAM is a system for plugging in external
+authentication and session management modules so that each application
+doesn't have to know the best way to check user authentication or create a
+user session on that system. For details on how to configure PAM on your
+system, see the PAM man page, often pam(7).
+
+Here are the actions of this module when called from each group:
+
+=over 4
+
+=item auth
+
+Provides implementations of pam_authenticate() and pam_setcred(). The
+former takes the username from the PAM session, prompts for the user's
+password (unless configured to use an already-entered password), and then
+performs a Kerberos initial authentication, storing the obtained
+credentials (if successful) in a temporary ticket cache. The latter,
+depending on the flags it is called with, either takes the contents of the
+temporary ticket cache and writes it out to a persistent ticket cache
+owned by the user or uses the temporary ticket cache to refresh an
+existing user ticket cache.
+
+Passwords as long or longer than PAM_MAX_RESP_SIZE octets (normally 512
+octets) will be rejected, since excessively long passwords can be used as
+a denial of service attack.
+
+After doing the initial authentication, the Kerberos PAM module will
+attempt to obtain tickets for a key in the local system keytab and then
+verify those tickets. Unless this step is performed, the authentication
+is vulnerable to KDC spoofing, but it requires that the system have a
+local key and that the PAM module be running as a user that can read the
+keytab file (normally F</etc/krb5.keytab>. You can point the Kerberos PAM
+module at a different keytab with the I<keytab> option. If that keytab
+cannot be read or if no keys are found in it, the default (potentially
+insecure) behavior is to skip this check. If you want to instead fail
+authentication if the obtained tickets cannot be checked, set
+C<verify_ap_req_nofail> to true in the [libdefaults] section of
+F</etc/krb5.conf>. Note that this will affect applications other than
+this PAM module.
+
+By default, whenever the user is authenticated, a basic authorization
+check will also be done using krb5_kuserok(). The default behavior of
+this function is to check the user's account for a F<.k5login> file and,
+if one is present, ensure that the user's principal is listed in that
+file. If F<.k5login> is not present, the default check is to ensure that
+the user's principal is in the default local realm and the user portion of
+the principal matches the account name (this can be changed by configuring
+a custom aname to localname mapping in F<krb5.conf>; see the Kerberos
+documentation for details). This can be customized with several
+configuration options; see below.
+
+If the username provided to PAM contains an C<@> and Kerberos can,
+treating the username as a principal, map it to a local account name,
+pam_authenticate() will change the PAM user to that local account name.
+This allows users to log in with their Kerberos principal and let Kerberos
+do the mapping to an account. This can be disabled with the
+I<no_update_user> option. Be aware, however, that this facility cannot be
+used with OpenSSH. OpenSSH will reject usernames that don't match local
+accounts before this remapping can be done and will pass an invalid
+password to the PAM module. Also be aware that several other common PAM
+modules, such as pam_securetty, expect to be able to look up the user with
+getpwnam() and cannot be called before pam_krb5 when using this feature.
+
+When pam_setcred() is called to initialize a new ticket cache, the
+environment variable KRB5CCNAME is set to the path to that ticket cache.
+By default, the cache will be named F</tmp/krb5cc_UID_RANDOM> where UID is
+the user's UID and RANDOM is six randomly-chosen letters. This can be
+configured with the I<ccache> and I<ccache_dir> options.
+
+pam-krb5 does not use the default ticket cache location or
+I<default_cc_name> in the C<[libdefaults]> section of F<krb5.conf>. The
+default cache location would share a cache for all sessions of the same
+user, which causes confusing behavior when the user logs out of one of
+multiple sessions.
+
+If pam_setcred() initializes a new ticket cache, it will also set up that
+ticket cache so that it will be deleted when the PAM session is closed.
+Normally, the calling program (B<login>, B<sshd>, etc.) will run the
+user's shell as a sub-process, wait for it to exit, and then close the PAM
+session, thereby cleaning up the user's session.
+
+=item session
+
+Provides implementations of pam_open_session(), which is equivalent to
+calling pam_setcred() with the PAM_ESTABLISH_CRED flag, and
+pam_close_session(), which destroys the ticket cache created by
+pam_setcred().
+
+=item account
+
+Provides an implementation of pam_acct_mgmt(). All it does is do the same
+authorization check as performed by the pam_authenticate() implementation
+described above.
+
+=item password
+
+Provides an implementation of pam_chauthtok(), which implements password
+changes. The user is prompted for their existing password (unless
+configured to use an already entered one) and the PAM module then obtains
+credentials for the special Kerberos principal C<kadmin/changepw>. It
+then prompts the user for a new password, twice to ensure that the user
+entered it properly (again, unless configured to use an already entered
+password), and then does a Kerberos password change.
+
+Passwords as long or longer than PAM_MAX_RESP_SIZE octets (normally 512
+octets) will be rejected, since excessively long passwords can be used as
+a denial of service attack.
+
+Unlike the normal Unix password module, this module will allow any user to
+change any other user's password if they know the old password. Also,
+unlike the normal Unix password module, root will always be prompted for
+the old password, since root has no special status in Kerberos. (To
+change passwords in Kerberos without knowing the old password, use
+kadmin(8) instead.)
+
+=back
+
+Both the account and session management calls of the Kerberos PAM module
+will return PAM_IGNORE if called in the context of a PAM session for a
+user who did not authenticate with Kerberos (a return code of C<ignore> in
+the Linux PAM configuration language).
+
+Note that this module assumes the network is available in order to do a
+Kerberos authentication. If the network is not available, some Kerberos
+libraries have timeouts longer than the timeout imposed by the login
+process. This means that using this module incautiously can make it
+impossible to log on to console as root. For this reason, you should
+always use the I<ignore_root> or I<minimum_uid> options, list a local
+authentication module such as B<pam_unix> first with a control field of
+C<sufficient> so that the Kerberos PAM module will be skipped if local
+password authentication was successful.
+
+This is not the same PAM module as the Kerberos PAM module available from
+Sourceforge, or the one included on Red Hat systems. It supports many of
+the same options, has some additional options, and doesn't support some of
+the options those modules do.
+
+=head1 CONFIGURATION
+
+The Kerberos PAM module takes many options, not all of which are relevant
+to every PAM group; options that are not relevant will be silently
+ignored. Any of these options can be set in the PAM configuration as
+arguments listed after C<pam_krb5.so>. Some of the options can also be
+set in the system F<krb5.conf> file; if this is possible, it will be noted
+below in the option description.
+
+To set a boolean option in the PAM configuration file, just give the name
+of the option in the arguments. To set an option that takes an argument,
+follow the option name with an equal sign (=) and the value, with no
+separating whitespace. Whitespace in option arguments is not supported in
+the PAM configuration.
+
+To set an option for the PAM module in the system F<krb5.conf> file, put
+that option in the C<[appdefaults]> section. All options must be followed
+by an equal sign (=) and a value, so for boolean options add C<= true>.
+The Kerberos PAM module will look for options either at the top level of
+the C<[appdefaults]> section or in a subsection named C<pam>, inside or
+outside a section for the realm. For example, the following fragment of a
+F<krb5.conf> file would set I<forwardable> to true, I<minimum_uid> to
+1000, and set I<ignore_k5login> only if the realm is EXAMPLE.COM.
+
+ [appdefaults]
+ forwardable = true
+ pam = {
+ minimum_uid = 1000
+ EXAMPLE.COM = {
+ ignore_k5login = true
+ }
+ }
+
+For more information on the syntax of F<krb5.conf>, see krb5.conf(5).
+Note that options that depend on the realm will be set only on the basis
+of the default realm, either as configured in krb5.conf(5) or as set by
+the I<realm> option described below. If the user authenticates to an
+account qualified with a realm, that realm will not be used when
+determining which options will apply.
+
+There is no difference to the PAM module whether options are specified at
+the top level or in a C<pam> section; the C<pam> section is supported in
+case there are options that should be set for the PAM module but not for
+other applications.
+
+If the same option is set in F<krb5.conf> and in the PAM configuration,
+the latter takes precedent. Note, however, that due to the configuration
+syntax, there's no way to turn off a boolean option in the PAM
+configuration that was turned on in F<krb5.conf>.
+
+The start of each option description is annotated with the version of
+pam-krb5 in which that option was added with the current meaning.
+
+=head2 Authorization
+
+=over 4
+
+=item alt_auth_map=<format>
+
+[3.12] This functions similarly to the I<search_k5login> option. The
+<format> argument is used as the authentication Kerberos principal, with
+any C<%s> in <format> replaced with the username. If the username
+contains an C<@>, only the part of the username before the realm is used
+to replace C<%s>. If <format> contains a realm, it will be used;
+otherwise, the realm of the username (if any) will be appended to the
+result. There is no quote removal.
+
+If this option is present, the default behavior is to try this alternate
+principal first and then fall back to the standard behavior if it fails.
+The primary usage is to allow alternative principals to be used for
+authentication in programs like B<sudo>. Most examples will look like:
+
+ alt_auth_map=%s/root
+
+which attempts authentication as the root instance of the username first
+and then falls back to the regular username (but see I<force_alt_auth> and
+I<only_alt_auth>).
+
+This option also allows a cheap way to attempt authentication in an
+alternative realm first and then fall back to the primary realm. A
+setting like:
+
+ alt_auth_map=%s@EXAMPLE.COM
+
+will attempt authentication in the EXAMPLE.COM realm first and then fall
+back on the local default realm. This is more convenient than running the
+module multiple times with multiple default realms set with I<realm>, but
+it is very limited: only two realms can be tried, and the alternate realm
+is always tried first.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf>, although
+normally it doesn't make sense to do that; normally it is used in the PAM
+options of configuration for specific programs. It is only applicable to
+the auth and account groups. If this option is set for the auth group, be
+sure to set it for the account group as well or account authorization may
+fail.
+
+=item force_alt_auth
+
+[3.12] This option is used with I<alt_auth_map> and forces authentication
+as the mapped principal if that principal exists in the KDC. Only if the
+KDC returns principal unknown does the Kerberos PAM module fall back to
+normal authentication. This can be used to force authentication with an
+alternate instance. If I<alt_auth_map> is not set, it has no effect.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item ignore_k5login
+
+[2.0] Never look for a F<.k5login> file in the user's home directory.
+Instead, only check that the Kerberos principal maps to the local account
+name. The default check is to ensure the realm matches the local realm
+and the user portion of the principal matches the local account name, but
+this can be customized by setting up an aname to localname mapping in
+F<krb5.conf>.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and account groups.
+
+=item ignore_root
+
+[1.1] Do not do anything if the username is C<root>. The authentication
+and password calls will silently fail (allowing that status to be ignored
+via a control of C<optional> or C<sufficient>), and the account and
+session calls (including pam_setcred) will return PAM_IGNORE, telling the
+PAM library to proceed as if they weren't mentioned in the PAM
+configuration. This option is supported and will remain, but normally you
+want to use I<minimum_uid> instead.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf>.
+
+=item minimum_uid=<uid>
+
+[2.0] Do not do anything if the authenticated account name corresponds to
+a local account and that local account has a UID lower than <uid>. If
+both of those conditions are true, the authentication and password calls
+will silently fail (allowing that status to be ignored via a control of
+C<optional> or C<sufficient>), and the account and session calls
+(including pam_setcred) will return PAM_IGNORE, telling the PAM library to
+proceed as if they weren't mentioned in the PAM configuration.
+
+Using this option is highly recommended if you don't need to use Kerberos
+to authenticate password logins to the root account (which isn't
+recommended since Kerberos requires a network connection). It provides
+some defense in depth against user principals that happen to match a
+system account incorrectly authenticating as that system account.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf>.
+
+=item only_alt_auth
+
+[3.12] This option is used with I<alt_auth_map> and forces the use of the
+mapped principal for authentication. It disables fallback to normal
+authentication in all cases and overrides I<search_k5login> and
+I<force_alt_auth>. If I<alt_auth_map> is not set, it has no effect and
+the standard authentication behavior is used.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item search_k5login
+
+[2.0] Normally, the Kerberos implementation of pam_authenticate attempts
+to obtain tickets for the authenticating username in the local realm. If
+this option is set and the local user has a F<.k5login> file in their home
+directory, the module will instead open and read that F<.k5login> file,
+attempting to use the supplied password to authenticate as each principal
+listed there in turn. If any of those authentications succeed, the user
+will be successfully authenticated; otherwise, authentication will fail.
+This option is useful for allowing password authentication (via console or
+B<sshd> without GSS-API support) to shared accounts. If there is no
+F<.k5login> file, the behavior is the same as normal. Using this option
+requires that the user's F<.k5login> file be readable at the time of
+authentication.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=back
+
+=head2 Kerberos Behavior
+
+=over 4
+
+=item anon_fast
+
+[4.6] Attempt to use Flexible Authentication Secure Tunneling (FAST) by
+first authenticating as the anonymous user (WELLKNOWN/ANONYMOUS) and using
+its credentials as the FAST armor. This requires anonymous PKINIT be
+enabled for the local realm, that PKINIT be configured on the local
+system, and that the Kerberos library support FAST and anonymous PKINIT.
+
+FAST is a mechanism to protect Kerberos against password guessing attacks
+and provide other security improvements. To work, FAST requires that a
+ticket be obtained with a strong key to protect exchanges with potentially
+weaker user passwords. This option uses anonymous authentication to
+obtain that key and then uses it to protect the subsequent authentication.
+
+If anonymous PKINIT is not available or fails, FAST will not be used and
+the authentication will proceed as normal.
+
+To instead use an existing ticket cache for the FAST credentials, use
+I<fast_ccache> instead of this option. If both I<fast_ccache> and
+I<anon_fast> are set, the ticket cache named by I<fast_ccache> will be
+tried first, and the Kerberos PAM module will fall back on attempting
+anonymous PKINIT if that cache could not be used.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and password groups.
+
+The operation is the same as if using the I<fast_ccache> option, but the
+cache is created and destroyed automatically. If both I<fast_ccache> and
+I<anon_fast> options are used, the I<fast_ccache> takes precedent and no
+anonymous authentication is done.
+
+=item fast_ccache=<ccache_name>
+
+[4.3] The same as I<anon_fast>, but use an existing Kerberos ticket cache
+rather than anonymous PKINIT. This allows use of FAST with a realm that
+doesn't support PKINIT or doesn't support anonymous authentication.
+
+<ccache_name> should be a credential cache containing a ticket obtained
+using a strong key, such as the randomized key for the host principal of
+the local system. If <ccache_name> names a ticket cache that is readable
+by the authenticating process and has tickets then FAST will be attempted.
+The easiest way to use this option is to use a program like B<k5start> to
+maintain a ticket cache using the host's keytab. This ticket cache should
+normally only be readable by root, so this option will not be able to
+protect authentications done as non-root users (such as screensavers).
+
+If no credentials are present in the ticket cache, or if the ticket cache
+does not exist or is not readable, FAST will not used and authentication
+will proceed as normal. However, if the credentials in that ticket cache
+are expired, authentication will fail if the KDC supports FAST.
+
+To use anonymous PKINIT to protect the FAST exchange, use the I<anon_fast>
+option instead. I<anon_fast> is easier to configure, since no existing
+ticket cache is required, but requires PKINIT be available and configured
+and that the local realm support anonymous authentication. If both
+I<fast_ccache> and I<anon_fast> are set, the ticket cache named by
+I<fast_ccache> will be tried first, and the Kerberos PAM module will fall
+back on attempting anonymous PKINIT if that cache could not be used.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and password groups.
+
+=item forwardable
+
+[1.0] Obtain forwardable tickets. If set (to either true or false,
+although it can only be set to false in F<krb5.conf>), this overrides the
+Kerberos library default set in the [libdefaults] section of F<krb5.conf>.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item keytab=<path>
+
+[3.0] Specifies the keytab to use when validating the user's credentials.
+The default is the default system keytab (normally F</etc/krb5.keytab>),
+which is usually only readable by root. Applications not running as root
+that use this PAM module for authentication may wish to point it to
+another keytab the application can read. The first principal found in the
+keytab will be used as the principal for credential verification.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item realm=<realm>
+
+[2.2] Set the default Kerberos realm and obtain credentials in that realm,
+rather than in the normal default realm for this system. If this option
+is used, it should be set for all groups being used for consistent
+results. This setting will affect authorization decisions since it
+changes the default realm. This setting will also change the service
+principal used to verify the obtained credentials to be in the specified
+realm.
+
+If you only want to set the realm assumed for user principals without
+changing the realm for authorization decisions or the service principal
+used to verify credentials, see the I<user_realm> option.
+
+=item renew_lifetime=<lifetime>
+
+[2.0] Obtain renewable tickets with a maximum renewable lifetime of
+<lifetime>. <lifetime> should be a Kerberos lifetime string such as
+C<2d4h10m> or a time in minutes. If set, this overrides the Kerberos
+library default set in the [libdefaults] section of F<krb5.conf>.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item ticket_lifetime=<lifetime>
+
+[3.0] Obtain tickets with a maximum lifetime of <lifetime>. <lifetime>
+should be a Kerberos lifetime string such as C<2d4h10m> or a time in
+minutes. If set, this overrides the Kerberos library default set in the
+[libdefaults] section of F<krb5.conf>.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item user_realm
+
+[4.6] Obtain credentials in the specified realm rather than in the default
+realm for this system. If this option is used, it should be set for all
+groups being used for consistent results (although the account group
+currently doesn't care about realm). This will not change authorization
+decisions. If the obtained credentials are supposed to allow access to a
+shell account, the user will need an appropriate F<.k5login> file entry or
+the system will have to have a custom aname_to_localname mapping.
+
+=back
+
+=head2 PAM Behavior
+
+=over 4
+
+=item clear_on_fail
+
+[3.9] When changing passwords, PAM first does a preliminary check through
+the complete password stack, and then calls each module again to do the
+password change. After that preliminary check, the order of module
+invocation is fixed. This means that even if the Kerberos password change
+fails (or if one of the other password changes in the stack fails), other
+password PAM modules in the stack will still be called even if the failing
+module is marked required or requisite. When using multiple password PAM
+modules to synchronize passwords between multiple systems when they
+change, this behavior can cause unwanted differences between the
+environments.
+
+Setting this option provides a way to work around this behavior. If this
+option is set and a Kerberos password change is attempted and fails (due
+to network errors or password strength checking on the KDC, for example),
+this module will clear the stored password in the PAM stack. This will
+force any subsequent modules that have I<use_authtok> set to fail so that
+those environments won't get out of sync with the password in Kerberos.
+The Kerberos PAM module will not meddle with the stored password if it
+skips the user due to configuration such as minimum_uid.
+
+Unfortunately, setting this option interferes with other desirable PAM
+configurations, such as attempting to change the password in Kerberos
+first and falling back on the local Unix password database if that fails.
+It therefore isn't the default. Turn it on (and list pam_krb5 first after
+pam_cracklib if used) when synchronizing passwords between multiple
+environments.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the password group.
+
+=item debug
+
+[1.0] Log more verbose trace and debugging information to syslog at
+LOG_DEBUG priority, including entry and exit from each of the external PAM
+interfaces (except pam_close_session).
+
+This option can be set in C<[appdefaults]> in F<krb5.conf>.
+
+=item defer_pwchange
+
+[3.11] By default, pam-krb5 lets the Kerberos library handle prompting for
+a password change if an account's password is expired during the auth
+group. If this fails, pam_authenticate() returns an error.
+
+According to the PAM standard, this is not the correct way to handle
+expired passwords. Instead, pam_authenticate() should return success
+without attempting a password change, and then pam_acct_mgmt() should
+return PAM_NEW_AUTHTOK_REQD, at which point the calling application is
+responsible for either rejecting the authentication or calling
+pam_chauthtok(). However, following the standard requires that all
+applications call pam_acct_mgmt() and check its return status; otherwise,
+expired accounts may be able to successfully authenticate. Many
+applications do not do this.
+
+If this option is set, pam-krb5 uses the fully correct PAM mechanism for
+handling expired accounts instead of failing in pam_authenticate(). Due
+to the security risk of widespread broken applications, be very careful
+about enabling this option. It should normally only be turned on to solve
+a specific problem (such as using Solaris Kerberos libraries that don't
+support prompting for password changes during authentication), and then
+only for specific applications known to call pam_acct_mgmt() and check its
+return status properly.
+
+This option is only supported when pam-krb5 is built with MIT Kerberos.
+If built against Heimdal, this option does nothing and normal expired
+password change handling still happens. (Heimdal is missing the required
+API to implement this option, at least as of version 1.6.)
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item fail_pwchange
+
+[4.2] By default, pam-krb5 lets the Kerberos library handle prompting for
+a password change if an account's password is expired during the auth
+group. If this option is set, expired passwords are instead treated as an
+authentication failure identical to an incorrect password. Also see
+I<defer_pwchange> and I<force_pwchange>.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item force_pwchange
+
+[3.11] If this option is set and authentication fails with a Kerberos
+error indicating the user's password is expired, attempt to immediately
+change their password during the authenticate step. Under normal
+circumstances, this is unnecessary. Most Kerberos libraries will do this
+for you, and setting this option will prompt the user twice to change
+their password if the first attempt (done by the Kerberos library) fails.
+However, some system Kerberos libraries (such as Solaris's) have password
+change prompting disabled in the Kerberos library; on those systems, you
+can set this option to simulate the normal library behavior.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item no_update_user
+
+[4.7] Normally, if pam-krb5 is able to canonicalize the principal to a
+local name using krb5_aname_to_localname() or similar calls, it changes
+the PAM_USER variable for this PAM session to the canonicalized local
+name. Setting this option disables this behavior and leaves PAM_USER set
+to the initial authentication identity.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth group.
+
+=item silent
+
+[1.0] Don't show messages and errors from Kerberos, such as warnings of
+expiring passwords, to the user via the prompter. This is equivalent to
+the behavior when the application passes in PAM_SILENT, but can be set in
+the PAM configuration.
+
+This option is only applicable to the auth and password groups.
+
+=item trace=<log-file>
+
+[4.6] Enables Kerberos library trace logging to the specified log file if
+it is supported by the Kerberos library. This is intended for temporary
+debugging. The specified file will be appended to without further
+security checks, so do not specify a file in a publicly writable directory
+like F</tmp>.
+
+=back
+
+=head2 PKINIT
+
+=over 4
+
+=item pkinit_anchors=<anchors>
+
+[3.0] When doing PKINIT authentication, use <anchors> as the client trust
+anchors. This is normally a reference to a file containing the trusted
+certificate authorities. This option is only used if I<try_pkinit> or
+I<use_pkinit> are set.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and password groups.
+
+=item pkinit_prompt
+
+[3.0] Before attempting PKINIT authentication, prompt the user to insert a
+smart card. You may want to set this option for programs such as
+B<gnome-screensaver> that call PAM as soon as the mouse is touched and
+don't give the user an opportunity to enter the smart card first. Any
+information entered at the first prompt is ignored. If I<try_pkinit> is
+set, a user who wishes to use a password instead can just press Enter and
+then enter their password as normal. This option is only used if
+I<try_pkinit> or I<use_pkinit> are set.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and password groups.
+
+=item pkinit_user=<userid>
+
+[3.0] When doing PKINIT authentication, use <userid> as the user ID. The
+value of this string is highly dependent on the type of PKINIT
+implementation you're using, but will generally be something like:
+
+ PKCS11:/usr/lib/pkcs11/lib/soft-pkcs11.so
+
+to specify the module to use with a smart card. It may also point to a
+user certificate or to other types of user IDs. See the Kerberos library
+documentation for more details. This option is only used if I<try_pkinit>
+or I<use_pkinit> are set.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and password groups.
+
+=item preauth_opt=<option>
+
+[3.3] Sets a preauth option (currently only applicable when built with MIT
+Kerberos). <option> is either a key/value pair with the key separated
+from the value by C<=> or a boolean option (in which case it's turned on).
+In F<krb5.conf>, multiple options should be separated by whitespace. In
+the PAM configuration, this option can be given multiple times to set
+multiple options. In either case, <option> may not contain whitespace.
+
+The primary use of this option, at least in the near future, will be to
+set options for the MIT Kerberos PKINIT support. For the full list of
+possible options, see the PKINIT plugin documentation. At the time of
+this writing, C<X509_user_identity> is equivalent to I<pkinit_user> and
+C<X509_anchors> is equivalent to I<pkinit_anchors>. C<flag_DSA_PROTOCOL>
+can only be set via this option.
+
+Any settings made with this option are applied after the I<pkinit_anchors>
+and I<pkinit_user> options, so if an equivalent setting is made via
+I<preauth_opt>, it will probably override the other setting.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and password groups. Note that there is no way to
+remove a setting made in F<krb5.conf> using the PAM configuration, but
+options set in the PAM configuration are applied after options set in
+F<krb5.conf> and therefore may override earlier settings.
+
+=item try_pkinit
+
+[3.0] Attempt PKINIT authentication before trying a regular password. You
+will probably also need to set the I<pkinit_user> configuration option.
+If PKINIT fails, the PAM module will fall back on regular password
+authentication. This option is currently only supported if pam-krb5 was
+built against Heimdal 0.8rc1 or later or MIT Kerberos 1.6.3 or later.
+
+If this option is set and pam-krb5 is built against MIT Kerberos, and
+PKINIT fails and the module falls back to password authentication, the
+user's password will not be stored in the PAM stack for subsequent
+modules. This is a bug in the interaction between the module and MIT
+Kerberos that requires some reworking of the PKINIT authentication method
+to fix.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and password groups.
+
+=item use_pkinit
+
+[3.0, 4.9 for MIT Kerberos] Require PKINIT authentication. You will
+probably also need to set the I<pkinit_user> configuration option. If
+PKINIT fails, authentication will fail. This option is only supported if
+pam-krb5 was built against Heimdal 0.8rc1 or later or MIT Kerberos 1.12 or
+later.
+
+Be aware that, with MIT Kerberos, this option is implemented by using a
+responder without a prompter, and thus any informational messages from the
+Kerberos libraries or KDC during authentication will not be displayed.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and password groups.
+
+=back
+
+=head2 Prompting
+
+=over 4
+
+=item banner=<banner>
+
+[3.0] By default, the prompts when a user changes their password are:
+
+ Current Kerberos password:
+ Enter new Kerberos password:
+ Retype new Kerberos password:
+
+The string "Kerberos" is inserted so that users aren't confused about
+which password they're changing. Setting this option replaces the word
+"Kerberos" with whatever this option is set to. Setting this option to
+the empty string removes the word before "password:" entirely.
+
+If set in the PAM configuration, <banner> may not contain whitespace. If
+you want a value containing whitespace, set it in F<krb5.conf>.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the password group.
+
+=item expose_account
+
+[3.0] By default, the Kerberos PAM module password prompt is simply
+"Password:". This avoids leaking any information about the system realm
+or account to principal conversions. If this option is set, the string
+"for <principal>" is added before the colon, where <principal> is the
+user's principal. This string is also added before the colon on prompts
+when changing the user's password.
+
+Enabling this option with ChallengeResponseAuthentication enabled in
+OpenSSH may cause problems for some ssh clients that only recognize
+"Password:" as a prompt. This option is automatically disabled if
+I<search_k5login> is enabled since the principal displayed would be
+inaccurate.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and password groups.
+
+=item force_first_pass
+
+[4.0] Use the password obtained by a previous authentication or password
+module to authenticate the user without prompting the user again. If no
+previous module obtained the user's password, fail without prompting the
+user. Also see I<try_first_pass> and I<use_first_pass> for weaker
+versions of this option.
+
+This option is only applicable to the auth and password groups. For the
+password group, it applies only to the old password. See I<use_authtok>
+for a similar setting for the new password.
+
+=item no_prompt
+
+[4.6] Never prompt for the current password. Instead, pass in a NULL
+password to the Kerberos library and let the Kerberos library do the
+prompting. This may be needed if, for example, the Kerberos library is
+configured to use other authentication mechanisms than passwords and needs
+full control over the prompting process.
+
+The major disadvantage of this option is that it means the PAM module will
+never see the user's password and therefore cannot save it in the PAM
+module data for any subsequent modules. In other words, this option
+cannot be used if another module is in the stack behind the Kerberos PAM
+module and wants to use I<use_first_pass>. The Kerberos library also
+usually includes the principal in the prompt, and therefore this option
+implies behavior similar to I<expose_account>. Similar to
+I<expose_account>, this can cause problems with OpenSSH if
+ChallengeResponseAuthentication is enabled, since clients may not
+recognize password prompts other than "Password:".
+
+Using this option with I<search_k5login> would result in a password prompt
+for every principal listed in the user's F<.k5login> file. This is
+probably not desired behavior, although it's not prohibited by the module.
+
+This option is only applicable to the auth and password groups. For the
+password group, it applies only to the authentication process; the user
+will still be prompted for a new password.
+
+=item prompt_principal
+
+[3.6] Before prompting for the user's password (or using the previously
+entered password, if I<try_first_pass>, I<use_first_pass>, or
+I<force_first_pass> are set), prompt the user for the Kerberos principal
+to use for authentication. This allows the user to authenticate with a
+different principal than the one corresponding to the local username,
+provided that either a F<.k5login> file or local Kerberos principal to
+account mapping authorize that principal to access the local account.
+
+Be cautious when using this configuration option and don't use it with
+OpenSSH PasswordAuthentication, only ChallengeResponseAuthentication.
+Some PAM-enabled applications expect PAM modules to only prompt for
+passwords and may even blindly give the password to the first prompt, no
+matter what it is. Such applications, in combination with this option,
+may expose the user's password in log messages and Kerberos requests.
+
+=item try_first_pass
+
+[1.0] If the authentication module isn't the first on the stack, and a
+previous module obtained the user's password, use that password to
+authenticate the user without prompting them again. If that
+authentication fails, fall back on prompting the user for their password.
+This option has no effect if the authentication module is first in the
+stack or if no previous module obtained the user's password. Also see
+I<use_first_pass> and I<force_first_pass> for stronger versions of this
+option.
+
+This option is only applicable to the auth and password groups. For the
+password group, it applies only to the old password.
+
+=item use_authtok
+
+[4.0] Use the new password obtained by a previous password module when
+changing passwords rather than prompting for the new password. If the new
+password isn't available, fail. This can be used to require passwords be
+checked by another, prior module, such as B<pam_cracklib>.
+
+This option is only applicable to the password group.
+
+=item use_first_pass
+
+[1.0] Use the password obtained by a previous authentication module to
+authenticate the user without prompting the user again. If no previous
+module obtained the user's password for either an authentication or
+password change, fall back on prompting the user. If a previous module
+did obtain the user's password but authentication with that password
+fails, fail without further prompting the user. Also see
+I<try_first_pass> and I<force_first_pass> for other versions of this
+option.
+
+This option is only applicable to the auth and password groups. For the
+password group, it applies only to the old password. See I<use_authtok>
+for a similar setting for the new password.
+
+=back
+
+=head2 Ticket Caches
+
+=over 4
+
+=item ccache=<pattern>
+
+[2.0] Use <pattern> as the pattern for creating credential cache names.
+<pattern> must be in the form <type>:<residual> where <type> and the
+following colon are optional if a file cache should be used. The special
+token C<%u>, anywhere in <pattern>, is replaced with the user's numeric
+UID. The special token C<%p>, anywhere in <pattern>, is replaced with the
+current process ID.
+
+If <pattern> ends in the literal string C<XXXXXX> (six X's), that string
+will be replaced by randomly generated characters and the ticket cache
+will be created using mkstemp(3). This is strongly recommended if
+<pattern> points to a world-writable directory.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and session groups.
+
+=item ccache_dir=<directory>
+
+[1.2] Store both the temporary ticket cache used during authentication and
+user ticket caches in <directory> instead of in F</tmp>. The algorithm
+for generating the ticket cache name is otherwise unchanged. <directory>
+may be prefixed with C<FILE:> to make the cache type unambiguous (and this
+may be required on systems that use a cache type other than file as the
+default).
+
+Be aware that pam_krb5 creates and stores a temporary ticket cache file
+owned by root during the login process. If you set I<ccache> above to
+avoid using the system F</tmp> directory for user ticket caches, you may
+also want to set I<ccache_dir> to move those temporary caches to some
+other location. This will allow pam_krb5 to continue working even if the
+system F</tmp> directory is full.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and session groups.
+
+=item no_ccache
+
+[1.0] Do not create a ticket cache after authentication. This option
+shouldn't be set in general, but is useful as part of the PAM
+configuration for a particular service that uses PAM for authentication
+but isn't creating user sessions and doesn't want the overhead of ever
+writing the user credentials to disk. When using this option, the
+application should only call pam_authenticate(); other functions like
+pam_setcred(), pam_start_session(), and pam_acct_mgmt() don't make sense
+with this option. Don't use this option if the application needs PAM
+account and session management calls.
+
+This option is only applicable to the auth group.
+
+=item retain_after_close
+
+[2.3] Normally, the user's ticket cache is destroyed when either pam_end()
+or pam_close_session() is called by the authenticating application so that
+ticket caches aren't left behind after the user logs out. In some cases,
+however, this isn't desirable. (On Solaris 8, for instance, the default
+behavior means login will destroy the ticket cache before running the
+user's shell.) If this option is set, the PAM module will never destroy
+the user's ticket cache. If you set this, you may want to call
+B<kdestroy> in the shell's logout configuration or run a temporary file
+removal program to avoid accumulating hundreds of ticket caches in
+F</tmp>.
+
+This option can be set in C<[appdefaults]> in F<krb5.conf> and is only
+applicable to the auth and session groups.
+
+=back
+
+=head1 ENVIRONMENT
+
+=over 4
+
+=item KRB5CCNAME
+
+Set by pam_setcred() with the PAM_ESTABLISH_CRED option, and therefore
+also by pam_open_session(), to point to the new credential cache for the
+user. See the I<ccache> and I<ccache_dir> options. By default, the cache
+name will be prefixed with C<FILE:> to make the cache type unambiguous.
+
+=item PAM_KRB5CCNAME
+
+Set by pam_authenticate() to point to the temporary ticket cache used for
+authentication (unless the I<no_ccache> option was given). pam_setcred()
+then uses that environment variable to locate the temporary cache even if
+it was not called in the same PAM session as pam_authenticate() (a problem
+with B<sshd> running in some modes). This environment variable is only
+used internal to the PAM module.
+
+=back
+
+=head1 FILES
+
+=over 4
+
+=item F</tmp/krb5cc_UID_RANDOM>
+
+The default credential cache name. UID is the decimal UID of the local
+user and RANDOM is a random six-character string. The pattern may be
+changed with the I<ccache> option and the directory with the I<ccache_dir>
+option.
+
+=item F</tmp/krb5cc_pam_RANDOM>
+
+The credential cache name used for the temporary credential cache created
+by pam_authenticate(). This cache is removed again when the PAM session
+is ended or when pam_setcred() is called and will normally not be
+user-visible. RANDOM is a random six-character string.
+
+=item F<~/.k5login>
+
+File containing Kerberos principals that are allowed access to that
+account.
+
+=back
+
+=head1 BUGS
+
+If I<try_pkinit> is set and pam-krb5 is built with MIT Kerberos, the
+user's password is not saved in the PAM data if PKINIT fails and the
+module falls back to password authentication.
+
+=head1 CAVEATS
+
+Be sure to list this module in the session group as well as the auth group
+when using it for interactive logins. Otherwise, some applications (such
+as OpenSSH) will not set up the user's ticket cache correctly.
+
+The Kerberos library, via pam-krb5, will prompt the user to change their
+password if their password is expired, but when using OpenSSH, this will
+only work when ChallengeResponseAuthentication is enabled. Unless this
+option is enabled, OpenSSH doesn't pass PAM messages to the user and can
+only respond to a simple password prompt.
+
+If you are using MIT Kerberos, be aware that users whose passwords are
+expired will not be prompted to change their password unless the KDC
+configuration for your realm in [realms] in krb5.conf contains a
+master_kdc setting or, if using DNS SRV records, you have a DNS entry for
+_kerberos-master as well as _kerberos.
+
+pam_authenticate() returns failure when called for an ignored account,
+requiring the system administrator to use C<optional> or C<sufficient> to
+ignore the module and move on to the next module. It's arguably more
+correct to return PAM_IGNORE, which causes the module to be ignored as if
+it weren't in the configuration, but this increases the risk of
+inadvertent security holes when listing pam-krb5 as the only
+authentication module.
+
+This module treats the empty password as an authentication failure
+rather than attempting to use that password to avoid unwanted prompting
+behavior in the Kerberos libraries. If you have a Kerberos principal that
+intentionally has an empty password, it won't work with this module.
+
+This module will not refresh an existing ticket cache if called with an
+effective UID or GID different than the real UID or GID, since refreshing
+an existing ticket cache requires trusting the KRB5CCNAME environment
+variable and the environment should not be trusted in a setuid context.
+
+Old versions of OpenSSH are known to call pam_authenticate followed by
+pam_setcred(PAM_REINITIALIZE_CRED) without first calling pam_open_session,
+thereby requesting that an existing ticket cache be renewed (similar to
+what a screensaver would want) rather than requesting a new ticket cache
+be created. Since this behavior is indistinguishable at the PAM level
+from a screensaver, pam-krb5 when used with these old versions of OpenSSH
+will refresh the ticket cache of the OpenSSH daemon rather than setting up
+a new ticket cache for the user. The resulting ticket cache will have the
+correct permissions, but will not be named correctly or referenced in the
+user's environment and will be overwritten by the next user login. The
+best solution to this problem is to upgrade OpenSSH. I'm not sure exactly
+when this problem was fixed, but at the very least OpenSSH 4.3 and later
+do not exhibit it.
+
+=head1 AUTHOR
+
+pam-krb5 was originally written by Frank Cusack. Andres Salomon made
+extensive modifications, and then Russ Allbery <eagle@eyrie.org> adopted
+it and made even more extensive modifications. Russ Allbery currently
+maintains the module.
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2005-2010, 2014, 2020 Russ Allbery <eagle@eyrie.org>
+
+Copyright 2008-2014 The Board of Trustees of the Leland Stanford Junior
+University
+
+Copying and distribution of this file, with or without modification, are
+permitted in any medium without royalty provided the copyright notice and
+this notice are preserved. This file is offered as-is, without any
+warranty.
+
+SPDX-License-Identifier: FSFAP
+
+=head1 SEE ALSO
+
+kadmin(8), kdestroy(1), krb5.conf(5), pam(7), passwd(1), syslog(3)
+
+The current version of this module is available from its web page at
+L<https://www.eyrie.org/~eagle/software/pam-krb5/>.
+
+=cut
diff --git a/m4/cc-flags.m4 b/m4/cc-flags.m4
new file mode 100644
index 000000000000..99fcdec6001b
--- /dev/null
+++ b/m4/cc-flags.m4
@@ -0,0 +1,131 @@
+dnl Check whether the compiler supports particular flags.
+dnl
+dnl Provides RRA_PROG_CC_FLAG and RRA_PROG_LD_FLAG, which checks whether a
+dnl compiler supports a given flag for either compilation or linking,
+dnl respectively. If it does, the commands in the second argument are run.
+dnl If not, the commands in the third argument are run.
+dnl
+dnl Provides RRA_PROG_CC_WARNINGS_FLAGS, which checks whether a compiler
+dnl supports a large set of warning flags and sets the WARNINGS_CFLAGS
+dnl substitution variable to all of the supported warning flags. (Note that
+dnl this may be too aggressive for some people.)
+dnl
+dnl Depends on RRA_PROG_CC_CLANG.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Copyright 2016-2021 Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2006, 2009, 2016
+dnl by Internet Systems Consortium, Inc. ("ISC")
+dnl
+dnl Permission to use, copy, modify, and/or distribute this software for any
+dnl purpose with or without fee is hereby granted, provided that the above
+dnl copyright notice and this permission notice appear in all copies.
+dnl
+dnl THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+dnl REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+dnl MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY
+dnl SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+dnl WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+dnl ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+dnl IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+dnl
+dnl SPDX-License-Identifier: ISC
+
+dnl Used to build the result cache name.
+AC_DEFUN([_RRA_PROG_CC_FLAG_CACHE],
+[translit([rra_cv_compiler_c_$1], [-=+,], [____])])
+AC_DEFUN([_RRA_PROG_LD_FLAG_CACHE],
+[translit([rra_cv_linker_c_$1], [-=+,], [____])])
+
+dnl Check whether a given flag is supported by the compiler when compiling a C
+dnl source file.
+AC_DEFUN([RRA_PROG_CC_FLAG],
+[AC_REQUIRE([AC_PROG_CC])
+ AC_MSG_CHECKING([if $CC supports $1])
+ AC_CACHE_VAL([_RRA_PROG_CC_FLAG_CACHE([$1])],
+ [save_CFLAGS=$CFLAGS
+ AS_CASE([$1],
+ [-Wno-*], [CFLAGS="$CFLAGS `AS_ECHO(["$1"]) | sed 's/-Wno-/-W/'`"],
+ [*], [CFLAGS="$CFLAGS $1"])
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [int foo = 0;])],
+ [_RRA_PROG_CC_FLAG_CACHE([$1])=yes],
+ [_RRA_PROG_CC_FLAG_CACHE([$1])=no])
+ CFLAGS=$save_CFLAGS])
+ AC_MSG_RESULT([$_RRA_PROG_CC_FLAG_CACHE([$1])])
+ AS_IF([test x"$_RRA_PROG_CC_FLAG_CACHE([$1])" = xyes], [$2], [$3])])
+
+dnl Check whether a given flag is supported by the compiler when linking an
+dnl executable.
+AC_DEFUN([RRA_PROG_LD_FLAG],
+[AC_REQUIRE([AC_PROG_CC])
+ AC_MSG_CHECKING([if $CC supports $1 for linking])
+ AC_CACHE_VAL([_RRA_PROG_LD_FLAG_CACHE([$1])],
+ [save_LDFLAGS=$LDFLAGS
+ LDFLAGS="$LDFLAGS $1"
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([], [int foo = 0;])],
+ [_RRA_PROG_LD_FLAG_CACHE([$1])=yes],
+ [_RRA_PROG_LD_FLAG_CACHE([$1])=no])
+ LDFLAGS=$save_LDFLAGS])
+ AC_MSG_RESULT([$_RRA_PROG_LD_FLAG_CACHE([$1])])
+ AS_IF([test x"$_RRA_PROG_LD_FLAG_CACHE([$1])" = xyes], [$2], [$3])])
+
+dnl Determine the full set of viable warning flags for the current compiler.
+dnl
+dnl This is based partly on personal preference and is a fairly aggressive set
+dnl of warnings. Desirable CC warnings that can't be turned on due to other
+dnl problems:
+dnl
+dnl -Wsign-conversion Too many fiddly changes for the benefit
+dnl -Wstack-protector Too many false positives from small buffers
+dnl
+dnl Last checked against gcc 9.2.1 (2019-09-01). -D_FORTIFY_SOURCE=2 enables
+dnl warn_unused_result attribute markings on glibc functions on Linux, which
+dnl catches a few more issues. Add -O2 because gcc won't find some warnings
+dnl without optimization turned on.
+dnl
+dnl For Clang, we try to use -Weverything, but we have to disable some of the
+dnl warnings:
+dnl
+dnl -Wcast-qual Some structs require casting away const
+dnl -Wdisabled-macro-expansion Triggers on libc (sigaction.sa_handler)
+dnl -Wpadded Not an actual problem
+dnl -Wreserved-id-macros Autoconf sets several of these normally
+dnl -Wsign-conversion Too many fiddly changes for the benefit
+dnl -Wtautological-pointer-compare False positives with for loops
+dnl -Wundef Conflicts with Autoconf probe results
+dnl -Wunreachable-code Happens with optional compilation
+dnl -Wunreachable-code-return Other compilers get confused
+dnl -Wunused-macros Often used on suppressed branches
+dnl -Wused-but-marked-unused Happens a lot with conditional code
+dnl
+dnl Sets WARNINGS_CFLAGS as a substitution variable.
+AC_DEFUN([RRA_PROG_CC_WARNINGS_FLAGS],
+[AC_REQUIRE([RRA_PROG_CC_CLANG])
+ AS_IF([test x"$CLANG" = xyes],
+ [WARNINGS_CFLAGS="-Werror"
+ m4_foreach_w([flag],
+ [-Weverything -Wno-cast-qual -Wno-disabled-macro-expansion -Wno-padded
+ -Wno-sign-conversion -Wno-reserved-id-macro
+ -Wno-tautological-pointer-compare -Wno-undef -Wno-unreachable-code
+ -Wno-unreachable-code-return -Wno-unused-macros
+ -Wno-used-but-marked-unused],
+ [RRA_PROG_CC_FLAG(flag,
+ [WARNINGS_CFLAGS="${WARNINGS_CFLAGS} flag"])])],
+ [WARNINGS_CFLAGS="-g -O2 -D_FORTIFY_SOURCE=2 -Werror"
+ m4_foreach_w([flag],
+ [-fstrict-overflow -fstrict-aliasing -Wall -Wextra -Wformat=2
+ -Wformat-overflow=2 -Wformat-signedness -Wformat-truncation=2
+ -Wnull-dereference -Winit-self -Wswitch-enum -Wstrict-overflow=5
+ -Wmissing-format-attribute -Walloc-zero -Wduplicated-branches
+ -Wduplicated-cond -Wtrampolines -Wfloat-equal
+ -Wdeclaration-after-statement -Wshadow -Wpointer-arith
+ -Wbad-function-cast -Wcast-align -Wwrite-strings -Wconversion
+ -Wno-sign-conversion -Wdate-time -Wjump-misses-init -Wlogical-op
+ -Wstrict-prototypes -Wold-style-definition -Wmissing-prototypes
+ -Wmissing-declarations -Wnormalized=nfc -Wpacked -Wredundant-decls
+ -Wrestrict -Wnested-externs -Winline -Wvla],
+ [RRA_PROG_CC_FLAG(flag,
+ [WARNINGS_CFLAGS="${WARNINGS_CFLAGS} flag"])])])
+ AC_SUBST([WARNINGS_CFLAGS])])
diff --git a/m4/clang.m4 b/m4/clang.m4
new file mode 100644
index 000000000000..c1815a5702c2
--- /dev/null
+++ b/m4/clang.m4
@@ -0,0 +1,28 @@
+dnl Determine whether the current compiler is Clang.
+dnl
+dnl If the current compiler is Clang, set the shell variable CLANG to yes.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Copyright 2015 Russ Allbery <eagle@eyrie.org>
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+dnl Source used by RRA_PROG_CC_CLANG.
+AC_DEFUN([_RRA_PROG_CC_CLANG_SOURCE], [[
+#if ! __clang__
+#error
+#endif
+]])
+
+AC_DEFUN([RRA_PROG_CC_CLANG],
+[AC_CACHE_CHECK([if the compiler is Clang], [rra_cv_prog_cc_clang],
+ [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_RRA_PROG_CC_CLANG_SOURCE])],
+ [rra_cv_prog_cc_clang=yes],
+ [rra_cv_prog_cc_clang=no])])
+ AS_IF([test x"$rra_cv_prog_cc_clang" = xyes], [CLANG=yes])])
diff --git a/m4/kadm5clnt.m4 b/m4/kadm5clnt.m4
new file mode 100644
index 000000000000..b52a32e04ed7
--- /dev/null
+++ b/m4/kadm5clnt.m4
@@ -0,0 +1,103 @@
+dnl Find the compiler and linker flags for the kadmin client library.
+dnl
+dnl Finds the compiler and linker flags for linking with the kadmin client
+dnl library. Provides the --with-kadm5clnt, --with-kadm5clnt-include, and
+dnl --with-kadm5clnt-lib configure option to specify a non-standard path to
+dnl the library. Uses krb5-config where available unless reduced dependencies
+dnl is requested or --with-kadm5clnt-include or --with-kadm5clnt-lib are
+dnl given.
+dnl
+dnl Provides the macros RRA_LIB_KADM5CLNT and RRA_LIB_KADM5CLNT_OPTIONAL and
+dnl sets the substitution variables KADM5CLNT_CPPFLAGS, KADM5CLNT_LDFLAGS, and
+dnl KADM5CLNT_LIBS. Also provides RRA_LIB_KADM5CLNT_SWITCH to set CPPFLAGS,
+dnl LDFLAGS, and LIBS to include the kadmin client libraries, saving the
+dnl ecurrent values, and RRA_LIB_KADM5CLNT_RESTORE to restore those settings
+dnl to before the last RRA_LIB_KADM5CLNT_SWITCH. Defines HAVE_KADM5CLNT and
+dnl sets rra_use_KADM5CLNT to true if the library is found.
+dnl
+dnl Depends on the RRA_LIB helper routines.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Written by Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2005-2009, 2011, 2013
+dnl The Board of Trustees of the Leland Stanford Junior University
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+dnl Save the current CPPFLAGS, LDFLAGS, and LIBS settings and switch to
+dnl versions that include the kadmin client flags. Used as a wrapper, with
+dnl RRA_LIB_KADM5CLNT_RESTORE, around tests.
+AC_DEFUN([RRA_LIB_KADM5CLNT_SWITCH], [RRA_LIB_HELPER_SWITCH([KADM5CLNT])])
+
+dnl Restore CPPFLAGS, LDFLAGS, and LIBS to their previous values (before
+dnl RRA_LIB_KADM5CLNT_SWITCH was called).
+AC_DEFUN([RRA_LIB_KADM5CLNT_RESTORE], [RRA_LIB_HELPER_RESTORE([KADM5CLNT])])
+
+dnl Set KADM5CLNT_CPPFLAGS and KADM5CLNT_LDFLAGS based on rra_KADM5CLNT_root,
+dnl rra_KADM5CLNT_libdir, and rra_KADM5CLNT_includedir.
+AC_DEFUN([_RRA_LIB_KADM5CLNT_PATHS], [RRA_LIB_HELPER_PATHS([KADM5CLNT])])
+
+dnl Does the appropriate library checks for reduced-dependency kadmin client
+dnl linkage. The single argument, if "true", says to fail if the kadmin
+dnl client library could not be found.
+AC_DEFUN([_RRA_LIB_KADM5CLNT_REDUCED],
+[RRA_LIB_KADM5CLNT_SWITCH
+ AC_CHECK_LIB([kadm5clnt], [kadm5_init_with_password],
+ [KADM5CLNT_LIBS=-lkadm5clnt],
+ [AS_IF([test x"$1" = xtrue],
+ [AC_MSG_ERROR([cannot find usable kadmin client library])])])
+ RRA_LIB_KADM5CLNT_RESTORE])
+
+dnl Sanity-check the results of krb5-config and be sure we can really link a
+dnl GSS-API program. If not, fall back on the manual check.
+AC_DEFUN([_RRA_LIB_KADM5CLNT_CHECK],
+[RRA_LIB_HELPER_CHECK([$1], [KADM5CLNT], [kadm5_init_with_password],
+ [kadmin client])])
+
+dnl Determine GSS-API compiler and linker flags from krb5-config.
+AC_DEFUN([_RRA_LIB_KADM5CLNT_CONFIG],
+[RRA_KRB5_CONFIG([${rra_KADM5CLNT_root}], [kadm-client], [KADM5CLNT],
+ [_RRA_LIB_KADM5CLNT_CHECK([$1])],
+ [_RRA_LIB_KADM5CLNT_PATHS
+ _RRA_LIB_KADM5CLNT_REDUCED([$1])])])
+
+dnl The core of the library checking, shared between RRA_LIB_KADM5CLNT and
+dnl RRA_LIB_KADM5CLNT_OPTIONAL. The single argument, if "true", says to fail
+dnl if the kadmin client library could not be found.
+AC_DEFUN([_RRA_LIB_KADM5CLNT_INTERNAL],
+[AC_REQUIRE([RRA_ENABLE_REDUCED_DEPENDS])
+ AS_IF([test x"$rra_reduced_depends" = xtrue],
+ [_RRA_LIB_KADM5CLNT_PATHS
+ _RRA_LIB_KADM5CLNT_REDUCED([$1])],
+ [AS_IF([test x"$rra_KADM5CLNT_includedir" = x \
+ && test x"$rra_KADM5CLNT_libdir" = x],
+ [_RRA_LIB_KADM5CLNT_CONFIG([$1])],
+ [_RRA_LIB_KADM5CLNT_PATHS
+ _RRA_LIB_KADM5CLNT_REDUCED([$1])])])])
+
+dnl The main macro for packages with mandatory kadmin client support.
+AC_DEFUN([RRA_LIB_KADM5CLNT],
+[RRA_LIB_HELPER_VAR_INIT([KADM5CLNT])
+ RRA_LIB_HELPER_WITH([kadm-client], [kadmin client], [KADM5CLNT])
+ _RRA_LIB_KADM5CLNT_INTERNAL([true])
+ rra_use_KADM5CLNT=true
+ AC_DEFINE([HAVE_KADM5CLNT], 1, [Define to enable kadmin client features.])])
+
+dnl The main macro for packages with optional kadmin client support.
+AC_DEFUN([RRA_LIB_KADM5CLNT_OPTIONAL],
+[RRA_LIB_HELPER_VAR_INIT([KADM5CLNT])
+ RRA_LIB_HELPER_WITH_OPTIONAL([kadm-client], [kadmin client], [KADM5CLNT])
+ AS_IF([test x"$rra_use_KADM5CLNT" != xfalse],
+ [AS_IF([test x"$rra_use_KADM5CLNT" = xtrue],
+ [_RRA_LIB_KADM5CLNT_INTERNAL([true])],
+ [_RRA_LIB_KADM5CLNT_INTERNAL([false])])])
+ AS_IF([test x"$KADM5CLNT_LIBS" != x],
+ [rra_use_KADM5CLNT=true
+ AC_DEFINE([HAVE_KADM5CLNT], 1,
+ [Define to enable kadmin client features.])])])
diff --git a/m4/krb5-config.m4 b/m4/krb5-config.m4
new file mode 100644
index 000000000000..701881e024a9
--- /dev/null
+++ b/m4/krb5-config.m4
@@ -0,0 +1,104 @@
+dnl Use krb5-config to get link paths for Kerberos libraries.
+dnl
+dnl Provides one macro, RRA_KRB5_CONFIG, which attempts to get compiler and
+dnl linker flags for a library via krb5-config and sets the appropriate shell
+dnl variables. Defines the Autoconf variable PATH_KRB5_CONFIG, which can be
+dnl used to find the default path to krb5-config.
+dnl
+dnl Depends on RRA_ENABLE_REDUCED_DEPENDS.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Written by Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2018, 2021 Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2011-2012
+dnl The Board of Trustees of the Leland Stanford Junior University
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+dnl Check for krb5-config in the user's path and set PATH_KRB5_CONFIG. This
+dnl is moved into a separate macro so that it can be loaded via AC_REQUIRE,
+dnl meaning it will only be run once even if we link with multiple krb5-config
+dnl libraries.
+AC_DEFUN([_RRA_KRB5_CONFIG_PATH],
+[AC_ARG_VAR([PATH_KRB5_CONFIG], [Path to krb5-config])
+ AC_PATH_PROG([PATH_KRB5_CONFIG], [krb5-config], [],
+ [${PATH}:/usr/kerberos/bin])])
+
+dnl Check whether the --deps flag is supported by krb5-config. Takes the path
+dnl to krb5-config to use. Note that this path is not embedded in the cache
+dnl variable, so this macro implicitly assumes that we will always use the
+dnl same krb5-config program.
+AC_DEFUN([_RRA_KRB5_CONFIG_DEPS],
+[AC_REQUIRE([_RRA_KRB5_CONFIG_PATH])
+ AC_CACHE_CHECK([for --deps support in krb5-config],
+ [rra_cv_krb5_config_deps],
+ [AS_IF(["$1" 2>&1 | grep deps >/dev/null 2>&1],
+ [rra_cv_krb5_config_deps=yes],
+ [rra_cv_krb5_config_deps=no])])])
+
+dnl Obtain the library flags for a particular library using krb5-config.
+dnl Takes the path to the krb5-config program to use, the argument to
+dnl krb5-config to use, and the variable prefix under which to store the
+dnl library flags.
+AC_DEFUN([_RRA_KRB5_CONFIG_LIBS],
+[AC_REQUIRE([_RRA_KRB5_CONFIG_PATH])
+ AC_REQUIRE([RRA_ENABLE_REDUCED_DEPENDS])
+ _RRA_KRB5_CONFIG_DEPS([$1])
+ AS_IF([test x"$rra_reduced_depends" = xfalse \
+ && test x"$rra_cv_krb5_config_deps" = xyes],
+ [$3[]_LIBS=`"$1" --deps --libs $2 2>/dev/null`],
+ [$3[]_LIBS=`"$1" --libs $2 2>/dev/null`])])
+
+dnl Attempt to find the flags for a library using krb5-config. Takes the
+dnl following arguments (in order):
+dnl
+dnl 1. The root directory for the library in question, generally from an
+dnl Autoconf --with flag. Used by preference as the path to krb5-config.
+dnl
+dnl 2. The argument to krb5-config to retrieve flags for this particular
+dnl library.
+dnl
+dnl 3. The variable prefix to use when setting CPPFLAGS and LIBS variables
+dnl based on the result of krb5-config.
+dnl
+dnl 4. Further actions to take if krb5-config was found and supported that
+dnl library type.
+dnl
+dnl 5. Further actions to take if krb5-config could not be used to get flags
+dnl for that library type.
+dnl
+dnl Special-case a krb5-config argument of krb5 and run krb5-config without an
+dnl argument if that option was requested and not supported. Old versions of
+dnl krb5-config didn't take an argument to specify the library type, but
+dnl always returned the flags for libkrb5.
+AC_DEFUN([RRA_KRB5_CONFIG],
+[rra_krb5_config_$3=
+ rra_krb5_config_$3[]_ok=
+ AS_IF([test x"$1" != x && test -x "$1/bin/krb5-config"],
+ [rra_krb5_config_$3="$1/bin/krb5-config"],
+ [_RRA_KRB5_CONFIG_PATH
+ rra_krb5_config_$3="$PATH_KRB5_CONFIG"])
+ AS_IF([test x"$rra_krb5_config_$3" != x && test -x "$rra_krb5_config_$3"],
+ [AC_CACHE_CHECK([for $2 support in krb5-config], [rra_cv_lib_$3[]_config],
+ [AS_IF(["$rra_krb5_config_$3" 2>&1 | grep $2 >/dev/null 2>&1],
+ [rra_cv_lib_$3[]_config=yes],
+ [rra_cv_lib_$3[]_config=no])])
+ AS_IF([test "$rra_cv_lib_$3[]_config" = yes],
+ [$3[]_CPPFLAGS=`"$rra_krb5_config_$3" --cflags $2 2>/dev/null`
+ _RRA_KRB5_CONFIG_LIBS([$rra_krb5_config_$3], [$2], [$3])
+ rra_krb5_config_$3[]_ok=yes],
+ [AS_IF([test x"$2" = xkrb5],
+ [$3[]_CPPFLAGS=`"$rra_krb5_config_$3" --cflags 2>/dev/null`
+ $3[]_LIBS=`"$rra_krb5_config_$3" --libs $2 2>/dev/null`
+ rra_krb5_config_$3[]_ok=yes])])])
+ AS_IF([test x"$rra_krb5_config_$3[]_ok" = xyes],
+ [$3[]_CPPFLAGS=`AS_ECHO(["$$3[]_CPPFLAGS"]) | sed 's%-I/usr/include %%'`
+ $3[]_CPPFLAGS=`AS_ECHO(["$$3[]_CPPFLAGS"]) | sed 's%-I/usr/include$%%'`
+ $4],
+ [$5])])
diff --git a/m4/krb5-pkinit.m4 b/m4/krb5-pkinit.m4
new file mode 100644
index 000000000000..5575817e6a1f
--- /dev/null
+++ b/m4/krb5-pkinit.m4
@@ -0,0 +1,47 @@
+dnl Additional probes for Kerberos PKINIT support.
+dnl
+dnl Additional Kerberos library probes that check behavior of the library
+dnl relevant to PKINIT support. Provides the macro:
+dnl
+dnl RRA_FUNC_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT_ARGS
+dnl
+dnl and defines HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT_9_ARGS if it takes
+dnl only nine arguments.
+dnl
+dnl Written by Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2007, 2018, 2020-2021 Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2011
+dnl The Board of Trustees of the Leland Stanford Junior University
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+dnl Source used by RRA_FUNC_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT_ARGS.
+AC_DEFUN([_RRA_FUNC_KRB5_PKINIT_ARGS_SOURCE], [RRA_INCLUDES_KRB5] [[
+int
+main(void)
+{
+ krb5_context c;
+ krb5_get_init_creds_opt *o;
+ krb5_principal p;
+
+ krb5_get_init_creds_opt_set_pkinit(c, o, p, NULL, NULL, 0, NULL, NULL,
+ NULL);
+}
+]])
+
+dnl Check whether krb5_get_init_creds_opt_set_pkinit takes eleven arguments
+dnl (0.8 release candidates and later) or only nine (0.7). Defines
+dnl HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT_9_ARGS if it takes nine arguments.
+AC_DEFUN([RRA_FUNC_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT_ARGS],
+[AC_CACHE_CHECK([if krb5_get_init_creds_opt_set_pkinit takes 9 arguments],
+ [rra_cv_func_krb5_get_init_creds_opt_set_pkinit_args],
+ [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_RRA_FUNC_KRB5_PKINIT_ARGS_SOURCE])],
+ [rra_cv_func_krb5_get_init_creds_opt_set_pkinit_args=yes],
+ [rra_cv_func_krb5_get_init_creds_opt_set_pkinit_args=no])])
+AS_IF([test $rra_cv_func_krb5_get_init_creds_opt_set_pkinit_args = yes],
+ [AC_DEFINE([HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT_9_ARGS], 1,
+ [Define if krb5_get_init_creds_opt_set_pkinit takes 9 arguments.])])])
diff --git a/m4/krb5.m4 b/m4/krb5.m4
new file mode 100644
index 000000000000..e6ec1bb09fa5
--- /dev/null
+++ b/m4/krb5.m4
@@ -0,0 +1,384 @@
+dnl Find the compiler and linker flags for Kerberos.
+dnl
+dnl Finds the compiler and linker flags for linking with Kerberos libraries.
+dnl Provides the --with-krb5, --with-krb5-include, and --with-krb5-lib
+dnl configure options to specify non-standard paths to the Kerberos libraries.
+dnl Uses krb5-config where available unless reduced dependencies is requested
+dnl or --with-krb5-include or --with-krb5-lib are given.
+dnl
+dnl Provides the macro RRA_LIB_KRB5 and sets the substitution variables
+dnl KRB5_CPPFLAGS, KRB5_LDFLAGS, and KRB5_LIBS. Also provides
+dnl RRA_LIB_KRB5_SWITCH to set CPPFLAGS, LDFLAGS, and LIBS to include the
+dnl Kerberos libraries, saving the current values first, and
+dnl RRA_LIB_KRB5_RESTORE to restore those settings to before the last
+dnl RRA_LIB_KRB5_SWITCH. HAVE_KRB5 will always be defined if RRA_LIB_KRB5 is
+dnl used.
+dnl
+dnl If KRB5_CPPFLAGS, KRB5_LDFLAGS, or KRB5_LIBS are set before calling these
+dnl macros, their values will be added to whatever the macros discover.
+dnl
+dnl KRB5_CPPFLAGS_WARNINGS will be set to the same value as KRB5_CPPFLAGS but
+dnl with any occurrences of -I changed to -isystem. This may be useful to
+dnl suppress warnings from the Kerberos header files when building with and
+dnl aggressive warning flags. Be aware that this change will change the
+dnl compiler header file search order as well.
+dnl
+dnl Provides the RRA_LIB_KRB5_OPTIONAL macro, which should be used if Kerberos
+dnl support is optional. In this case, Kerberos libraries are mandatory if
+dnl --with-krb5 is given, and will not be probed for if --without-krb5 is
+dnl given. Otherwise, they'll be probed for but will not be required.
+dnl Defines HAVE_KRB5 and sets rra_use_KRB5 to true if the libraries are
+dnl found. The substitution variables will always be set, but they will be
+dnl empty unless Kerberos libraries are found and the user did not disable
+dnl Kerberos support.
+dnl
+dnl Sets the Automake conditional KRB5_USES_COM_ERR saying whether we use
+dnl com_err, since if we're also linking with AFS libraries, we may have to
+dnl change library ordering in that case.
+dnl
+dnl Depends on RRA_KRB5_CONFIG, RRA_ENABLE_REDUCED_DEPENDS, and
+dnl RRA_SET_LDFLAGS.
+dnl
+dnl Also provides RRA_FUNC_KRB5_GET_INIT_CREDS_OPT_FREE_ARGS, which checks
+dnl whether krb5_get_init_creds_opt_free takes one argument or two. Defines
+dnl HAVE_KRB5_GET_INIT_CREDS_OPT_FREE_2_ARGS if it takes two arguments.
+dnl
+dnl Also provides RRA_INCLUDES_KRB5, which are the headers to include when
+dnl probing the Kerberos library properties.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Written by Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2018, 2020-2021 Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2005-2011, 2013-2014
+dnl The Board of Trustees of the Leland Stanford Junior University
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+dnl Headers to include when probing for Kerberos library properties.
+AC_DEFUN([RRA_INCLUDES_KRB5], [[
+#if HAVE_KRB5_H
+# include <krb5.h>
+#elif HAVE_KERBEROSV5_KRB5_H
+# include <kerberosv5/krb5.h>
+#else
+# include <krb5/krb5.h>
+#endif
+]])
+
+dnl Save the current CPPFLAGS, LDFLAGS, and LIBS settings and switch to
+dnl versions that include the Kerberos flags. Used as a wrapper, with
+dnl RRA_LIB_KRB5_RESTORE, around tests.
+AC_DEFUN([RRA_LIB_KRB5_SWITCH],
+[rra_krb5_save_CPPFLAGS="$CPPFLAGS"
+ rra_krb5_save_LDFLAGS="$LDFLAGS"
+ rra_krb5_save_LIBS="$LIBS"
+ CPPFLAGS="$KRB5_CPPFLAGS $CPPFLAGS"
+ LDFLAGS="$KRB5_LDFLAGS $LDFLAGS"
+ LIBS="$KRB5_LIBS $LIBS"])
+
+dnl Restore CPPFLAGS, LDFLAGS, and LIBS to their previous values (before
+dnl RRA_LIB_KRB5_SWITCH was called).
+AC_DEFUN([RRA_LIB_KRB5_RESTORE],
+[CPPFLAGS="$rra_krb5_save_CPPFLAGS"
+ LDFLAGS="$rra_krb5_save_LDFLAGS"
+ LIBS="$rra_krb5_save_LIBS"])
+
+dnl Set KRB5_CPPFLAGS and KRB5_LDFLAGS based on rra_krb5_root,
+dnl rra_krb5_libdir, and rra_krb5_includedir.
+AC_DEFUN([_RRA_LIB_KRB5_PATHS],
+[AS_IF([test x"$rra_krb5_libdir" != x],
+ [KRB5_LDFLAGS="-L$rra_krb5_libdir"],
+ [AS_IF([test x"$rra_krb5_root" != x],
+ [RRA_SET_LDFLAGS([KRB5_LDFLAGS], [$rra_krb5_root])])])
+ AS_IF([test x"$rra_krb5_includedir" != x],
+ [KRB5_CPPFLAGS="-I$rra_krb5_includedir"],
+ [AS_IF([test x"$rra_krb5_root" != x],
+ [AS_IF([test x"$rra_krb5_root" != x/usr],
+ [KRB5_CPPFLAGS="-I${rra_krb5_root}/include"])])])])
+
+dnl Check for a header using a file existence check rather than using
+dnl AC_CHECK_HEADERS. This is used if there were arguments to configure
+dnl specifying the Kerberos header path, since we may have one header in the
+dnl default include path and another under our explicitly-configured Kerberos
+dnl location. The second argument is run if the header was found.
+AC_DEFUN([_RRA_LIB_KRB5_CHECK_HEADER],
+[AC_MSG_CHECKING([for $1])
+ AS_IF([test -f "${rra_krb5_incroot}/$1"],
+ [AC_DEFINE_UNQUOTED(AS_TR_CPP([HAVE_$1]), [1],
+ [Define to 1 if you have the <$1> header file.])
+ AC_MSG_RESULT([yes])
+ $2],
+ [AC_MSG_RESULT([no])])])
+
+dnl Check for the com_err header. Internal helper macro since we need
+dnl to do the same checks in multiple places.
+AC_DEFUN([_RRA_LIB_KRB5_CHECK_HEADER_COM_ERR],
+[AS_IF([test x"$rra_krb5_incroot" = x],
+ [AC_CHECK_HEADERS([et/com_err.h kerberosv5/com_err.h])],
+ [_RRA_LIB_KRB5_CHECK_HEADER([et/com_err.h])
+ _RRA_LIB_KRB5_CHECK_HEADER([kerberosv5/com_err.h])])])
+
+dnl Check for the main Kerberos header. Internal helper macro since we need
+dnl to do the same checks in multiple places. The first argument is run if
+dnl some header was found, and the second if no header was found.
+dnl header could not be found.
+AC_DEFUN([_RRA_LIB_KRB5_CHECK_HEADER_KRB5],
+[rra_krb5_found_header=
+ AS_IF([test x"$rra_krb5_incroot" = x],
+ [AC_CHECK_HEADERS([krb5.h kerberosv5/krb5.h krb5/krb5.h],
+ [rra_krb5_found_header=true])],
+ [_RRA_LIB_KRB5_CHECK_HEADER([krb5.h],
+ [rra_krb5_found_header=true])
+ _RRA_LIB_KRB5_CHECK_HEADER([kerberosv5/krb5.h],
+ [rra_krb5_found_header=true])
+ _RRA_LIB_KRB5_CHECK_HEADER([krb5/krb5.h],
+ [rra_krb5_found_header=true])])
+ AS_IF([test x"$rra_krb5_found_header" = xtrue], [$1], [$2])])
+
+dnl Does the appropriate library checks for reduced-dependency Kerberos
+dnl linkage. The single argument, if true, says to fail if Kerberos could not
+dnl be found.
+AC_DEFUN([_RRA_LIB_KRB5_REDUCED],
+[RRA_LIB_KRB5_SWITCH
+ AC_CHECK_LIB([krb5], [krb5_init_context],
+ [KRB5_LIBS="-lkrb5"
+ LIBS="$KRB5_LIBS $LIBS"
+ AC_CHECK_FUNCS([krb5_get_error_message],
+ [AC_CHECK_FUNCS([krb5_free_error_message])],
+ [AC_CHECK_FUNCS([krb5_get_error_string], [],
+ [AC_CHECK_FUNCS([krb5_get_err_txt], [],
+ [AC_CHECK_LIB([ksvc], [krb5_svc_get_msg],
+ [KRB5_LIBS="$KRB5_LIBS -lksvc"
+ AC_DEFINE([HAVE_KRB5_SVC_GET_MSG], [1])
+ AC_CHECK_HEADERS([ibm_svc/krb5_svc.h], [], [],
+ [RRA_INCLUDES_KRB5])],
+ [AC_CHECK_LIB([com_err], [com_err],
+ [KRB5_LIBS="$KRB5_LIBS -lcom_err"],
+ [AS_IF([test x"$1" = xtrue],
+ [AC_MSG_ERROR([cannot find usable com_err library])],
+ [KRB5_LIBS=""])])
+ _RRA_LIB_KRB5_CHECK_HEADER_COM_ERR])])])])
+ _RRA_LIB_KRB5_CHECK_HEADER_KRB5([],
+ [KRB5_CPPFLAGS=
+ KRB5_LIBS=
+ AS_IF([test x"$1" = xtrue],
+ [AC_MSG_ERROR([cannot find usable Kerberos header])])])],
+ [AS_IF([test x"$1" = xtrue],
+ [AC_MSG_ERROR([cannot find usable Kerberos library])])])
+ RRA_LIB_KRB5_RESTORE])
+
+dnl Does the appropriate library checks for Kerberos linkage when we don't
+dnl have krb5-config or reduced dependencies. The single argument, if true,
+dnl says to fail if Kerberos could not be found.
+AC_DEFUN([_RRA_LIB_KRB5_MANUAL],
+[RRA_LIB_KRB5_SWITCH
+ rra_krb5_extra=
+ LIBS=
+ AC_SEARCH_LIBS([res_search], [resolv], [],
+ [AC_SEARCH_LIBS([__res_search], [resolv])])
+ AC_SEARCH_LIBS([gethostbyname], [nsl])
+ AC_SEARCH_LIBS([socket], [socket], [],
+ [AC_CHECK_LIB([nsl], [socket], [LIBS="-lnsl -lsocket $LIBS"], [],
+ [-lsocket])])
+ AC_SEARCH_LIBS([crypt], [crypt])
+ AC_SEARCH_LIBS([roken_concat], [roken])
+ rra_krb5_extra="$LIBS"
+ LIBS="$rra_krb5_save_LIBS"
+ AC_CHECK_LIB([krb5], [krb5_init_context],
+ [KRB5_LIBS="-lkrb5 -lasn1 -lcom_err -lcrypto $rra_krb5_extra"],
+ [AC_CHECK_LIB([krb5support], [krb5int_getspecific],
+ [rra_krb5_extra="-lkrb5support $rra_krb5_extra"],
+ [AC_CHECK_LIB([pthreads], [pthread_setspecific],
+ [rra_krb5_pthread="-lpthreads"],
+ [AC_CHECK_LIB([pthread], [pthread_setspecific],
+ [rra_krb5_pthread="-lpthread"])])
+ AC_CHECK_LIB([krb5support], [krb5int_setspecific],
+ [rra_krb5_extra="-lkrb5support $rra_krb5_extra $rra_krb5_pthread"],
+ [], [$rra_krb5_pthread $rra_krb5_extra])],
+ [$rra_krb5_extra])
+ AC_CHECK_LIB([com_err], [error_message],
+ [rra_krb5_extra="-lcom_err $rra_krb5_extra"], [], [$rra_krb5_extra])
+ AC_CHECK_LIB([ksvc], [krb5_svc_get_msg],
+ [rra_krb5_extra="-lksvc $rra_krb5_extra"], [], [$rra_krb5_extra])
+ AC_CHECK_LIB([k5crypto], [krb5int_hash_md5],
+ [rra_krb5_extra="-lk5crypto $rra_krb5_extra"], [], [$rra_krb5_extra])
+ AC_CHECK_LIB([k5profile], [profile_get_values],
+ [rra_krb5_extra="-lk5profile $rra_krb5_extra"], [], [$rra_krb5_extra])
+ AC_CHECK_LIB([krb5], [krb5_cc_default],
+ [KRB5_LIBS="-lkrb5 $rra_krb5_extra"],
+ [AS_IF([test x"$1" = xtrue],
+ [AC_MSG_ERROR([cannot find usable Kerberos library])])],
+ [$rra_krb5_extra])],
+ [-lasn1 -lcom_err -lcrypto $rra_krb5_extra])
+ LIBS="$KRB5_LIBS $LIBS"
+ AC_CHECK_FUNCS([krb5_get_error_message],
+ [AC_CHECK_FUNCS([krb5_free_error_message])],
+ [AC_CHECK_FUNCS([krb5_get_error_string], [],
+ [AC_CHECK_FUNCS([krb5_get_err_txt], [],
+ [AC_CHECK_FUNCS([krb5_svc_get_msg],
+ [AC_CHECK_HEADERS([ibm_svc/krb5_svc.h], [], [],
+ [RRA_INCLUDES_KRB5])],
+ [_RRA_LIB_KRB5_CHECK_HEADER_COM_ERR])])])])
+ _RRA_LIB_KRB5_CHECK_HEADER_KRB5([],
+ [KRB5_CPPFLAGS=
+ KRB5_LIBS=
+ AS_IF([test x"$1" = xtrue],
+ [AC_MSG_ERROR([cannot find usable Kerberos header])])])
+ RRA_LIB_KRB5_RESTORE])
+
+dnl Sanity-check the results of krb5-config and be sure we can really link a
+dnl Kerberos program. If that fails, clear KRB5_CPPFLAGS and KRB5_LIBS so
+dnl that we know we don't have usable flags and fall back on the manual
+dnl check.
+AC_DEFUN([_RRA_LIB_KRB5_CHECK],
+[RRA_LIB_KRB5_SWITCH
+ AC_CHECK_FUNC([krb5_init_context],
+ [_RRA_LIB_KRB5_CHECK_HEADER_KRB5([RRA_LIB_KRB5_RESTORE],
+ [RRA_LIB_KRB5_RESTORE
+ KRB5_CPPFLAGS=
+ KRB5_LIBS=
+ _RRA_LIB_KRB5_PATHS
+ _RRA_LIB_KRB5_MANUAL([$1])])],
+ [RRA_LIB_KRB5_RESTORE
+ KRB5_CPPFLAGS=
+ KRB5_LIBS=
+ _RRA_LIB_KRB5_PATHS
+ _RRA_LIB_KRB5_MANUAL([$1])])])
+
+dnl Determine Kerberos compiler and linker flags from krb5-config. Does the
+dnl additional probing we need to do to uncover error handling features, and
+dnl falls back on the manual checks.
+AC_DEFUN([_RRA_LIB_KRB5_CONFIG],
+[RRA_KRB5_CONFIG([${rra_krb5_root}], [krb5], [KRB5],
+ [_RRA_LIB_KRB5_CHECK([$1])
+ RRA_LIB_KRB5_SWITCH
+ AC_CHECK_FUNCS([krb5_get_error_message],
+ [AC_CHECK_FUNCS([krb5_free_error_message])],
+ [AC_CHECK_FUNCS([krb5_get_error_string], [],
+ [AC_CHECK_FUNCS([krb5_get_err_txt], [],
+ [AC_CHECK_FUNCS([krb5_svc_get_msg],
+ [AC_CHECK_HEADERS([ibm_svc/krb5_svc.h], [], [],
+ [RRA_INCLUDES_KRB5])],
+ [_RRA_LIB_KRB5_CHECK_HEADER_COM_ERR])])])])
+ RRA_LIB_KRB5_RESTORE],
+ [_RRA_LIB_KRB5_PATHS
+ _RRA_LIB_KRB5_MANUAL([$1])])])
+
+dnl The core of the library checking, shared between RRA_LIB_KRB5 and
+dnl RRA_LIB_KRB5_OPTIONAL. The single argument, if "true", says to fail if
+dnl Kerberos could not be found. Set up rra_krb5_incroot for later header
+dnl checking.
+AC_DEFUN([_RRA_LIB_KRB5_INTERNAL],
+[AC_REQUIRE([RRA_ENABLE_REDUCED_DEPENDS])
+ rra_krb5_incroot=
+ AC_SUBST([KRB5_CPPFLAGS])
+ AC_SUBST([KRB5_CPPFLAGS_WARNINGS])
+ AC_SUBST([KRB5_LDFLAGS])
+ AC_SUBST([KRB5_LIBS])
+ AS_IF([test x"$rra_krb5_includedir" != x],
+ [rra_krb5_incroot="$rra_krb5_includedir"],
+ [AS_IF([test x"$rra_krb5_root" != x],
+ [rra_krb5_incroot="${rra_krb5_root}/include"])])
+ AS_IF([test x"$rra_reduced_depends" = xtrue],
+ [_RRA_LIB_KRB5_PATHS
+ _RRA_LIB_KRB5_REDUCED([$1])],
+ [AS_IF([test x"$rra_krb5_includedir" = x && test x"$rra_krb5_libdir" = x],
+ [_RRA_LIB_KRB5_CONFIG([$1])],
+ [_RRA_LIB_KRB5_PATHS
+ _RRA_LIB_KRB5_MANUAL([$1])])])
+ rra_krb5_uses_com_err=false
+ AS_CASE([$KRB5_LIBS], [*-lcom_err*], [rra_krb5_uses_com_err=true])
+ AM_CONDITIONAL([KRB5_USES_COM_ERR],
+ [test x"$rra_krb5_uses_com_err" = xtrue])
+ KRB5_CPPFLAGS_WARNINGS=`AS_ECHO(["$KRB5_CPPFLAGS"]) | sed 's/-I/-isystem /g'`])
+
+dnl The main macro for packages with mandatory Kerberos support.
+AC_DEFUN([RRA_LIB_KRB5],
+[rra_krb5_root=
+ rra_krb5_libdir=
+ rra_krb5_includedir=
+ rra_use_KRB5=true
+
+ AC_ARG_WITH([krb5],
+ [AS_HELP_STRING([--with-krb5=DIR],
+ [Location of Kerberos headers and libraries])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_krb5_root="$withval"])])
+ AC_ARG_WITH([krb5-include],
+ [AS_HELP_STRING([--with-krb5-include=DIR],
+ [Location of Kerberos headers])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_krb5_includedir="$withval"])])
+ AC_ARG_WITH([krb5-lib],
+ [AS_HELP_STRING([--with-krb5-lib=DIR],
+ [Location of Kerberos libraries])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_krb5_libdir="$withval"])])
+ _RRA_LIB_KRB5_INTERNAL([true])
+ AC_DEFINE([HAVE_KRB5], 1, [Define to enable Kerberos features.])])
+
+dnl The main macro for packages with optional Kerberos support.
+AC_DEFUN([RRA_LIB_KRB5_OPTIONAL],
+[rra_krb5_root=
+ rra_krb5_libdir=
+ rra_krb5_includedir=
+ rra_use_KRB5=
+
+ AC_ARG_WITH([krb5],
+ [AS_HELP_STRING([--with-krb5@<:@=DIR@:>@],
+ [Location of Kerberos headers and libraries])],
+ [AS_IF([test x"$withval" = xno],
+ [rra_use_KRB5=false],
+ [AS_IF([test x"$withval" != xyes], [rra_krb5_root="$withval"])
+ rra_use_KRB5=true])])
+ AC_ARG_WITH([krb5-include],
+ [AS_HELP_STRING([--with-krb5-include=DIR],
+ [Location of Kerberos headers])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_krb5_includedir="$withval"])])
+ AC_ARG_WITH([krb5-lib],
+ [AS_HELP_STRING([--with-krb5-lib=DIR],
+ [Location of Kerberos libraries])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_krb5_libdir="$withval"])])
+
+ AS_IF([test x"$rra_use_KRB5" != xfalse],
+ [AS_IF([test x"$rra_use_KRB5" = xtrue],
+ [_RRA_LIB_KRB5_INTERNAL([true])],
+ [_RRA_LIB_KRB5_INTERNAL([false])])],
+ [AM_CONDITIONAL([KRB5_USES_COM_ERR], [false])])
+ AS_IF([test x"$KRB5_LIBS" != x],
+ [rra_use_KRB5=true
+ AC_DEFINE([HAVE_KRB5], 1, [Define to enable Kerberos features.])])])
+
+dnl Source used by RRA_FUNC_KRB5_GET_INIT_CREDS_OPT_FREE_ARGS.
+AC_DEFUN([_RRA_FUNC_KRB5_OPT_FREE_ARGS_SOURCE], [RRA_INCLUDES_KRB5] [[
+int
+main(void)
+{
+ krb5_get_init_creds_opt *opts;
+ krb5_context c;
+ krb5_get_init_creds_opt_free(c, opts);
+}
+]])
+
+dnl Check whether krb5_get_init_creds_opt_free takes one argument or two.
+dnl Early Heimdal used to take a single argument. Defines
+dnl HAVE_KRB5_GET_INIT_CREDS_OPT_FREE_2_ARGS if it takes two arguments.
+dnl
+dnl Should be called with RRA_LIB_KRB5_SWITCH active.
+AC_DEFUN([RRA_FUNC_KRB5_GET_INIT_CREDS_OPT_FREE_ARGS],
+[AC_CACHE_CHECK([if krb5_get_init_creds_opt_free takes two arguments],
+ [rra_cv_func_krb5_get_init_creds_opt_free_args],
+ [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_RRA_FUNC_KRB5_OPT_FREE_ARGS_SOURCE])],
+ [rra_cv_func_krb5_get_init_creds_opt_free_args=yes],
+ [rra_cv_func_krb5_get_init_creds_opt_free_args=no])])
+ AS_IF([test $rra_cv_func_krb5_get_init_creds_opt_free_args = yes],
+ [AC_DEFINE([HAVE_KRB5_GET_INIT_CREDS_OPT_FREE_2_ARGS], 1,
+ [Define if krb5_get_init_creds_opt_free takes two arguments.])])])
diff --git a/m4/ld-version.m4 b/m4/ld-version.m4
new file mode 100644
index 000000000000..f94347f41a30
--- /dev/null
+++ b/m4/ld-version.m4
@@ -0,0 +1,40 @@
+dnl Check whether the linker supports --version-script.
+dnl
+dnl Probes whether the linker supports --version-script with a simple version
+dnl script that only defines a single version. Sets the Automake conditional
+dnl HAVE_LD_VERSION_SCRIPT based on whether it is supported.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Written by Russ Allbery <eagle@eyrie.org>
+dnl Based on the gnulib ld-version-script macro from Simon Josefsson
+dnl Copyright 2010
+dnl The Board of Trustees of the Leland Stanford Junior University
+dnl Copyright 2008-2010 Free Software Foundation, Inc.
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+AC_DEFUN([RRA_LD_VERSION_SCRIPT],
+[AC_CACHE_CHECK([if -Wl,--version-script works], [rra_cv_ld_version_script],
+ [save_LDFLAGS="$LDFLAGS"
+ LDFLAGS="$LDFLAGS -Wl,--version-script=conftest.map"
+ cat > conftest.map <<EOF
+VERSION_1 {
+ global:
+ sym;
+
+ local:
+ *;
+};
+EOF
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([], [])],
+ [rra_cv_ld_version_script=yes], [rra_cv_ld_version_script=no])
+ rm -f conftest.map
+ LDFLAGS="$save_LDFLAGS"])
+ AM_CONDITIONAL([HAVE_LD_VERSION_SCRIPT],
+ [test x"$rra_cv_ld_version_script" = xyes])])
diff --git a/m4/lib-depends.m4 b/m4/lib-depends.m4
new file mode 100644
index 000000000000..09a2cf9f0737
--- /dev/null
+++ b/m4/lib-depends.m4
@@ -0,0 +1,30 @@
+dnl Provides option to change library probes.
+dnl
+dnl This file provides RRA_ENABLE_REDUCED_DEPENDS, which adds the configure
+dnl option --enable-reduced-depends to request that library probes assume
+dnl shared libraries are in use and dependencies of libraries should not be
+dnl probed. If this option is given, the shell variable rra_reduced_depends
+dnl is set to true; otherwise, it is set to false.
+dnl
+dnl This macro doesn't do much but is defined separately so that other macros
+dnl can require it with AC_REQUIRE.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Written by Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2005-2007
+dnl The Board of Trustees of the Leland Stanford Junior University
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+AC_DEFUN([RRA_ENABLE_REDUCED_DEPENDS],
+[rra_reduced_depends=false
+AC_ARG_ENABLE([reduced-depends],
+ [AS_HELP_STRING([--enable-reduced-depends],
+ [Try to minimize shared library dependencies])],
+ [AS_IF([test x"$enableval" = xyes], [rra_reduced_depends=true])])])
diff --git a/m4/lib-helper.m4 b/m4/lib-helper.m4
new file mode 100644
index 000000000000..481122b72a38
--- /dev/null
+++ b/m4/lib-helper.m4
@@ -0,0 +1,149 @@
+dnl Helper functions to manage compiler variables.
+dnl
+dnl These are a wide variety of helper macros to make it easier to construct
+dnl standard macros to probe for a library and to set library-specific
+dnl CPPFLAGS, LDFLAGS, and LIBS shell substitution variables. Most of them
+dnl take as one of the arguments the prefix string to use for variables, which
+dnl is usually something like "KRB5" or "GSSAPI".
+dnl
+dnl Depends on RRA_SET_LDFLAGS.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Written by Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2018 Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2011, 2013
+dnl The Board of Trustees of the Leland Stanford Junior University
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+dnl Add the library flags to the default compiler flags and then remove them.
+dnl
+dnl To use these macros, pass the prefix string used for the variables as the
+dnl only argument. For example, to use these for a library with KRB5 as a
+dnl prefix, one would use:
+dnl
+dnl AC_DEFUN([RRA_LIB_KRB5_SWITCH], [RRA_LIB_HELPER_SWITCH([KRB5])])
+dnl AC_DEFUN([RRA_LIB_KRB5_RESTORE], [RRA_LIB_HELPER_RESTORE([KRB5])])
+dnl
+dnl Then, wrap checks for library features with RRA_LIB_KRB5_SWITCH and
+dnl RRA_LIB_KRB5_RESTORE.
+AC_DEFUN([RRA_LIB_HELPER_SWITCH],
+[rra_$1[]_save_CPPFLAGS="$CPPFLAGS"
+ rra_$1[]_save_LDFLAGS="$LDFLAGS"
+ rra_$1[]_save_LIBS="$LIBS"
+ CPPFLAGS="$$1[]_CPPFLAGS $CPPFLAGS"
+ LDFLAGS="$$1[]_LDFLAGS $LDFLAGS"
+ LIBS="$$1[]_LIBS $LIBS"])
+
+AC_DEFUN([RRA_LIB_HELPER_RESTORE],
+[CPPFLAGS="$rra_$1[]_save_CPPFLAGS"
+ LDFLAGS="$rra_$1[]_save_LDFLAGS"
+ LIBS="$rra_$1[]_save_LIBS"])
+
+dnl Given _root, _libdir, and _includedir variables set for a library (set by
+dnl RRA_LIB_HELPER_WITH*), set the LDFLAGS and CPPFLAGS variables for that
+dnl library accordingly. Takes the variable prefix as the only argument.
+AC_DEFUN([RRA_LIB_HELPER_PATHS],
+[AS_IF([test x"$rra_$1[]_libdir" != x],
+ [$1[]_LDFLAGS="-L$rra_$1[]_libdir"],
+ [AS_IF([test x"$rra_$1[]_root" != x],
+ [RRA_SET_LDFLAGS([$1][_LDFLAGS], [${rra_$1[]_root}])])])
+ AS_IF([test x"$rra_$1[]_includedir" != x],
+ [$1[]_CPPFLAGS="-I$rra_$1[]_includedir"],
+ [AS_IF([test x"$rra_$1[]_root" != x],
+ [AS_IF([test x"$rra_$1[]_root" != x/usr],
+ [$1[]_CPPFLAGS="-I${rra_$1[]_root}/include"])])])])
+
+dnl Check whether a library works. This is used as a sanity check on the
+dnl results of *-config shell scripts. Takes four arguments; the first, if
+dnl "true", says that a working library is mandatory and errors out if it
+dnl doesn't. The second is the variable prefix. The third is a function to
+dnl look for that should be in the libraries. The fourth is the
+dnl human-readable name of the library for error messages.
+AC_DEFUN([RRA_LIB_HELPER_CHECK],
+[RRA_LIB_HELPER_SWITCH([$2])
+ AC_CHECK_FUNC([$3], [],
+ [AS_IF([test x"$1" = xtrue],
+ [AC_MSG_FAILURE([unable to link with $4 library])])
+ $2[]_CPPFLAGS=
+ $2[]_LDFLAGS=
+ $2[]_LIBS=])
+ RRA_LIB_HELPER_RESTORE([$2])])
+
+dnl Initialize the variables used by a library probe and set the appropriate
+dnl ones as substitution variables. Takes the library variable prefix as its
+dnl only argument.
+AC_DEFUN([RRA_LIB_HELPER_VAR_INIT],
+[rra_$1[]_root=
+ rra_$1[]_libdir=
+ rra_$1[]_includedir=
+ rra_use_$1=
+ $1[]_CPPFLAGS=
+ $1[]_LDFLAGS=
+ $1[]_LIBS=
+ AC_SUBST([$1][_CPPFLAGS])
+ AC_SUBST([$1][_LDFLAGS])
+ AC_SUBST([$1][_LIBS])])
+
+dnl Unset all of the variables used by a library probe. Used with the
+dnl _OPTIONAL versions of header probes when a header or library wasn't found
+dnl and therefore the library isn't usable.
+AC_DEFUN([RRA_LIB_HELPER_VAR_CLEAR],
+[$1[]_CPPFLAGS=
+ $1[]_LDFLAGS=
+ $1[]_LIBS=])
+
+dnl Handles --with options for a non-optional library. First argument is the
+dnl base for the switch names. Second argument is the short description.
+dnl Third argument is the variable prefix. The variables set are used by
+dnl RRA_LIB_HELPER_PATHS.
+AC_DEFUN([RRA_LIB_HELPER_WITH],
+[AC_ARG_WITH([$1],
+ [AS_HELP_STRING([--with-][$1][=DIR],
+ [Location of $2 headers and libraries])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_$3[]_root="$withval"])])
+ AC_ARG_WITH([$1][-include],
+ [AS_HELP_STRING([--with-][$1][-include=DIR],
+ [Location of $2 headers])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_$3[]_includedir="$withval"])])
+ AC_ARG_WITH([$1][-lib],
+ [AS_HELP_STRING([--with-][$1][-lib=DIR],
+ [Location of $2 libraries])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_$3[]_libdir="$withval"])])])
+
+dnl Handles --with options for an optional library, so --with-<library> can
+dnl cause the checks to be skipped entirely or become mandatory. Sets an
+dnl rra_use_PREFIX variable to true or false if the library is explicitly
+dnl enabled or disabled.
+dnl
+dnl First argument is the base for the switch names. Second argument is the
+dnl short description. Third argument is the variable prefix.
+dnl
+dnl The variables set are used by RRA_LIB_HELPER_PATHS.
+AC_DEFUN([RRA_LIB_HELPER_WITH_OPTIONAL],
+[AC_ARG_WITH([$1],
+ [AS_HELP_STRING([--with-][$1][@<:@=DIR@:>@],
+ [Location of $2 headers and libraries])],
+ [AS_IF([test x"$withval" = xno],
+ [rra_use_$3=false],
+ [AS_IF([test x"$withval" != xyes], [rra_$3[]_root="$withval"])
+ rra_use_$3=true])])
+ AC_ARG_WITH([$1][-include],
+ [AS_HELP_STRING([--with-][$1][-include=DIR],
+ [Location of $2 headers])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_$3[]_includedir="$withval"])])
+ AC_ARG_WITH([$1][-lib],
+ [AS_HELP_STRING([--with-][$1][-lib=DIR],
+ [Location of $2 libraries])],
+ [AS_IF([test x"$withval" != xyes && test x"$withval" != xno],
+ [rra_$3[]_libdir="$withval"])])])
diff --git a/m4/lib-pathname.m4 b/m4/lib-pathname.m4
new file mode 100644
index 000000000000..11f6cab0673d
--- /dev/null
+++ b/m4/lib-pathname.m4
@@ -0,0 +1,54 @@
+dnl Determine the library path name.
+dnl
+dnl Red Hat systems and some other Linux systems use lib64 and lib32 rather
+dnl than just lib in some circumstances. This file provides an Autoconf
+dnl macro, RRA_SET_LDFLAGS, which given a variable, a prefix, and an optional
+dnl suffix, adds -Lprefix/lib, -Lprefix/lib32, or -Lprefix/lib64 to the
+dnl variable depending on which directories exist and the size of a long in
+dnl the compilation environment. If a suffix is given, a slash and that
+dnl suffix will be appended, to allow for adding a subdirectory of the library
+dnl directory.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Written by Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2021 Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2008-2009
+dnl The Board of Trustees of the Leland Stanford Junior University
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+dnl Probe for the alternate library name that we should attempt on this
+dnl architecture, given the size of an int, and set rra_lib_arch_name to that
+dnl name. Separated out so that it can be AC_REQUIRE'd and not run multiple
+dnl times.
+dnl
+dnl There is an unfortunate abstraction violation here where we assume we know
+dnl the cache variable name used by Autoconf. Unfortunately, Autoconf doesn't
+dnl provide any other way of getting at that information in shell that I can
+dnl see.
+AC_DEFUN([_RRA_LIB_ARCH_NAME],
+[rra_lib_arch_name=lib
+ AC_CHECK_SIZEOF([long])
+ AS_IF([test "$ac_cv_sizeof_long" -eq 4],
+ [rra_lib_arch_name=lib32],
+ [AS_IF([test "$ac_cv_sizeof_long" -eq 8],
+ [rra_lib_arch_name=lib64])])])
+
+dnl Set VARIABLE to -LPREFIX/lib{,32,64} or -LPREFIX/lib{,32,64}/SUFFIX as
+dnl appropriate.
+AC_DEFUN([RRA_SET_LDFLAGS],
+[AC_REQUIRE([_RRA_LIB_ARCH_NAME])
+ AS_IF([test -d "$2/$rra_lib_arch_name"],
+ [AS_IF([test x"$3" = x],
+ [$1="[$]$1 -L$2/${rra_lib_arch_name}"],
+ [$1="[$]$1 -L$2/${rra_lib_arch_name}/$3"])],
+ [AS_IF([test x"$3" = x],
+ [$1="[$]$1 -L$2/lib"],
+ [$1="[$]$1 -L$2/lib/$3"])])
+ $1=`AS_ECHO(["[$]$1"]) | sed -e 's/^ *//'`])
diff --git a/m4/pam-const.m4 b/m4/pam-const.m4
new file mode 100644
index 000000000000..3423d1a60d3f
--- /dev/null
+++ b/m4/pam-const.m4
@@ -0,0 +1,53 @@
+dnl Determine whether PAM uses const in prototypes.
+dnl
+dnl Linux marks several PAM arguments const, including the argument to
+dnl pam_get_item and some arguments to conversation functions, which Solaris
+dnl doesn't. Mac OS X, OS X, and macOS mark the first argument to
+dnl pam_strerror const, and other platforms don't. This test tries to
+dnl determine which style is in use to select whether to declare variables
+dnl const and how to prototype functions in order to avoid compiler warnings.
+dnl
+dnl Since this is just for compiler warnings, it's not horribly important if
+dnl we guess wrong. This test is ugly, but it seems to work.
+dnl
+dnl The canonical version of this file is maintained in the rra-c-util
+dnl package, available at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+dnl
+dnl Written by Markus Moeller
+dnl Copyright 2007, 2015 Russ Allbery <eagle@eyrie.org>
+dnl Copyright 2007-2008 Markus Moeller
+dnl
+dnl This file is free software; the authors give unlimited permission to copy
+dnl and/or distribute it, with or without modifications, as long as this
+dnl notice is preserved.
+dnl
+dnl SPDX-License-Identifier: FSFULLR
+
+dnl Source used by RRA_HEADER_PAM_CONST.
+AC_DEFUN([_RRA_HEADER_PAM_CONST_SOURCE],
+[#ifdef HAVE_SECURITY_PAM_APPL_H
+# include <security/pam_appl.h>
+#else
+# include <pam/pam_appl.h>
+#endif
+])
+
+AC_DEFUN([RRA_HEADER_PAM_CONST],
+[AC_CACHE_CHECK([whether PAM prefers const], [rra_cv_header_pam_const],
+ [AC_EGREP_CPP([const void \*\* *_?item], _RRA_HEADER_PAM_CONST_SOURCE(),
+ [rra_cv_header_pam_const=yes], [rra_cv_header_pam_const=no])])
+ AS_IF([test x"$rra_cv_header_pam_const" = xyes],
+ [rra_header_pam_const=const], [rra_header_pam_const=])
+ AC_DEFINE_UNQUOTED([PAM_CONST], [$rra_header_pam_const],
+ [Define to const if PAM uses const in pam_get_item, empty otherwise.])])
+
+AC_DEFUN([RRA_HEADER_PAM_STRERROR_CONST],
+[AC_CACHE_CHECK([whether pam_strerror uses const],
+ [rra_cv_header_pam_strerror_const],
+ [AC_EGREP_CPP([pam_strerror *\(const], _RRA_HEADER_PAM_CONST_SOURCE(),
+ [rra_cv_header_pam_strerror_const=yes],
+ [rra_cv_header_pam_strerror_const=no])])
+ AS_IF([test x"$rra_cv_header_pam_strerror_const" = xyes],
+ [rra_header_pam_strerror_const=const], [rra_header_pam_strerror_const=])
+ AC_DEFINE_UNQUOTED([PAM_STRERROR_CONST], [$rra_header_pam_strerror_const],
+ [Define to const if PAM uses const in pam_strerror, empty otherwise.])])
diff --git a/module/account.c b/module/account.c
new file mode 100644
index 000000000000..c270c9b97431
--- /dev/null
+++ b/module/account.c
@@ -0,0 +1,92 @@
+/*
+ * Implements the PAM authorization function (pam_acct_mgmt).
+ *
+ * We don't have much to do for account management, but we do recheck the
+ * user's authorization against .k5login (or whatever equivalent we've been
+ * configured for).
+ *
+ * Copyright 2005-2009, 2014, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+/* Get prototypes for the account management functions. */
+#define PAM_SM_ACCOUNT
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Check the authorization of the user. It's not entirely clear what this
+ * function is supposed to do, but rechecking .k5login and friends makes the
+ * most sense.
+ */
+int
+pamk5_account(struct pam_args *args)
+{
+ struct context *ctx;
+ int retval;
+ const char *name;
+
+ /* If the account was expired, here's where we actually fail. */
+ ctx = args->config->ctx;
+ if (ctx->expired) {
+ pam_syslog(args->pamh, LOG_INFO, "user %s account password is expired",
+ ctx->name);
+ return PAM_NEW_AUTHTOK_REQD;
+ }
+
+ /*
+ * Re-retrieve the user rather than trusting our context; it's conceivable
+ * the application could have changed it. We have to cast &name due to
+ * C's broken type system.
+ *
+ * Use pam_get_item rather than pam_get_user here since the user should be
+ * set by the time we get to this point. If we would have to prompt for a
+ * user, something is definitely broken and we should fail.
+ */
+ retval = pam_get_item(args->pamh, PAM_USER, (PAM_CONST void **) &name);
+ if (retval != PAM_SUCCESS || name == NULL) {
+ putil_err_pam(args, retval, "unable to retrieve user");
+ return PAM_AUTH_ERR;
+ }
+ if (ctx->name != name) {
+ free(ctx->name);
+ ctx->name = strdup(name);
+ args->user = ctx->name;
+ }
+
+ /*
+ * If we have a ticket cache, then we can apply an additional bit of
+ * paranoia. Rather than trusting princ in the context, extract the
+ * principal from the Kerberos ticket cache we actually received and then
+ * validate that. This should make no difference in practice, but it's a
+ * bit more thorough.
+ */
+ if (ctx->cache != NULL) {
+ putil_debug(args, "retrieving principal from cache");
+ if (ctx->princ != NULL) {
+ krb5_free_principal(ctx->context, ctx->princ);
+ ctx->princ = NULL;
+ }
+ retval = krb5_cc_get_principal(ctx->context, ctx->cache, &ctx->princ);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot get principal from cache");
+ return PAM_AUTH_ERR;
+ }
+ }
+ return pamk5_authorized(args);
+}
diff --git a/module/alt-auth.c b/module/alt-auth.c
new file mode 100644
index 000000000000..e5294bbceb7b
--- /dev/null
+++ b/module/alt-auth.c
@@ -0,0 +1,240 @@
+/*
+ * Support for alternate authentication mapping.
+ *
+ * pam-krb5 supports a feature where the principal for authentication can be
+ * set via a PAM option and possibly based on the authenticating user. This
+ * can be used to, for example, require /root instances be used with sudo
+ * while still using normal instances for other system authentications.
+ *
+ * This file collects all the pieces related to that support.
+ *
+ * Original support written by Booker Bense <bbense@slac.stanford.edu>
+ * Further updates by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2008-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Map the user to a Kerberos principal according to alt_auth_map. Returns 0
+ * on success, storing the mapped principal name in newly allocated memory in
+ * principal. The caller is responsible for freeing. Returns an errno value
+ * on any error.
+ */
+int
+pamk5_map_principal(struct pam_args *args, const char *username,
+ char **principal)
+{
+ char *realm;
+ char *new_user = NULL;
+ const char *user;
+ const char *p;
+ size_t needed, offset;
+ int oerrno;
+
+ /* Makes no sense if alt_auth_map isn't set. */
+ if (args->config->alt_auth_map == NULL)
+ return EINVAL;
+
+ /* Need to split off the realm if it is present. */
+ realm = strchr(username, '@');
+ if (realm == NULL)
+ user = username;
+ else {
+ new_user = strdup(username);
+ if (new_user == NULL)
+ return errno;
+ realm = strchr(new_user, '@');
+ if (realm == NULL)
+ goto fail;
+ *realm = '\0';
+ realm++;
+ user = new_user;
+ }
+
+ /* Now, allocate a string and build the principal. */
+ needed = 0;
+ for (p = args->config->alt_auth_map; *p != '\0'; p++) {
+ if (p[0] == '%' && p[1] == 's') {
+ needed += strlen(user);
+ p++;
+ } else {
+ needed++;
+ }
+ }
+ if (realm != NULL && strchr(args->config->alt_auth_map, '@') == NULL)
+ needed += 1 + strlen(realm);
+ needed++;
+ *principal = malloc(needed);
+ if (*principal == NULL)
+ goto fail;
+ offset = 0;
+ for (p = args->config->alt_auth_map; *p != '\0'; p++) {
+ if (p[0] == '%' && p[1] == 's') {
+ memcpy(*principal + offset, user, strlen(user));
+ offset += strlen(user);
+ p++;
+ } else {
+ (*principal)[offset] = *p;
+ offset++;
+ }
+ }
+ if (realm != NULL && strchr(args->config->alt_auth_map, '@') == NULL) {
+ (*principal)[offset] = '@';
+ offset++;
+ memcpy(*principal + offset, realm, strlen(realm));
+ offset += strlen(realm);
+ }
+ (*principal)[offset] = '\0';
+ free(new_user);
+ return 0;
+
+fail:
+ if (new_user != NULL) {
+ oerrno = errno;
+ free(new_user);
+ errno = oerrno;
+ }
+ return errno;
+}
+
+
+/*
+ * Authenticate using an alternate principal mapping.
+ *
+ * Create a principal based on the principal mapping and the user, and use the
+ * provided password to try to authenticate as that user. If we succeed, fill
+ * out creds, set princ to the successful principal in the context, and return
+ * 0. Otherwise, return a Kerberos error code or an errno value.
+ */
+krb5_error_code
+pamk5_alt_auth(struct pam_args *args, const char *service,
+ krb5_get_init_creds_opt *opts, const char *pass,
+ krb5_creds *creds)
+{
+ struct context *ctx = args->config->ctx;
+ char *kuser;
+ krb5_principal princ;
+ krb5_error_code retval;
+
+ retval = pamk5_map_principal(args, ctx->name, &kuser);
+ if (retval != 0)
+ return retval;
+ retval = krb5_parse_name(ctx->context, kuser, &princ);
+ if (retval != 0) {
+ free(kuser);
+ return retval;
+ }
+ free(kuser);
+
+ /* Log the principal we're attempting to authenticate as. */
+ if (args->debug) {
+ char *principal;
+
+ retval = krb5_unparse_name(ctx->context, princ, &principal);
+ if (retval != 0)
+ putil_debug_krb5(args, retval, "krb5_unparse_name failed");
+ else {
+ putil_debug(args, "mapping %s to %s", ctx->name, principal);
+ krb5_free_unparsed_name(ctx->context, principal);
+ }
+ }
+
+ /*
+ * Now, attempt to authenticate as that user. On success, save the
+ * principal. Return the Kerberos status code.
+ */
+ retval = krb5_get_init_creds_password(ctx->context, creds, princ,
+ (char *) pass, pamk5_prompter_krb5,
+ args, 0, (char *) service, opts);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "alternate authentication failed");
+ krb5_free_principal(ctx->context, princ);
+ return retval;
+ } else {
+ putil_debug(args, "alternate authentication successful");
+ if (ctx->princ != NULL)
+ krb5_free_principal(ctx->context, ctx->princ);
+ ctx->princ = princ;
+ return 0;
+ }
+}
+
+
+/*
+ * Verify an alternate authentication.
+ *
+ * Meant to be called from pamk5_authorized, this checks that the principal in
+ * the context matches the alt_auth_map-derived identity of the user we're
+ * authenticating. Returns PAM_SUCCESS if they match, PAM_AUTH_ERR if they
+ * don't match, and PAM_SERVICE_ERR on an internal error.
+ */
+int
+pamk5_alt_auth_verify(struct pam_args *args)
+{
+ struct context *ctx;
+ char *name = NULL;
+ char *mapped = NULL;
+ char *authed = NULL;
+ krb5_principal princ = NULL;
+ krb5_error_code retval;
+ int status = PAM_SERVICE_ERR;
+
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+ if (ctx->context == NULL || ctx->name == NULL)
+ return PAM_SERVICE_ERR;
+ if (pamk5_map_principal(args, ctx->name, &name) != 0) {
+ putil_err(args, "cannot map principal name");
+ goto done;
+ }
+ retval = krb5_parse_name(ctx->context, name, &princ);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot parse mapped principal name %s",
+ mapped);
+ goto done;
+ }
+ retval = krb5_unparse_name(ctx->context, princ, &mapped);
+ if (retval != 0) {
+ putil_err_krb5(args, retval,
+ "krb5_unparse_name on mapped principal failed");
+ goto done;
+ }
+ retval = krb5_unparse_name(ctx->context, ctx->princ, &authed);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "krb5_unparse_name failed");
+ goto done;
+ }
+ if (strcmp(authed, mapped) == 0)
+ status = PAM_SUCCESS;
+ else {
+ putil_debug(args, "mapped user %s does not match principal %s", mapped,
+ authed);
+ status = PAM_AUTH_ERR;
+ }
+
+done:
+ free(name);
+ if (authed != NULL)
+ krb5_free_unparsed_name(ctx->context, authed);
+ if (mapped != NULL)
+ krb5_free_unparsed_name(ctx->context, mapped);
+ if (princ != NULL)
+ krb5_free_principal(ctx->context, princ);
+ return status;
+}
diff --git a/module/auth.c b/module/auth.c
new file mode 100644
index 000000000000..065ce97b6596
--- /dev/null
+++ b/module/auth.c
@@ -0,0 +1,1135 @@
+/*
+ * Core authentication routines for pam_krb5.
+ *
+ * The actual authentication work is done here, either via password or via
+ * PKINIT. The only external interface is pamk5_password_auth, which calls
+ * the appropriate internal functions. This interface is used by both the
+ * authentication and the password groups.
+ *
+ * Copyright 2005-2010, 2014-2015, 2017, 2020
+ * Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+#ifdef HAVE_HX509_ERR_H
+# include <hx509_err.h>
+#endif
+#include <pwd.h>
+#include <sys/stat.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+#include <pam-util/vector.h>
+
+/*
+ * If the PKINIT smart card error statuses aren't defined, define them to 0.
+ * This will cause the right thing to happen with the logic around PKINIT.
+ */
+#ifndef HX509_PKCS11_NO_TOKEN
+# define HX509_PKCS11_NO_TOKEN 0
+#endif
+#ifndef HX509_PKCS11_NO_SLOT
+# define HX509_PKCS11_NO_SLOT 0
+#endif
+
+
+/*
+ * Fill in ctx->princ from the value of ctx->name or (if configured) from
+ * prompting. If we don't prompt and ctx->name contains an @-sign,
+ * canonicalize it to a local account name unless no_update_user is set. If
+ * the canonicalization fails, don't worry about it. It may be that the
+ * application doesn't care.
+ */
+static krb5_error_code
+parse_name(struct pam_args *args)
+{
+ struct context *ctx = args->config->ctx;
+ krb5_context c = ctx->context;
+ char *user_realm;
+ char *user = ctx->name;
+ char *newuser = NULL;
+ char kuser[65] = ""; /* MAX_USERNAME == 65 (MIT Kerberos 1.4.1). */
+ krb5_error_code k5_errno;
+ int retval;
+
+ /*
+ * If configured to prompt for the principal, do that first. Fall back on
+ * using the local username as normal if prompting fails or if the user
+ * just presses Enter.
+ */
+ if (args->config->prompt_principal) {
+ retval = pamk5_conv(args, "Principal: ", PAM_PROMPT_ECHO_ON, &user);
+ if (retval != PAM_SUCCESS)
+ putil_err_pam(args, retval, "error getting principal");
+ if (*user == '\0') {
+ free(user);
+ user = ctx->name;
+ }
+ }
+
+ /*
+ * We don't just call krb5_parse_name so that we can work around a bug in
+ * MIT Kerberos versions prior to 1.4, which store the realm in a static
+ * variable inside the library and don't notice changes. If no realm is
+ * specified and a realm is set in our arguments, append the realm to
+ * force krb5_parse_name to do the right thing.
+ */
+ user_realm = args->realm;
+ if (args->config->user_realm)
+ user_realm = args->config->user_realm;
+ if (user_realm != NULL && strchr(user, '@') == NULL) {
+ if (asprintf(&newuser, "%s@%s", user, user_realm) < 0) {
+ if (user != ctx->name)
+ free(user);
+ return KRB5_CC_NOMEM;
+ }
+ if (user != ctx->name)
+ free(user);
+ user = newuser;
+ }
+ k5_errno = krb5_parse_name(c, user, &ctx->princ);
+ if (user != ctx->name)
+ free(user);
+ if (k5_errno != 0)
+ return k5_errno;
+
+ /*
+ * Now that we have a principal to call krb5_aname_to_localname, we can
+ * canonicalize ctx->name to a local name. We do this even if we were
+ * explicitly prompting for a principal, but we use ctx->name to generate
+ * the local username, not the principal name. It's unlikely, and would
+ * be rather weird, if the user were to specify a principal name for the
+ * username and then enter a different username at the principal prompt,
+ * but this behavior seems to make the most sense.
+ *
+ * Skip canonicalization if no_update_user was set. In that case,
+ * continue to use the initial authentication identity everywhere.
+ */
+ if (strchr(ctx->name, '@') != NULL && !args->config->no_update_user) {
+ if (krb5_aname_to_localname(c, ctx->princ, sizeof(kuser), kuser) != 0)
+ return 0;
+ user = strdup(kuser);
+ if (user == NULL) {
+ putil_crit(args, "cannot allocate memory: %s", strerror(errno));
+ return 0;
+ }
+ free(ctx->name);
+ ctx->name = user;
+ args->user = user;
+ }
+ return k5_errno;
+}
+
+
+/*
+ * Set initial credential options based on our configuration information, and
+ * using the Heimdal call to set initial credential options if it's available.
+ * This function is used both for regular password authentication and for
+ * PKINIT. It also configures FAST if requested and the Kerberos libraries
+ * support it.
+ *
+ * Takes a flag indicating whether we're getting tickets for a specific
+ * service. If so, we don't try to get forwardable, renewable, or proxiable
+ * tickets.
+ */
+static void
+set_credential_options(struct pam_args *args, krb5_get_init_creds_opt *opts,
+ int service)
+{
+ struct pam_config *config = args->config;
+ krb5_context c = config->ctx->context;
+
+ krb5_get_init_creds_opt_set_default_flags(c, "pam", args->realm, opts);
+ if (!service) {
+ if (config->forwardable)
+ krb5_get_init_creds_opt_set_forwardable(opts, 1);
+ if (config->ticket_lifetime != 0)
+ krb5_get_init_creds_opt_set_tkt_life(opts,
+ config->ticket_lifetime);
+ if (config->renew_lifetime != 0)
+ krb5_get_init_creds_opt_set_renew_life(opts,
+ config->renew_lifetime);
+ krb5_get_init_creds_opt_set_change_password_prompt(
+ opts, (config->defer_pwchange || config->fail_pwchange) ? 0 : 1);
+ } else {
+ krb5_get_init_creds_opt_set_forwardable(opts, 0);
+ krb5_get_init_creds_opt_set_proxiable(opts, 0);
+ krb5_get_init_creds_opt_set_renew_life(opts, 0);
+ }
+ pamk5_fast_setup(args, opts);
+
+ /*
+ * Set options for PKINIT. Only used with MIT Kerberos; Heimdal's
+ * implementation of PKINIT uses a separate API instead of setting
+ * get_init_creds options.
+ */
+#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PA
+ if (config->use_pkinit || config->try_pkinit) {
+ if (config->pkinit_user != NULL)
+ krb5_get_init_creds_opt_set_pa(c, opts, "X509_user_identity",
+ config->pkinit_user);
+ if (config->pkinit_anchors != NULL)
+ krb5_get_init_creds_opt_set_pa(c, opts, "X509_anchors",
+ config->pkinit_anchors);
+ if (config->preauth_opt != NULL && config->preauth_opt->count > 0) {
+ size_t i;
+ char *name, *value;
+ char save = '\0';
+
+ for (i = 0; i < config->preauth_opt->count; i++) {
+ name = config->preauth_opt->strings[i];
+ if (name == NULL)
+ continue;
+ value = strchr(name, '=');
+ if (value != NULL) {
+ save = *value;
+ *value = '\0';
+ value++;
+ }
+ krb5_get_init_creds_opt_set_pa(
+ c, opts, name, (value != NULL) ? value : "yes");
+ if (value != NULL)
+ value[-1] = save;
+ }
+ }
+ }
+#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PA */
+}
+
+
+/*
+ * Retrieve the existing password (authtok) stored in the PAM data if
+ * appropriate and if available. We decide whether to retrieve it based on
+ * the PAM configuration, and also decied whether failing to retrieve it is a
+ * fatal error. Takes the PAM arguments, the PAM authtok code to retrieve
+ * (may be PAM_AUTHTOK or PAM_OLDAUTHTOK depending on whether we're
+ * authenticating or changing the password), and the place to store the
+ * password. Returns a PAM status code.
+ *
+ * If try_first_pass, use_first_pass, or force_first_pass is set, grab the old
+ * password (if set). If force_first_pass is set, fail if the password is not
+ * already set.
+ *
+ * The empty password has to be handled separately, since the Kerberos
+ * libraries may treat it as equivalent to no password and prompt when we
+ * don't want them to. We make the assumption here that the empty password is
+ * always invalid and is an authentication failure.
+ */
+static int
+maybe_retrieve_password(struct pam_args *args, int authtok, const char **pass)
+{
+ int status;
+ const bool try_first = args->config->try_first_pass;
+ const bool use = args->config->use_first_pass;
+ const bool force = args->config->force_first_pass;
+
+ *pass = NULL;
+ if (!try_first && !use && !force)
+ return PAM_SUCCESS;
+ status = pam_get_item(args->pamh, authtok, (PAM_CONST void **) pass);
+ if (*pass != NULL && **pass == '\0') {
+ if (use || force) {
+ putil_debug(args, "rejecting empty password");
+ return PAM_AUTH_ERR;
+ }
+ *pass = NULL;
+ }
+ if (*pass != NULL && strlen(*pass) > PAM_MAX_RESP_SIZE - 1) {
+ putil_debug(args, "rejecting password longer than %d",
+ PAM_MAX_RESP_SIZE - 1);
+ return PAM_AUTH_ERR;
+ }
+ if (force && (status != PAM_SUCCESS || *pass == NULL)) {
+ putil_debug_pam(args, status, "no stored password");
+ return PAM_AUTH_ERR;
+ }
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Prompt for the password. Takes the PAM arguments, the authtok for which
+ * we're prompting (may be PAM_AUTHTOK or PAM_OLDAUTHTOK depending on whether
+ * we're authenticating or changing the password), and the place to store the
+ * password. Returns a PAM status code.
+ *
+ * If we successfully get a password, store it in the PAM data, free it, and
+ * then return the password as retrieved from the PAM data so that we don't
+ * have to worry about memory allocation later.
+ *
+ * The empty password has to be handled separately, since the Kerberos
+ * libraries may treat it as equivalent to no password and prompt when we
+ * don't want them to. We make the assumption here that the empty password is
+ * always invalid and is an authentication failure.
+ */
+static int
+prompt_password(struct pam_args *args, int authtok, const char **pass)
+{
+ char *password;
+ int status;
+ const char *prompt = (authtok == PAM_AUTHTOK) ? NULL : "Current";
+
+ *pass = NULL;
+ status = pamk5_get_password(args, prompt, &password);
+ if (status != PAM_SUCCESS) {
+ putil_debug_pam(args, status, "error getting password");
+ return PAM_AUTH_ERR;
+ }
+ if (password[0] == '\0') {
+ putil_debug(args, "rejecting empty password");
+ free(password);
+ return PAM_AUTH_ERR;
+ }
+ if (strlen(password) > PAM_MAX_RESP_SIZE - 1) {
+ putil_debug(args, "rejecting password longer than %d",
+ PAM_MAX_RESP_SIZE - 1);
+ explicit_bzero(password, strlen(password));
+ free(password);
+ return PAM_AUTH_ERR;
+ }
+
+ /* Set this for the next PAM module. */
+ status = pam_set_item(args->pamh, authtok, password);
+ explicit_bzero(password, strlen(password));
+ free(password);
+ if (status != PAM_SUCCESS) {
+ putil_err_pam(args, status, "error storing password");
+ return PAM_AUTH_ERR;
+ }
+
+ /* Return the password retrieved from PAM. */
+ status = pam_get_item(args->pamh, authtok, (PAM_CONST void **) pass);
+ if (status != PAM_SUCCESS) {
+ putil_err_pam(args, status, "error retrieving password");
+ status = PAM_AUTH_ERR;
+ }
+ return status;
+}
+
+
+/*
+ * Authenticate via password.
+ *
+ * This is our basic authentication function. Log what principal we're
+ * attempting to authenticate with and then attempt password authentication.
+ * Returns 0 on success or a Kerberos error on failure.
+ */
+static krb5_error_code
+password_auth(struct pam_args *args, krb5_creds *creds,
+ krb5_get_init_creds_opt *opts, const char *service,
+ const char *pass)
+{
+ struct context *ctx = args->config->ctx;
+ krb5_error_code retval;
+
+ /* Log the principal as which we're attempting authentication. */
+ if (args->debug) {
+ char *principal;
+
+ retval = krb5_unparse_name(ctx->context, ctx->princ, &principal);
+ if (retval != 0)
+ putil_debug_krb5(args, retval, "krb5_unparse_name failed");
+ else {
+ if (service == NULL)
+ putil_debug(args, "attempting authentication as %s",
+ principal);
+ else
+ putil_debug(args, "attempting authentication as %s for %s",
+ principal, service);
+ free(principal);
+ }
+ }
+
+ /* Do the authentication. */
+ retval = krb5_get_init_creds_password(ctx->context, creds, ctx->princ,
+ (char *) pass, pamk5_prompter_krb5,
+ args, 0, (char *) service, opts);
+
+ /*
+ * Heimdal may return an expired key error even if the password is
+ * incorrect. To avoid accepting any incorrect password for the user
+ * in the fully correct password change case, confirm that we can get
+ * a password change ticket for the user using this password, and
+ * otherwise change the error to invalid password.
+ */
+ if (retval == KRB5KDC_ERR_KEY_EXP) {
+ krb5_get_init_creds_opt *heimdal_opts = NULL;
+
+ retval = krb5_get_init_creds_opt_alloc(ctx->context, &heimdal_opts);
+ if (retval == 0) {
+ set_credential_options(args, opts, 1);
+ retval = krb5_get_init_creds_password(
+ ctx->context, creds, ctx->princ, (char *) pass,
+ pamk5_prompter_krb5, args, 0, (char *) "kadmin/changepw",
+ heimdal_opts);
+ krb5_get_init_creds_opt_free(ctx->context, heimdal_opts);
+ }
+ if (retval == 0) {
+ retval = KRB5KDC_ERR_KEY_EXP;
+ krb5_free_cred_contents(ctx->context, creds);
+ explicit_bzero(creds, sizeof(krb5_creds));
+ }
+ }
+ return retval;
+}
+
+
+/*
+ * Authenticate by trying each principal in the .k5login file.
+ *
+ * Read through each line that parses correctly as a principal and use the
+ * provided password to try to authenticate as that user. If at any point we
+ * succeed, fill out creds, set princ to the successful principal in the
+ * context, and return 0. Otherwise, return either a Kerberos error code or
+ * errno for a system error.
+ */
+static krb5_error_code
+k5login_password_auth(struct pam_args *args, krb5_creds *creds,
+ krb5_get_init_creds_opt *opts, const char *service,
+ const char *pass)
+{
+ struct context *ctx = args->config->ctx;
+ char *filename = NULL;
+ char line[BUFSIZ];
+ size_t len;
+ FILE *k5login;
+ struct passwd *pwd;
+ struct stat st;
+ krb5_error_code k5_errno, retval;
+ krb5_principal princ;
+
+ /*
+ * C sucks at string manipulation. Generate the filename for the user's
+ * .k5login file. If the user doesn't exist, the .k5login file doesn't
+ * exist, or the .k5login file cannot be read, fall back on the easy way
+ * and assume ctx->princ is already set properly.
+ */
+ pwd = pam_modutil_getpwnam(args->pamh, ctx->name);
+ if (pwd != NULL)
+ if (asprintf(&filename, "%s/.k5login", pwd->pw_dir) < 0) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ return errno;
+ }
+ if (pwd == NULL || filename == NULL || access(filename, R_OK) != 0) {
+ free(filename);
+ return krb5_get_init_creds_password(ctx->context, creds, ctx->princ,
+ (char *) pass, pamk5_prompter_krb5,
+ args, 0, (char *) service, opts);
+ }
+
+ /*
+ * Make sure the ownership on .k5login is okay. The user must own their
+ * own .k5login or it must be owned by root. If that fails, set the
+ * Kerberos error code to errno.
+ */
+ k5login = fopen(filename, "r");
+ if (k5login == NULL) {
+ retval = errno;
+ free(filename);
+ return retval;
+ }
+ free(filename);
+ if (fstat(fileno(k5login), &st) != 0) {
+ retval = errno;
+ goto fail;
+ }
+ if (st.st_uid != 0 && (st.st_uid != pwd->pw_uid)) {
+ retval = EACCES;
+ putil_err(args, "unsafe .k5login ownership (saw %lu, expected %lu)",
+ (unsigned long) st.st_uid, (unsigned long) pwd->pw_uid);
+ goto fail;
+ }
+
+ /*
+ * Parse the .k5login file and attempt authentication for each principal.
+ * Ignore any lines that are too long or that don't parse into a Kerberos
+ * principal. Assume an invalid password error if there are no valid
+ * lines in .k5login.
+ */
+ retval = KRB5KRB_AP_ERR_BAD_INTEGRITY;
+ while (fgets(line, BUFSIZ, k5login) != NULL) {
+ len = strlen(line);
+ if (line[len - 1] != '\n') {
+ while (fgets(line, BUFSIZ, k5login) != NULL) {
+ len = strlen(line);
+ if (line[len - 1] == '\n')
+ break;
+ }
+ continue;
+ }
+ line[len - 1] = '\0';
+ k5_errno = krb5_parse_name(ctx->context, line, &princ);
+ if (k5_errno != 0)
+ continue;
+
+ /* Now, attempt to authenticate as that user. */
+ if (service == NULL)
+ putil_debug(args, "attempting authentication as %s", line);
+ else
+ putil_debug(args, "attempting authentication as %s for %s", line,
+ service);
+ retval = krb5_get_init_creds_password(
+ ctx->context, creds, princ, (char *) pass, pamk5_prompter_krb5,
+ args, 0, (char *) service, opts);
+
+ /*
+ * If that worked, update ctx->princ and return success. Otherwise,
+ * continue on to the next line.
+ */
+ if (retval == 0) {
+ if (ctx->princ != NULL)
+ krb5_free_principal(ctx->context, ctx->princ);
+ ctx->princ = princ;
+ fclose(k5login);
+ return 0;
+ }
+ krb5_free_principal(ctx->context, princ);
+ }
+
+fail:
+ fclose(k5login);
+ return retval;
+}
+
+
+#if (defined(HAVE_KRB5_HEIMDAL) \
+ && defined(HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT)) \
+ || defined(HAVE_KRB5_GET_PROMPT_TYPES)
+/*
+ * Attempt authentication via PKINIT. Currently, this uses an API specific to
+ * Heimdal. Once MIT Kerberos supports PKINIT, some of the details may need
+ * to move into the compat layer.
+ *
+ * Some smart card readers require the user to enter the PIN at the keyboard
+ * after inserting the smart card. Others have a pad on the card and no
+ * prompting by PAM is required. The Kerberos library prompting functions
+ * should be able to work out which is required.
+ *
+ * PKINIT is just one of many pre-authentication mechanisms that could be
+ * used. It's handled separately because of possible smart card interactions
+ * and the possibility that some users may be authenticated via PKINIT and
+ * others may not.
+ *
+ * Takes the same arguments as pamk5_password_auth and returns a
+ * krb5_error_code. If successful, the credentials will be stored in creds.
+ */
+static krb5_error_code
+pkinit_auth(struct pam_args *args, const char *service, krb5_creds **creds)
+{
+ struct context *ctx = args->config->ctx;
+ krb5_get_init_creds_opt *opts = NULL;
+ krb5_error_code retval;
+ char *dummy = NULL;
+
+ /*
+ * We may not be able to dive directly into the PKINIT functions because
+ * the user may not have a chance to enter the smart card. For example,
+ * gnome-screensaver jumps into PAM as soon as the mouse is moved and
+ * expects to be prompted for a password, which may not happen if the
+ * smart card is the type that has a pad for the PIN on the card.
+ *
+ * Allow the user to set pkinit_prompt as an option. If set, we tell the
+ * user they need to insert the card.
+ *
+ * We always ignore the input. If the user wants to use a password
+ * instead, they'll be prompted later when the PKINIT code discovers that
+ * no smart card is available.
+ */
+ if (args->config->pkinit_prompt) {
+ pamk5_conv(args,
+ args->config->use_pkinit
+ ? "Insert smart card and press Enter: "
+ : "Insert smart card if desired, then press Enter: ",
+ PAM_PROMPT_ECHO_OFF, &dummy);
+ }
+
+ /*
+ * Set credential options. We have to use the allocated version of the
+ * credential option struct to store the PKINIT options.
+ */
+ *creds = calloc(1, sizeof(krb5_creds));
+ if (*creds == NULL)
+ return ENOMEM;
+ retval = krb5_get_init_creds_opt_alloc(ctx->context, &opts);
+ if (retval != 0)
+ return retval;
+ set_credential_options(args, opts, service != NULL);
+
+ /* Finally, do the actual work and return the results. */
+# ifdef HAVE_KRB5_HEIMDAL
+ retval = krb5_get_init_creds_opt_set_pkinit(
+ ctx->context, opts, ctx->princ, args->config->pkinit_user,
+ args->config->pkinit_anchors, NULL, NULL, 0, pamk5_prompter_krb5, args,
+ NULL);
+ if (retval == 0)
+ retval = krb5_get_init_creds_password(ctx->context, *creds, ctx->princ,
+ NULL, NULL, args, 0,
+ (char *) service, opts);
+# else /* !HAVE_KRB5_HEIMDAL */
+ retval = krb5_get_init_creds_password(
+ ctx->context, *creds, ctx->princ, NULL,
+ pamk5_prompter_krb5_no_password, args, 0, (char *) service, opts);
+# endif /* !HAVE_KRB5_HEIMDAL */
+
+ krb5_get_init_creds_opt_free(ctx->context, opts);
+ if (retval != 0) {
+ krb5_free_cred_contents(ctx->context, *creds);
+ free(*creds);
+ *creds = NULL;
+ }
+ return retval;
+}
+#endif
+
+
+/*
+ * Attempt authentication once with a given password. This is the core of the
+ * authentication loop, and handles alt_auth_map and search_k5login. It takes
+ * the PAM arguments, the service for which to get tickets (NULL for the
+ * default TGT), the initial credential options, and the password, and returns
+ * a Kerberos status code or errno. On success (return status 0), it stores
+ * the obtained credentials in the provided creds argument.
+ */
+static krb5_error_code
+password_auth_attempt(struct pam_args *args, const char *service,
+ krb5_get_init_creds_opt *opts, const char *pass,
+ krb5_creds *creds)
+{
+ krb5_error_code retval;
+
+ /*
+ * First, try authenticating as the alternate principal if one were
+ * configured. If that fails or wasn't configured, continue on to trying
+ * search_k5login or a regular authentication unless configuration
+ * indicates that regular authentication should not be attempted.
+ */
+ if (args->config->alt_auth_map != NULL) {
+ retval = pamk5_alt_auth(args, service, opts, pass, creds);
+ if (retval == 0)
+ return retval;
+
+ /* If only_alt_auth is set, we cannot continue. */
+ if (args->config->only_alt_auth)
+ return retval;
+
+ /*
+ * If force_alt_auth is set, skip attempting normal authentication iff
+ * the alternate principal exists.
+ */
+ if (args->config->force_alt_auth)
+ if (retval != KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN)
+ return retval;
+ }
+
+ /* Attempt regular authentication, via either search_k5login or normal. */
+ if (args->config->search_k5login)
+ retval = k5login_password_auth(args, creds, opts, service, pass);
+ else
+ retval = password_auth(args, creds, opts, service, pass);
+ if (retval != 0)
+ putil_debug_krb5(args, retval, "krb5_get_init_creds_password");
+ return retval;
+}
+
+
+/*
+ * Try to verify credentials by obtaining and checking a service ticket. This
+ * is required to verify that no one is spoofing the KDC, but requires read
+ * access to a keytab with a valid key. By default, the Kerberos library will
+ * silently succeed if no verification keys are available, but the user can
+ * change this by setting verify_ap_req_nofail in [libdefaults] in
+ * /etc/krb5.conf.
+ *
+ * The MIT Kerberos implementation of krb5_verify_init_creds hardwires the
+ * host key for the local system as the desired principal if no principal is
+ * given. If we have an explicitly configured keytab, instead read that
+ * keytab, find the first principal in that keytab, and use that.
+ *
+ * Returns a Kerberos status code (0 for success).
+ */
+static krb5_error_code
+verify_creds(struct pam_args *args, krb5_creds *creds)
+{
+ krb5_verify_init_creds_opt opts;
+ krb5_keytab keytab = NULL;
+ krb5_kt_cursor cursor;
+ int cursor_valid = 0;
+ krb5_keytab_entry entry;
+ krb5_principal princ = NULL;
+ krb5_error_code retval;
+ krb5_context c = args->config->ctx->context;
+
+ memset(&entry, 0, sizeof(entry));
+ krb5_verify_init_creds_opt_init(&opts);
+ if (args->config->keytab) {
+ retval = krb5_kt_resolve(c, args->config->keytab, &keytab);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot open keytab %s",
+ args->config->keytab);
+ keytab = NULL;
+ }
+ if (retval == 0)
+ retval = krb5_kt_start_seq_get(c, keytab, &cursor);
+ if (retval == 0) {
+ cursor_valid = 1;
+ retval = krb5_kt_next_entry(c, keytab, &entry, &cursor);
+ }
+ if (retval == 0)
+ retval = krb5_copy_principal(c, entry.principal, &princ);
+ if (retval != 0)
+ putil_err_krb5(args, retval, "error reading keytab %s",
+ args->config->keytab);
+ if (entry.principal != NULL)
+ krb5_kt_free_entry(c, &entry);
+ if (cursor_valid)
+ krb5_kt_end_seq_get(c, keytab, &cursor);
+ }
+ retval = krb5_verify_init_creds(c, creds, princ, keytab, NULL, &opts);
+ if (retval != 0)
+ putil_err_krb5(args, retval, "credential verification failed");
+ if (princ != NULL)
+ krb5_free_principal(c, princ);
+ if (keytab != NULL)
+ krb5_kt_close(c, keytab);
+ return retval;
+}
+
+
+/*
+ * Give the user a nicer error message when we've attempted PKINIT without
+ * success. We can only do this if the rich status codes are available.
+ * Currently, this only works with Heimdal.
+ */
+static void UNUSED
+report_pkinit_error(struct pam_args *args, krb5_error_code retval UNUSED)
+{
+ const char *message;
+
+#ifdef HAVE_HX509_ERR_H
+ switch (retval) {
+# ifdef HX509_PKCS11_PIN_LOCKED
+ case HX509_PKCS11_PIN_LOCKED:
+ message = "PKINIT failed: user PIN locked";
+ break;
+# endif
+# ifdef HX509_PKCS11_PIN_EXPIRED
+ case HX509_PKCS11_PIN_EXPIRED:
+ message = "PKINIT failed: user PIN expired";
+ break;
+# endif
+# ifdef HX509_PKCS11_PIN_INCORRECT
+ case HX509_PKCS11_PIN_INCORRECT:
+ message = "PKINIT failed: user PIN incorrect";
+ break;
+# endif
+# ifdef HX509_PKCS11_PIN_NOT_INITIALIZED
+ case HX509_PKCS11_PIN_NOT_INITIALIZED:
+ message = "PKINIT fialed: user PIN not initialized";
+ break;
+# endif
+ default:
+ message = "PKINIT failed";
+ break;
+ }
+#else
+ message = "PKINIT failed";
+#endif
+ pamk5_conv(args, message, PAM_TEXT_INFO, NULL);
+}
+
+
+/*
+ * Prompt the user for a password and authenticate the password with the KDC.
+ * If correct, fill in creds with the obtained TGT or ticket. service, if
+ * non-NULL, specifies the service to get tickets for; the only interesting
+ * non-null case is kadmin/changepw for changing passwords. Therefore, if it
+ * is non-null, we look for the password in PAM_OLDAUTHOK and save it there
+ * instead of using PAM_AUTHTOK.
+ */
+int
+pamk5_password_auth(struct pam_args *args, const char *service,
+ krb5_creds **creds)
+{
+ struct context *ctx;
+ krb5_get_init_creds_opt *opts = NULL;
+ krb5_error_code retval = 0;
+ int status = PAM_SUCCESS;
+ bool retry, prompt;
+ bool creds_valid = false;
+ const char *pass = NULL;
+ int authtok = (service == NULL) ? PAM_AUTHTOK : PAM_OLDAUTHTOK;
+
+ /* Sanity check and initialization. */
+ if (args->config->ctx == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+
+ /*
+ * Fill in the default principal to authenticate as. alt_auth_map or
+ * search_k5login may change this later.
+ */
+ if (ctx->princ == NULL) {
+ retval = parse_name(args);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "parse_name failed");
+ return PAM_SERVICE_ERR;
+ }
+ }
+
+ /*
+ * If PKINIT is available and we were configured to attempt it, try
+ * authenticating with PKINIT first. Otherwise, fail all authentication
+ * if PKINIT is not available and use_pkinit was set. Fake an error code
+ * that gives an approximately correct error message.
+ */
+#if defined(HAVE_KRB5_HEIMDAL) \
+ && defined(HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT)
+ if (args->config->use_pkinit || args->config->try_pkinit) {
+ retval = pkinit_auth(args, service, creds);
+ if (retval == 0)
+ goto verify;
+ putil_debug_krb5(args, retval, "PKINIT failed");
+ if (retval != HX509_PKCS11_NO_TOKEN && retval != HX509_PKCS11_NO_SLOT)
+ goto done;
+ if (retval != 0) {
+ report_pkinit_error(args, retval);
+ if (args->config->use_pkinit)
+ goto done;
+ }
+ }
+#elif defined(HAVE_KRB5_GET_PROMPT_TYPES)
+ if (args->config->use_pkinit) {
+ retval = pkinit_auth(args, service, creds);
+ if (retval == 0)
+ goto verify;
+ putil_debug_krb5(args, retval, "PKINIT failed");
+ report_pkinit_error(args, retval);
+ goto done;
+ }
+#endif
+
+ /* Allocate cred structure and set credential options. */
+ *creds = calloc(1, sizeof(krb5_creds));
+ if (*creds == NULL) {
+ putil_crit(args, "cannot allocate memory: %s", strerror(errno));
+ status = PAM_SERVICE_ERR;
+ goto done;
+ }
+ retval = krb5_get_init_creds_opt_alloc(ctx->context, &opts);
+ if (retval != 0) {
+ putil_crit_krb5(args, retval, "cannot allocate credential options");
+ goto done;
+ }
+ set_credential_options(args, opts, service != NULL);
+
+ /*
+ * Obtain the saved password, if appropriate and available, and determine
+ * our retry strategy. If try_first_pass is set, we will prompt for a
+ * password and retry the authentication if the stored password didn't
+ * work.
+ */
+ status = maybe_retrieve_password(args, authtok, &pass);
+ if (status != PAM_SUCCESS)
+ goto done;
+
+ /*
+ * Main authentication loop.
+ *
+ * If we had no stored password, we prompt for a password the first time
+ * through. If try_first_pass is set and we had an old password, we try
+ * with it. If the old password doesn't work, we loop once, prompt for a
+ * password, and retry. If use_first_pass is set, we'll prompt once if
+ * the password isn't already set but won't retry.
+ *
+ * If we don't have a password but try_pkinit or no_prompt are true, we
+ * don't attempt to prompt for a password and we go into the Kerberos
+ * libraries with no password. We rely on the Kerberos libraries to do
+ * the prompting if PKINIT fails. In this case, make sure we don't retry.
+ * Be aware that in this case, we also have no way of saving whatever
+ * password or other credentials the user might enter, so subsequent PAM
+ * modules will not see a stored authtok.
+ *
+ * We've already handled empty passwords in our other functions.
+ */
+ retry = args->config->try_first_pass;
+ prompt = !(args->config->try_pkinit || args->config->no_prompt);
+ do {
+ if (pass == NULL)
+ retry = false;
+ if (pass == NULL && prompt) {
+ status = prompt_password(args, authtok, &pass);
+ if (status != PAM_SUCCESS)
+ goto done;
+ }
+
+ /*
+ * Attempt authentication. If we succeeded, we're done. Otherwise,
+ * clear the password and then see if we should try again after
+ * prompting for a password.
+ */
+ retval = password_auth_attempt(args, service, opts, pass, *creds);
+ if (retval == 0) {
+ creds_valid = true;
+ break;
+ }
+ pass = NULL;
+ } while (retry
+ && (retval == KRB5KRB_AP_ERR_BAD_INTEGRITY
+ || retval == KRB5KRB_AP_ERR_MODIFIED
+ || retval == KRB5KDC_ERR_PREAUTH_FAILED
+ || retval == KRB5_GET_IN_TKT_LOOP
+ || retval == KRB5_BAD_ENCTYPE));
+
+verify:
+ UNUSED
+ /*
+ * If we think we succeeded, whether through the regular path or via
+ * PKINIT, try to verify the credentials. Don't do this if we're
+ * authenticating for password changes (or any other case where we're not
+ * getting a TGT). We can't get a service ticket from a kadmin/changepw
+ * ticket.
+ */
+ if (retval == 0 && service == NULL)
+ retval = verify_creds(args, *creds);
+
+done:
+ /*
+ * Free resources, including any credentials we have sitting around if we
+ * failed, and return the appropriate PAM error code. If status is
+ * already set to something other than PAM_SUCCESS, we encountered a PAM
+ * error and will just return that code. Otherwise, we need to map the
+ * Kerberos status code in retval to a PAM error code.
+ */
+ if (status == PAM_SUCCESS) {
+ switch (retval) {
+ case 0:
+ status = PAM_SUCCESS;
+ break;
+ case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
+ status = PAM_USER_UNKNOWN;
+ break;
+ case KRB5KDC_ERR_KEY_EXP:
+ status = PAM_NEW_AUTHTOK_REQD;
+ break;
+ case KRB5KDC_ERR_NAME_EXP:
+ status = PAM_ACCT_EXPIRED;
+ break;
+ case KRB5_KDC_UNREACH:
+ case KRB5_LIBOS_CANTREADPWD:
+ case KRB5_REALM_CANT_RESOLVE:
+ case KRB5_REALM_UNKNOWN:
+ status = PAM_AUTHINFO_UNAVAIL;
+ break;
+ default:
+ status = PAM_AUTH_ERR;
+ break;
+ }
+ }
+ if (status != PAM_SUCCESS && *creds != NULL) {
+ if (creds_valid)
+ krb5_free_cred_contents(ctx->context, *creds);
+ free(*creds);
+ *creds = NULL;
+ }
+ if (opts != NULL)
+ krb5_get_init_creds_opt_free(ctx->context, opts);
+
+ /* Whatever the results, destroy the anonymous FAST cache. */
+ if (ctx->fast_cache != NULL) {
+ krb5_cc_destroy(ctx->context, ctx->fast_cache);
+ ctx->fast_cache = NULL;
+ }
+ return status;
+}
+
+
+/*
+ * Authenticate a user via Kerberos.
+ *
+ * It would be nice to be able to save the ticket cache temporarily as a
+ * memory cache and then only write it out to disk during the session
+ * initialization. Unfortunately, OpenSSH 4.2 and later do PAM authentication
+ * in a subprocess and therefore has no saved module-specific data available
+ * once it opens a session, so we have to save the ticket cache to disk and
+ * store in the environment where it is. The alternative is to use something
+ * like System V shared memory, which seems like more trouble than it's worth.
+ */
+int
+pamk5_authenticate(struct pam_args *args)
+{
+ struct context *ctx = NULL;
+ krb5_creds *creds = NULL;
+ char *pass = NULL;
+ char *principal;
+ int pamret;
+ bool set_context = false;
+ krb5_error_code retval;
+
+ /* Temporary backward compatibility. */
+ if (args->config->use_authtok && !args->config->force_first_pass) {
+ putil_err(args, "use_authtok option in authentication group should"
+ " be changed to force_first_pass");
+ args->config->force_first_pass = true;
+ }
+
+ /* Create a context and obtain the user. */
+ pamret = pamk5_context_new(args);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ ctx = args->config->ctx;
+
+ /* Check whether we should ignore this user. */
+ if (pamk5_should_ignore(args, ctx->name)) {
+ pamret = PAM_USER_UNKNOWN;
+ goto done;
+ }
+
+ /*
+ * Do the actual authentication.
+ *
+ * The complexity arises if the password was expired (which means the
+ * Kerberos library was also unable to prompt for the password change
+ * internally). In that case, there are three possibilities:
+ * fail_pwchange says we treat that as an authentication failure and stop,
+ * defer_pwchange says to set a flag that will result in an error at the
+ * acct_mgmt step, and force_pwchange says that we should change the
+ * password here and now.
+ *
+ * defer_pwchange is the formally correct behavior. Set a flag in the
+ * context and return success. That flag will later be checked by
+ * pam_sm_acct_mgmt. We need to set the context as PAM data in the
+ * defer_pwchange case, but we don't want to set the PAM data until we've
+ * checked .k5login. If we've stacked multiple pam-krb5 invocations in
+ * different realms as optional, we don't want to override a previous
+ * successful authentication.
+ *
+ * Note this means that, if the user can authenticate with multiple realms
+ * and authentication succeeds in one realm and is then expired in a later
+ * realm, the expiration in the latter realm wins. This isn't ideal, but
+ * avoiding that case is more complicated than it's worth.
+ *
+ * We would like to set the current password as PAM_OLDAUTHTOK so that
+ * when the application subsequently calls pam_chauthtok, the user won't
+ * be reprompted. However, the PAM library clears all the auth tokens
+ * when pam_authenticate exits, so this isn't possible.
+ *
+ * In the force_pwchange case, try to use the password the user just
+ * entered to authenticate to the password changing service, but don't
+ * throw an error if that doesn't work. We have to move it from
+ * PAM_AUTHTOK to PAM_OLDAUTHTOK to be in the place where password
+ * changing expects, and have to unset PAM_AUTHTOK or we'll just change
+ * the password to the same thing it was.
+ */
+ pamret = pamk5_password_auth(args, NULL, &creds);
+ if (pamret == PAM_NEW_AUTHTOK_REQD) {
+ if (args->config->fail_pwchange)
+ pamret = PAM_AUTH_ERR;
+ else if (args->config->defer_pwchange) {
+ putil_debug(args, "expired account, deferring failure");
+ ctx->expired = 1;
+ pamret = PAM_SUCCESS;
+ } else if (args->config->force_pwchange) {
+ pam_syslog(args->pamh, LOG_INFO,
+ "user %s password expired, forcing password change",
+ ctx->name);
+ pamk5_conv(args, "Password expired. You must change it now.",
+ PAM_TEXT_INFO, NULL);
+ pamret = pam_get_item(args->pamh, PAM_AUTHTOK,
+ (PAM_CONST void **) &pass);
+ if (pamret == PAM_SUCCESS && pass != NULL)
+ pam_set_item(args->pamh, PAM_OLDAUTHTOK, pass);
+ pam_set_item(args->pamh, PAM_AUTHTOK, NULL);
+ args->config->use_first_pass = true;
+ pamret = pamk5_password_change(args, false);
+ if (pamret == PAM_SUCCESS)
+ putil_debug(args, "successfully changed expired password");
+ }
+ }
+ if (pamret != PAM_SUCCESS) {
+ putil_log_failure(args, "authentication failure");
+ goto done;
+ }
+
+ /* Check .k5login and alt_auth_map. */
+ pamret = pamk5_authorized(args);
+ if (pamret != PAM_SUCCESS) {
+ putil_log_failure(args, "failed authorization check");
+ goto done;
+ }
+
+ /* Reset PAM_USER in case we canonicalized, but ignore errors. */
+ if (!ctx->expired && !args->config->no_update_user) {
+ pamret = pam_set_item(args->pamh, PAM_USER, ctx->name);
+ if (pamret != PAM_SUCCESS)
+ putil_err_pam(args, pamret, "cannot set PAM_USER");
+ }
+
+ /* Log the successful authentication. */
+ retval = krb5_unparse_name(ctx->context, ctx->princ, &principal);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "krb5_unparse_name failed");
+ pam_syslog(args->pamh, LOG_INFO, "user %s authenticated as UNKNOWN",
+ ctx->name);
+ } else {
+ pam_syslog(args->pamh, LOG_INFO, "user %s authenticated as %s%s",
+ ctx->name, principal, ctx->expired ? " (expired)" : "");
+ krb5_free_unparsed_name(ctx->context, principal);
+ }
+
+ /* Now that we know we're successful, we can store the context. */
+ pamret = pam_set_data(args->pamh, "pam_krb5", ctx, pamk5_context_destroy);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "cannot set context data");
+ pamk5_context_free(args);
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ set_context = true;
+
+ /*
+ * If we have an expired account or if we're not creating a ticket cache,
+ * we're done. Otherwise, store the obtained credentials in a temporary
+ * cache.
+ */
+ if (!args->config->no_ccache && !ctx->expired)
+ pamret = pamk5_cache_init_random(args, creds);
+
+done:
+ if (creds != NULL && ctx != NULL) {
+ krb5_free_cred_contents(ctx->context, creds);
+ free(creds);
+ }
+
+ /*
+ * Don't free our Kerberos context if we set a context, since the context
+ * will take care of that.
+ */
+ if (set_context)
+ args->ctx = NULL;
+
+ /*
+ * Clear the context on failure so that the account management module
+ * knows that we didn't authenticate with Kerberos. Only clear the
+ * context if we set it. Otherwise, we may be blowing away the context of
+ * a previous successful authentication.
+ */
+ if (pamret != PAM_SUCCESS) {
+ if (set_context)
+ pam_set_data(args->pamh, "pam_krb5", NULL, NULL);
+ else
+ pamk5_context_free(args);
+ }
+ return pamret;
+}
diff --git a/module/cache.c b/module/cache.c
new file mode 100644
index 000000000000..7acfef07b8eb
--- /dev/null
+++ b/module/cache.c
@@ -0,0 +1,185 @@
+/*
+ * Ticket cache initialization.
+ *
+ * Provides functions for creating ticket caches, used by pam_authenticate,
+ * pam_setcred, and pam_chauthtok after changing an expired password.
+ *
+ * Copyright 2005-2009, 2014, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Get the name of a cache. Takes the name of the environment variable that
+ * should be set to indicate which cache to use, either the permanent cache
+ * (KRB5CCNAME) or the temporary cache (PAM_KRB5CCNAME).
+ *
+ * Treat an empty environment variable setting the same as if the variable
+ * was not set, since on FreeBSD we can't delete the environment variable,
+ * only set it to an empty value.
+ */
+const char *
+pamk5_get_krb5ccname(struct pam_args *args, const char *key)
+{
+ const char *name;
+
+ /* When refreshing a cache, we need to try the regular environment. */
+ name = pam_getenv(args->pamh, key);
+ if (name == NULL || *name == '\0')
+ name = getenv(key);
+ if (name == NULL || *name == '\0')
+ return NULL;
+ else
+ return name;
+}
+
+
+/*
+ * Put the ticket cache information into the environment. Takes the path and
+ * the environment variable to set, since this is used both for the permanent
+ * cache (KRB5CCNAME) and the temporary cache (PAM_KRB5CCNAME). Returns a PAM
+ * status code.
+ */
+int
+pamk5_set_krb5ccname(struct pam_args *args, const char *name, const char *key)
+{
+ char *env_name = NULL;
+ int pamret;
+
+ if (asprintf(&env_name, "%s=%s", key, name) < 0) {
+ putil_crit(args, "asprintf failed: %s", strerror(errno));
+ pamret = PAM_BUF_ERR;
+ goto done;
+ }
+ pamret = pam_putenv(args->pamh, env_name);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "pam_putenv failed");
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ pamret = PAM_SUCCESS;
+
+done:
+ free(env_name);
+ return pamret;
+}
+
+
+/*
+ * Given the template for a ticket cache name, initialize that file securely
+ * mkstemp. Returns a PAM success or error code.
+ */
+int
+pamk5_cache_mkstemp(struct pam_args *args, char *template)
+{
+ int ccfd, oerrno;
+
+ ccfd = mkstemp(template);
+ if (ccfd < 0) {
+ oerrno = errno;
+ putil_crit(args, "mkstemp(\"%s\") failed: %s", template,
+ strerror(errno));
+ errno = oerrno;
+ return PAM_SERVICE_ERR;
+ }
+ close(ccfd);
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Given a cache name and the initial credentials, initialize the cache, store
+ * the credentials in that cache, and return a pointer to the new cache in the
+ * cache argument. Returns a PAM success or error code.
+ */
+int
+pamk5_cache_init(struct pam_args *args, const char *ccname, krb5_creds *creds,
+ krb5_ccache *cache)
+{
+ struct context *ctx;
+ int retval;
+
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL
+ || args->config->ctx->context == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+ retval = krb5_cc_resolve(ctx->context, ccname, cache);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot resolve ticket cache %s", ccname);
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+ retval = krb5_cc_initialize(ctx->context, *cache, ctx->princ);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot initialize ticket cache %s",
+ ccname);
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+ retval = krb5_cc_store_cred(ctx->context, *cache, creds);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot store credentials in %s", ccname);
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+
+done:
+ if (retval != PAM_SUCCESS && *cache != NULL) {
+ krb5_cc_destroy(ctx->context, *cache);
+ *cache = NULL;
+ }
+ return retval;
+}
+
+
+/*
+ * Initialize an internal ticket cache with a random name, store the given
+ * credentials in the cache, and store the cache in the context. Put the path
+ * in PAM_KRB5CCNAME where it can be picked up later by pam_setcred. Returns
+ * a PAM success or error code.
+ */
+int
+pamk5_cache_init_random(struct pam_args *args, krb5_creds *creds)
+{
+ char *cache_name = NULL;
+ const char *dir;
+ int pamret;
+
+ /* Store the obtained credentials in a temporary cache. */
+ dir = args->config->ccache_dir;
+ if (strncmp("FILE:", args->config->ccache_dir, strlen("FILE:")) == 0)
+ dir += strlen("FILE:");
+ if (asprintf(&cache_name, "%s/krb5cc_pam_XXXXXX", dir) < 0) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ return PAM_SERVICE_ERR;
+ }
+ pamret = pamk5_cache_mkstemp(args, cache_name);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ pamret =
+ pamk5_cache_init(args, cache_name, creds, &args->config->ctx->cache);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ putil_debug(args, "temporarily storing credentials in %s", cache_name);
+ pamret = pamk5_set_krb5ccname(args, cache_name, "PAM_KRB5CCNAME");
+
+done:
+ free(cache_name);
+ return pamret;
+}
diff --git a/module/context.c b/module/context.c
new file mode 100644
index 000000000000..bd90f51f5549
--- /dev/null
+++ b/module/context.c
@@ -0,0 +1,177 @@
+/*
+ * Manage context structure.
+ *
+ * The context structure is the internal state maintained by the pam-krb5
+ * module between calls to the various public interfaces.
+ *
+ * Copyright 2005-2009, 2014, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Create a new context and populate it with the user from PAM and the current
+ * Kerberos context. Set the default realm if one was configured.
+ */
+int
+pamk5_context_new(struct pam_args *args)
+{
+ struct context *ctx;
+ int retval;
+ PAM_CONST char *name;
+
+ ctx = calloc(1, sizeof(struct context));
+ if (ctx == NULL) {
+ retval = PAM_BUF_ERR;
+ goto done;
+ }
+ ctx->cache = NULL;
+ ctx->princ = NULL;
+ ctx->creds = NULL;
+ ctx->fast_cache = NULL;
+ ctx->context = args->ctx;
+ args->config->ctx = ctx;
+
+ /*
+ * This will prompt for the username if it's not already set (generally it
+ * will be). Otherwise, grab the saved username.
+ */
+ retval = pam_get_user(args->pamh, &name, NULL);
+ if (retval != PAM_SUCCESS || name == NULL) {
+ if (retval == PAM_CONV_AGAIN)
+ retval = PAM_INCOMPLETE;
+ else
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ctx->name = strdup(name);
+ args->user = ctx->name;
+
+ /* Set a default realm if one was configured. */
+ if (args->realm != NULL) {
+ retval = krb5_set_default_realm(ctx->context, args->realm);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot set default realm");
+ retval = PAM_SERVICE_ERR;
+ goto done;
+ }
+ }
+
+done:
+ if (ctx != NULL && retval != PAM_SUCCESS)
+ pamk5_context_free(args);
+ return retval;
+}
+
+
+/*
+ * Retrieve a context from the PAM data structures, returning failure if no
+ * context was present. Note that OpenSSH loses contexts between authenticate
+ * and setcred, so failure shouldn't always be fatal.
+ */
+int
+pamk5_context_fetch(struct pam_args *args)
+{
+ int pamret;
+
+ pamret = pam_get_data(args->pamh, "pam_krb5", (void *) &args->config->ctx);
+ if (pamret != PAM_SUCCESS)
+ args->config->ctx = NULL;
+ if (pamret == PAM_SUCCESS && args->config->ctx == NULL)
+ return PAM_SERVICE_ERR;
+ if (args->config->ctx != NULL)
+ args->user = args->config->ctx->name;
+ return pamret;
+}
+
+
+/*
+ * Free a context and all of the data that's stored in it. Normally this also
+ * includes destroying the ticket cache, but don't do this (just close it) if
+ * a flag was set to preserve it.
+ *
+ * This function is common code between pamk5_context_free (called internally
+ * by our code) and pamk5_context_destroy (called by PAM as a data callback).
+ */
+static void
+context_free(struct context *ctx, bool free_context)
+{
+ if (ctx == NULL)
+ return;
+ free(ctx->name);
+ if (ctx->context != NULL) {
+ if (ctx->princ != NULL)
+ krb5_free_principal(ctx->context, ctx->princ);
+ if (ctx->cache != NULL) {
+ if (ctx->dont_destroy_cache)
+ krb5_cc_close(ctx->context, ctx->cache);
+ else
+ krb5_cc_destroy(ctx->context, ctx->cache);
+ }
+ if (ctx->creds != NULL) {
+ krb5_free_cred_contents(ctx->context, ctx->creds);
+ free(ctx->creds);
+ }
+ if (free_context)
+ krb5_free_context(ctx->context);
+ }
+ if (ctx->fast_cache != NULL)
+ krb5_cc_destroy(ctx->context, ctx->fast_cache);
+ free(ctx);
+}
+
+
+/*
+ * Free the current context, used internally by pam-krb5 code. This is a
+ * wrapper around context_free that makes sure we don't destroy the Kerberos
+ * context if it's the same as the top-level context and handles other
+ * bookkeeping in the top-level pam_args struct.
+ */
+void
+pamk5_context_free(struct pam_args *args)
+{
+ if (args->config->ctx == NULL)
+ return;
+ if (args->user == args->config->ctx->name)
+ args->user = NULL;
+ context_free(args->config->ctx, args->ctx != args->config->ctx->context);
+ args->config->ctx = NULL;
+}
+
+
+/*
+ * The PAM callback to destroy the context stored in the PAM data structures.
+ */
+void
+pamk5_context_destroy(pam_handle_t *pamh UNUSED, void *data,
+ int pam_end_status)
+{
+ struct context *ctx = (struct context *) data;
+
+ /*
+ * Do not destroy the cache if the status contains PAM_DATA_SILENT, since
+ * in that case we may be in a child and the parent will still rely on
+ * underlying resources such as the ticket cache to exist.
+ */
+ if (PAM_DATA_SILENT != 0 && (pam_end_status & PAM_DATA_SILENT))
+ ctx->dont_destroy_cache = true;
+
+ /* The rest of the work is in context_free. */
+ if (ctx != NULL)
+ context_free(ctx, true);
+}
diff --git a/module/fast.c b/module/fast.c
new file mode 100644
index 000000000000..466199977fad
--- /dev/null
+++ b/module/fast.c
@@ -0,0 +1,288 @@
+/*
+ * Support for FAST (Flexible Authentication Secure Tunneling).
+ *
+ * FAST is a mechanism to protect Kerberos against password guessing attacks
+ * and provide other security improvements. It requires existing credentials
+ * to protect the initial preauthentication exchange. These can come either
+ * from a ticket cache for another principal or via anonymous PKINIT.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Contributions from Sam Hartman and Yair Yarom
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010, 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Initialize an internal anonymous ticket cache with a random name and store
+ * the resulting ticket cache in the ccache argument. Returns a Kerberos
+ * error code.
+ */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ANONYMOUS
+
+static krb5_error_code
+cache_init_anonymous(struct pam_args *args, krb5_ccache *ccache UNUSED)
+{
+ putil_debug(args, "not built with anonymous FAST support");
+ return KRB5KDC_ERR_BADOPTION;
+}
+
+#else /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ANONYMOUS */
+
+static krb5_error_code
+cache_init_anonymous(struct pam_args *args, krb5_ccache *ccache)
+{
+ krb5_context c = args->config->ctx->context;
+ krb5_error_code retval;
+ krb5_principal princ = NULL;
+ char *realm;
+ char *name = NULL;
+ krb5_creds creds;
+ bool creds_valid = false;
+ krb5_get_init_creds_opt *opts = NULL;
+
+ *ccache = NULL;
+ memset(&creds, 0, sizeof(creds));
+
+ /* Construct the anonymous principal name. */
+ retval = krb5_get_default_realm(c, &realm);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "cannot find realm for anonymous FAST");
+ return retval;
+ }
+ retval = krb5_build_principal_ext(
+ c, &princ, (unsigned int) strlen(realm), realm,
+ strlen(KRB5_WELLKNOWN_NAME), KRB5_WELLKNOWN_NAME,
+ strlen(KRB5_ANON_NAME), KRB5_ANON_NAME, NULL);
+ if (retval != 0) {
+ krb5_free_default_realm(c, realm);
+ putil_debug_krb5(args, retval, "cannot create anonymous principal");
+ return retval;
+ }
+ krb5_free_default_realm(c, realm);
+
+ /*
+ * Set up the credential cache the anonymous credentials. We use a
+ * memory cache whose name is based on the pointer value of our Kerberos
+ * context, since that should be unique among threads.
+ */
+ if (asprintf(&name, "MEMORY:%p", (void *) c) < 0) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ retval = errno;
+ goto done;
+ }
+ retval = krb5_cc_resolve(c, name, ccache);
+ if (retval != 0) {
+ putil_err_krb5(args, retval,
+ "cannot create anonymous FAST credential cache %s",
+ name);
+ goto done;
+ }
+
+ /* Obtain the credentials. */
+ retval = krb5_get_init_creds_opt_alloc(c, &opts);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot create FAST credential options");
+ goto done;
+ }
+ krb5_get_init_creds_opt_set_anonymous(opts, 1);
+ krb5_get_init_creds_opt_set_tkt_life(opts, 60);
+# ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_OUT_CCACHE
+ krb5_get_init_creds_opt_set_out_ccache(c, opts, *ccache);
+# endif
+ retval = krb5_get_init_creds_password(c, &creds, princ, NULL, NULL, NULL,
+ 0, NULL, opts);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval,
+ "cannot obtain anonymous credentials for FAST");
+ goto done;
+ }
+ creds_valid = true;
+
+ /*
+ * If set_out_ccache was available, we're done. Otherwise, we have to
+ * manually set up the ticket cache. Use the principal from the acquired
+ * credentials when initializing the ticket cache, since the realm will
+ * not match the realm of our input principal.
+ */
+# ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_OUT_CCACHE
+ retval = krb5_cc_initialize(c, *ccache, creds.client);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot initialize FAST ticket cache");
+ goto done;
+ }
+ retval = krb5_cc_store_cred(c, *ccache, &creds);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot store FAST credentials");
+ goto done;
+ }
+# endif /* !HAVE_KRB5_GET_INIT_CREDS_OPT_SET_OUT_CCACHE */
+
+done:
+ if (retval != 0 && *ccache != NULL) {
+ krb5_cc_destroy(c, *ccache);
+ *ccache = NULL;
+ }
+ if (princ != NULL)
+ krb5_free_principal(c, princ);
+ free(name);
+ if (opts != NULL)
+ krb5_get_init_creds_opt_free(c, opts);
+ if (creds_valid)
+ krb5_free_cred_contents(c, &creds);
+ return retval;
+}
+#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_ANONYMOUS */
+
+
+/*
+ * Attempt to use an existing ticket cache for FAST. Checks whether
+ * fast_ccache is set in the options and, if so, opens that cache and does
+ * some sanity checks, returning the cache name to use if everything checks
+ * out in newly allocated memory. Caller is responsible for freeing. If not,
+ * returns NULL.
+ */
+UNUSED static char *
+fast_setup_cache(struct pam_args *args)
+{
+ krb5_context c = args->config->ctx->context;
+ krb5_error_code retval;
+ krb5_principal princ;
+ krb5_ccache ccache;
+ char *result;
+ const char *cache = args->config->fast_ccache;
+
+ if (cache == NULL)
+ return NULL;
+ retval = krb5_cc_resolve(c, cache, &ccache);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "cannot open FAST ccache %s", cache);
+ return NULL;
+ }
+ retval = krb5_cc_get_principal(c, ccache, &princ);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval,
+ "failed to get principal from FAST"
+ " ccache %s",
+ cache);
+ krb5_cc_close(c, ccache);
+ return NULL;
+ } else {
+ krb5_free_principal(c, princ);
+ krb5_cc_close(c, ccache);
+ result = strdup(cache);
+ if (result == NULL)
+ putil_crit(args, "strdup failure: %s", strerror(errno));
+ return result;
+ }
+}
+
+
+/*
+ * Attempt to use an anonymous ticket cache for FAST. Checks whether
+ * anon_fast is set in the options and, if so, opens that cache and does some
+ * sanity checks, returning the cache name to use if everything checks out in
+ * newly allocated memory. Caller is responsible for freeing. If not,
+ * returns NULL.
+ *
+ * If successful, store the anonymous FAST cache in the context where it will
+ * be freed following authentication.
+ */
+UNUSED static char *
+fast_setup_anon(struct pam_args *args)
+{
+ krb5_context c = args->config->ctx->context;
+ krb5_error_code retval;
+ krb5_ccache ccache;
+ char *cache, *result;
+
+ if (!args->config->anon_fast)
+ return NULL;
+ retval = cache_init_anonymous(args, &ccache);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "skipping anonymous FAST");
+ return NULL;
+ }
+ retval = krb5_cc_get_full_name(c, ccache, &cache);
+ if (retval != 0) {
+ putil_debug_krb5(args, retval,
+ "cannot get name of anonymous FAST"
+ " credential cache");
+ krb5_cc_destroy(c, ccache);
+ return NULL;
+ }
+ result = strdup(cache);
+ if (result == NULL) {
+ putil_crit(args, "strdup failure: %s", strerror(errno));
+ krb5_cc_destroy(c, ccache);
+ }
+ krb5_free_string(c, cache);
+ putil_debug(args, "anonymous authentication for FAST succeeded");
+ if (args->config->ctx->fast_cache != NULL)
+ krb5_cc_destroy(c, args->config->ctx->fast_cache);
+ args->config->ctx->fast_cache = ccache;
+ return result;
+}
+
+
+/*
+ * Set initial credential options for FAST if support is available.
+ *
+ * If fast_ccache is set, we try to use that ticket cache first. Open it and
+ * read the principal from it first to ensure that the cache exists and
+ * contains credentials. If that fails, skip setting the FAST cache.
+ *
+ * If anon_fast is set and fast_ccache is not or is skipped for the reasons
+ * described above, try to obtain anonymous credentials and then use them as
+ * FAST armor.
+ *
+ * Note that this function cannot fail. If anything about FAST setup doesn't
+ * work, we continue without FAST.
+ */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME
+
+void
+pamk5_fast_setup(struct pam_args *args UNUSED,
+ krb5_get_init_creds_opt *opts UNUSED)
+{
+}
+
+#else /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME */
+
+void
+pamk5_fast_setup(struct pam_args *args, krb5_get_init_creds_opt *opts)
+{
+ krb5_context c = args->config->ctx->context;
+ krb5_error_code retval;
+ char *cache;
+
+ /* First try to use fast_ccache, and then fall back on anon_fast. */
+ cache = fast_setup_cache(args);
+ if (cache == NULL)
+ cache = fast_setup_anon(args);
+ if (cache == NULL)
+ return;
+
+ /* We have a valid FAST ticket cache. Set the option. */
+ retval = krb5_get_init_creds_opt_set_fast_ccache_name(c, opts, cache);
+ if (retval != 0)
+ putil_err_krb5(args, retval, "failed to set FAST ccache");
+ else
+ putil_debug(args, "setting FAST credential cache to %s", cache);
+ free(cache);
+}
+
+#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME */
diff --git a/module/internal.h b/module/internal.h
new file mode 100644
index 000000000000..f3d832a17248
--- /dev/null
+++ b/module/internal.h
@@ -0,0 +1,261 @@
+/*
+ * Internal prototypes and structures for pam-krb5.
+ *
+ * Copyright 2005-2009, 2014, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011, 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#ifndef INTERNAL_H
+#define INTERNAL_H 1
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/macros.h>
+#include <portable/pam.h>
+
+#include <stdarg.h>
+#include <syslog.h>
+
+/* Forward declarations to avoid unnecessary includes. */
+struct pam_args;
+struct passwd;
+struct vector;
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+/*
+ * An authentication context, including all the data we want to preserve
+ * across calls to the public entry points. This context is stored in the PAM
+ * state and a pointer to it is stored in the pam_args struct that is passed
+ * as the first argument to most internal functions.
+ */
+struct context {
+ char *name; /* Username being authenticated. */
+ krb5_context context; /* Kerberos context. */
+ krb5_ccache cache; /* Active credential cache, if any. */
+ krb5_principal princ; /* Principal being authenticated. */
+ int expired; /* If set, account was expired. */
+ int dont_destroy_cache; /* If set, don't destroy cache on shutdown. */
+ int initialized; /* If set, ticket cache initialized. */
+ krb5_creds *creds; /* Credentials for password changing. */
+ krb5_ccache fast_cache; /* Temporary credential cache for FAST. */
+};
+
+/*
+ * The global structure holding our arguments, both from krb5.conf and from
+ * the PAM configuration. Filled in by pamk5_init and stored in the pam_args
+ * struct passed as a first argument to most internal functions. Sort by
+ * documentation order.
+ */
+struct pam_config {
+ /* Authorization. */
+ char *alt_auth_map; /* An sprintf pattern to map principals. */
+ bool force_alt_auth; /* Alt principal must be used if it exists. */
+ bool ignore_k5login; /* Don't check .k5login files. */
+ bool ignore_root; /* Skip authentication for root. */
+ long minimum_uid; /* Ignore users below this UID. */
+ bool only_alt_auth; /* Alt principal must be used. */
+ bool search_k5login; /* Try password with each line of .k5login. */
+
+ /* Kerberos behavior. */
+ char *fast_ccache; /* Cache containing armor ticket. */
+ bool anon_fast; /* sets up an anonymous fast armor cache */
+ bool forwardable; /* Obtain forwardable tickets. */
+ char *keytab; /* Keytab for credential validation. */
+ char *realm; /* Default realm for Kerberos. */
+ krb5_deltat renew_lifetime; /* Renewable lifetime of credentials. */
+ krb5_deltat ticket_lifetime; /* Lifetime of credentials. */
+ char *user_realm; /* Default realm for user principals. */
+
+ /* PAM behavior. */
+ bool clear_on_fail; /* Delete saved password on change failure. */
+ bool debug; /* Log debugging information. */
+ bool defer_pwchange; /* Defer expired account fail to account. */
+ bool fail_pwchange; /* Treat expired password as auth failure. */
+ bool force_pwchange; /* Change expired passwords in auth. */
+ bool no_update_user; /* Don't update PAM_USER with local name. */
+ bool silent; /* Suppress text and errors (PAM_SILENT). */
+ char *trace; /* File name for trace logging. */
+
+ /* PKINIT. */
+ char *pkinit_anchors; /* Trusted certificates, usually per realm. */
+ bool pkinit_prompt; /* Prompt user to insert smart card. */
+ char *pkinit_user; /* User ID to pass to PKINIT. */
+ struct vector *preauth_opt; /* Preauth options. */
+ bool try_pkinit; /* Attempt PKINIT, fall back to password. */
+ bool use_pkinit; /* Require PKINIT. */
+
+ /* Prompting. */
+ char *banner; /* Addition to password changing prompts. */
+ bool expose_account; /* Display principal in password prompts. */
+ bool force_first_pass; /* Require a previous password be stored. */
+ bool no_prompt; /* Let Kerberos handle password prompting. */
+ bool prompt_principal; /* Prompt for the Kerberos principal. */
+ bool try_first_pass; /* Try the previously entered password. */
+ bool use_authtok; /* Use the stored new password for changes. */
+ bool use_first_pass; /* Always use the previous password. */
+
+ /* Ticket caches. */
+ char *ccache; /* Path to write ticket cache to. */
+ char *ccache_dir; /* Directory for ticket cache. */
+ bool no_ccache; /* Don't create a ticket cache. */
+ bool retain_after_close; /* Don't destroy the cache on session end. */
+
+ /* The authentication context, which bundles together Kerberos data. */
+ struct context *ctx;
+};
+
+/* Default to a hidden visibility for all internal functions. */
+#pragma GCC visibility push(hidden)
+
+/* Parse the PAM flags, arguments, and krb5.conf and fill out pam_args. */
+struct pam_args *pamk5_init(pam_handle_t *, int flags, int, const char **);
+
+/* Free the pam_args struct when we're done. */
+void pamk5_free(struct pam_args *);
+
+/*
+ * The underlying functions between several of the major PAM interfaces.
+ */
+int pamk5_account(struct pam_args *);
+int pamk5_authenticate(struct pam_args *);
+
+/*
+ * The underlying function below pam_sm_chauthtok. If the second argument is
+ * true, we're doing the preliminary check and shouldn't actually change the
+ * password.
+ */
+int pamk5_password(struct pam_args *, bool only_auth);
+
+/*
+ * Create or refresh the user's ticket cache. This is the underlying function
+ * beneath pam_sm_setcred and pam_sm_open_session.
+ */
+int pamk5_setcred(struct pam_args *, bool refresh);
+
+/*
+ * Authenticate the user. Prompts for the password as needed and obtains
+ * tickets for in_tkt_service, krbtgt/<realm> by default. Stores the initial
+ * credentials in the final argument, allocating a new krb5_creds structure.
+ * If possible, the initial credentials are verified by checking them against
+ * the local system key.
+ */
+int pamk5_password_auth(struct pam_args *, const char *service, krb5_creds **);
+
+/*
+ * Prompt the user for a new password, twice so that they can confirm. Sets
+ * PAM_AUTHTOK and puts the new password in newly allocated memory in pass if
+ * it's not NULL.
+ */
+int pamk5_password_prompt(struct pam_args *, char **pass);
+
+/*
+ * Change the user's password. Prompts for the current password as needed and
+ * the new password. If the second argument is true, only obtains the
+ * necessary credentials without changing anything.
+ */
+int pamk5_password_change(struct pam_args *, bool only_auth);
+
+/*
+ * Generic conversation function to display messages or get information from
+ * the user. Takes the message, the message type, and a place to put the
+ * result of a prompt.
+ */
+int pamk5_conv(struct pam_args *, const char *, int, char **);
+
+/*
+ * Function specifically for getting a password. Takes a prefix (if non-NULL,
+ * args->banner will also be prepended) and a pointer into which to store the
+ * password. The password must be freed by the caller.
+ */
+int pamk5_get_password(struct pam_args *, const char *, char **);
+
+/* Prompting function for the Kerberos libraries. */
+krb5_error_code pamk5_prompter_krb5(krb5_context, void *data, const char *name,
+ const char *banner, int, krb5_prompt *);
+
+/* Prompting function that doesn't allow passwords. */
+krb5_error_code pamk5_prompter_krb5_no_password(krb5_context, void *data,
+ const char *name,
+ const char *banner, int,
+ krb5_prompt *);
+
+/* Check the user with krb5_kuserok or the configured equivalent. */
+int pamk5_authorized(struct pam_args *);
+
+/* Returns true if we should ignore this user (root or low UID). */
+int pamk5_should_ignore(struct pam_args *, PAM_CONST char *);
+
+/*
+ * alt_auth_map support.
+ *
+ * pamk5_map_principal attempts to map the user to a Kerberos principal
+ * according to alt_auth_map. Returns 0 on success, storing the mapped
+ * principal name in newly allocated memory in principal. The caller is
+ * responsiple for freeing. Returns an errno value on any error.
+ *
+ * pamk5_alt_auth attempts an authentication to the given service with the
+ * given options and password and returns a Kerberos error code. On success,
+ * the new credentials are stored in krb5_creds.
+ *
+ * pamk5_alt_auth_verify verifies that Kerberos credentials are authorized to
+ * access the account given the configured alt_auth_map and is meant to be
+ * called from pamk5_authorized. It returns a PAM status code.
+ */
+int pamk5_map_principal(struct pam_args *, const char *username,
+ char **principal);
+krb5_error_code pamk5_alt_auth(struct pam_args *, const char *service,
+ krb5_get_init_creds_opt *, const char *pass,
+ krb5_creds *);
+int pamk5_alt_auth_verify(struct pam_args *);
+
+/* FAST support. Set up FAST protection of authentication. */
+void pamk5_fast_setup(struct pam_args *, krb5_get_init_creds_opt *);
+
+/* Context management. */
+int pamk5_context_new(struct pam_args *);
+int pamk5_context_fetch(struct pam_args *);
+void pamk5_context_free(struct pam_args *);
+void pamk5_context_destroy(pam_handle_t *, void *data, int pam_end_status);
+
+/* Get and set environment variables for the ticket cache. */
+const char *pamk5_get_krb5ccname(struct pam_args *, const char *key);
+int pamk5_set_krb5ccname(struct pam_args *, const char *, const char *key);
+
+/*
+ * Create a ticket cache file securely given a mkstemp template. Modifies
+ * template in place to store the name of the created file.
+ */
+int pamk5_cache_mkstemp(struct pam_args *, char *template);
+
+/*
+ * Create a ticket cache and initialize it with the provided credentials,
+ * returning the new cache in the last argument
+ */
+int pamk5_cache_init(struct pam_args *, const char *ccname, krb5_creds *,
+ krb5_ccache *);
+
+/*
+ * Create a ticket cache with a random path, initialize it with the provided
+ * credentials, store it in the context, and put the path into PAM_KRB5CCNAME.
+ */
+int pamk5_cache_init_random(struct pam_args *, krb5_creds *);
+
+/*
+ * Compatibility functions. Depending on whether pam_krb5 is built with MIT
+ * Kerberos or Heimdal, appropriate implementations for the Kerberos
+ * implementation will be provided.
+ */
+krb5_error_code pamk5_compat_set_realm(struct pam_config *, const char *);
+void pamk5_compat_free_realm(struct pam_config *);
+
+/* Undo default visibility change. */
+#pragma GCC visibility pop
+
+#endif /* !INTERNAL_H */
diff --git a/module/options.c b/module/options.c
new file mode 100644
index 000000000000..f2c3791d895a
--- /dev/null
+++ b/module/options.c
@@ -0,0 +1,259 @@
+/*
+ * Option handling for pam-krb5.
+ *
+ * Responsible for initializing the args struct that's passed to nearly all
+ * internal functions. Retrieves configuration information from krb5.conf and
+ * parses the PAM configuration.
+ *
+ * Copyright 2005-2010, 2014, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+#include <pam-util/options.h>
+#include <pam-util/vector.h>
+
+/* Our option definition. Must be sorted. */
+#define K(name) (#name), offsetof(struct pam_config, name)
+/* clang-format off */
+static const struct option options[] = {
+ { K(alt_auth_map), true, STRING (NULL) },
+ { K(anon_fast), true, BOOL (false) },
+ { K(banner), true, STRING ("Kerberos") },
+ { K(ccache), true, STRING (NULL) },
+ { K(ccache_dir), true, STRING ("FILE:/tmp") },
+ { K(clear_on_fail), true, BOOL (false) },
+ { K(debug), true, BOOL (false) },
+ { K(defer_pwchange), true, BOOL (false) },
+ { K(expose_account), true, BOOL (false) },
+ { K(fail_pwchange), true, BOOL (false) },
+ { K(fast_ccache), true, STRING (NULL) },
+ { K(force_alt_auth), true, BOOL (false) },
+ { K(force_first_pass), false, BOOL (false) },
+ { K(force_pwchange), true, BOOL (false) },
+ { K(forwardable), true, BOOL (false) },
+ { K(ignore_k5login), true, BOOL (false) },
+ { K(ignore_root), true, BOOL (false) },
+ { K(keytab), true, STRING (NULL) },
+ { K(minimum_uid), true, NUMBER (0) },
+ { K(no_ccache), false, BOOL (false) },
+ { K(no_prompt), true, BOOL (false) },
+ { K(no_update_user), true, BOOL (false) },
+ { K(only_alt_auth), true, BOOL (false) },
+ { K(pkinit_anchors), true, STRING (NULL) },
+ { K(pkinit_prompt), true, BOOL (false) },
+ { K(pkinit_user), true, STRING (NULL) },
+ { K(preauth_opt), true, LIST (NULL) },
+ { K(prompt_principal), true, BOOL (false) },
+ { K(realm), false, STRING (NULL) },
+ { K(renew_lifetime), true, TIME (0) },
+ { K(retain_after_close), true, BOOL (false) },
+ { K(search_k5login), true, BOOL (false) },
+ { K(silent), false, BOOL (false) },
+ { K(ticket_lifetime), true, TIME (0) },
+ { K(trace), false, STRING (NULL) },
+ { K(try_first_pass), false, BOOL (false) },
+ { K(try_pkinit), true, BOOL (false) },
+ { K(use_authtok), false, BOOL (false) },
+ { K(use_first_pass), false, BOOL (false) },
+ { K(use_pkinit), true, BOOL (false) },
+ { K(user_realm), true, STRING (NULL) },
+};
+/* clang-format on */
+static const size_t optlen = sizeof(options) / sizeof(options[0]);
+
+
+/*
+ * Allocate a new struct pam_args and initialize its data members, including
+ * parsing the arguments and getting settings from krb5.conf. Check the
+ * resulting options for consistency.
+ */
+struct pam_args *
+pamk5_init(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ int i;
+ struct pam_args *args;
+ struct pam_config *config = NULL;
+
+ args = putil_args_new(pamh, flags);
+ if (args == NULL) {
+ return NULL;
+ }
+ config = calloc(1, sizeof(struct pam_config));
+ if (config == NULL) {
+ goto nomem;
+ }
+ args->config = config;
+
+ /*
+ * Do an initial scan to see if the realm is already set in our options.
+ * If so, make sure that's set before we start loading option values,
+ * since it affects what comes out of krb5.conf.
+ *
+ * We will then ignore args->config->realm, set later by option parsing,
+ * in favor of using args->realm extracted here. However, the latter must
+ * exist to avoid throwing unknown option errors.
+ */
+ for (i = 0; i < argc; i++) {
+ if (strncmp(argv[i], "realm=", 6) != 0)
+ continue;
+ free(args->realm);
+ args->realm = strdup(&argv[i][strlen("realm=")]);
+ if (args->realm == NULL)
+ goto nomem;
+ }
+
+ if (!putil_args_defaults(args, options, optlen)) {
+ free(config);
+ putil_args_free(args);
+ return NULL;
+ }
+ if (!putil_args_krb5(args, "pam", options, optlen)) {
+ goto fail;
+ }
+ if (!putil_args_parse(args, argc, argv, options, optlen)) {
+ goto fail;
+ }
+ if (config->debug) {
+ args->debug = true;
+ }
+ if (config->silent) {
+ args->silent = true;
+ }
+
+ /* An empty banner should be treated the same as not having one. */
+ if (config->banner != NULL && config->banner[0] == '\0') {
+ free(config->banner);
+ config->banner = NULL;
+ }
+
+ /* Sanity-check try_first_pass, use_first_pass, and force_first_pass. */
+ if (config->force_first_pass && config->try_first_pass) {
+ putil_err(args, "force_first_pass set, ignoring try_first_pass");
+ config->try_first_pass = 0;
+ }
+ if (config->force_first_pass && config->use_first_pass) {
+ putil_err(args, "force_first_pass set, ignoring use_first_pass");
+ config->use_first_pass = 0;
+ }
+ if (config->use_first_pass && config->try_first_pass) {
+ putil_err(args, "use_first_pass set, ignoring try_first_pass");
+ config->try_first_pass = 0;
+ }
+
+ /*
+ * Don't set expose_account if we're using search_k5login. The user will
+ * get a principal formed from the account into which they're logging in,
+ * which isn't the password they'll use (that's the whole point of
+ * search_k5login).
+ */
+ if (config->search_k5login) {
+ config->expose_account = 0;
+ }
+
+ /* UIDs are unsigned on some systems. */
+ if (config->minimum_uid < 0) {
+ config->minimum_uid = 0;
+ }
+
+ /*
+ * Warn if PKINIT options were set and PKINIT isn't supported. The MIT
+ * method (krb5_get_init_creds_opt_set_pa) can't support use_pkinit.
+ */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT
+# ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PA
+ if (config->try_pkinit) {
+ putil_err(args, "try_pkinit requested but PKINIT not available");
+ } else if (config->use_pkinit) {
+ putil_err(args, "use_pkinit requested but PKINIT not available");
+ }
+# endif
+# ifndef HAVE_KRB5_GET_PROMPT_TYPES
+ if (config->use_pkinit) {
+ putil_err(args, "use_pkinit requested but PKINIT cannot be enforced");
+ }
+# endif
+#endif
+
+ /* Warn if the FAST option was set and FAST isn't supported. */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME
+ if (config->fast_ccache || config->anon_fast) {
+ putil_err(args, "fast_ccache or anon_fast requested but FAST not"
+ " supported by Kerberos libraries");
+ }
+#endif
+
+ /* If tracing was requested enable it if possible. */
+#ifdef HAVE_KRB5_SET_TRACE_FILENAME
+ if (config->trace != NULL) {
+ krb5_error_code retval;
+
+ retval = krb5_set_trace_filename(args->ctx, config->trace);
+ if (retval == 0)
+ putil_debug(args, "enabled trace logging to %s", config->trace);
+ else
+ putil_err_krb5(args, retval, "cannot enable trace logging to %s",
+ config->trace);
+ }
+#else
+ if (config->trace != NULL) {
+ putil_err(args, "trace logging requested but not supported");
+ }
+#endif
+
+ return args;
+
+nomem:
+ putil_crit(args, "cannot allocate memory: %s", strerror(errno));
+ free(config);
+ putil_args_free(args);
+ return NULL;
+
+fail:
+ pamk5_free(args);
+ return NULL;
+}
+
+
+/*
+ * Free the allocated args struct and any memory it points to.
+ */
+void
+pamk5_free(struct pam_args *args)
+{
+ struct pam_config *config;
+
+ if (args == NULL)
+ return;
+ config = args->config;
+ if (config != NULL) {
+ free(config->alt_auth_map);
+ free(config->banner);
+ free(config->ccache);
+ free(config->ccache_dir);
+ free(config->fast_ccache);
+ free(config->keytab);
+ free(config->pkinit_anchors);
+ free(config->pkinit_user);
+ vector_free(config->preauth_opt);
+ free(config->realm);
+ free(config->trace);
+ free(config->user_realm);
+ free(args->config);
+ args->config = NULL;
+ }
+ putil_args_free(args);
+}
diff --git a/module/pam_krb5.map b/module/pam_krb5.map
new file mode 100644
index 000000000000..b187908ee26a
--- /dev/null
+++ b/module/pam_krb5.map
@@ -0,0 +1,11 @@
+{
+ global:
+ pam_sm_acct_mgmt;
+ pam_sm_authenticate;
+ pam_sm_chauthtok;
+ pam_sm_close_session;
+ pam_sm_open_session;
+ pam_sm_setcred;
+ local:
+ *;
+};
diff --git a/module/pam_krb5.sym b/module/pam_krb5.sym
new file mode 100644
index 000000000000..1e7fc6b967c9
--- /dev/null
+++ b/module/pam_krb5.sym
@@ -0,0 +1,6 @@
+pam_sm_acct_mgmt
+pam_sm_authenticate
+pam_sm_chauthtok
+pam_sm_close_session
+pam_sm_open_session
+pam_sm_setcred
diff --git a/module/password.c b/module/password.c
new file mode 100644
index 000000000000..c1371234fa07
--- /dev/null
+++ b/module/password.c
@@ -0,0 +1,401 @@
+/*
+ * Kerberos password changing.
+ *
+ * Copyright 2005-2009, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Get the new password. Store it in PAM_AUTHTOK if we obtain it and verify
+ * it successfully and return it in the pass parameter. If pass is set to
+ * NULL, only store the new password in PAM_AUTHTOK.
+ *
+ * Returns a PAM error code, usually either PAM_AUTHTOK_ERR or PAM_SUCCESS.
+ */
+int
+pamk5_password_prompt(struct pam_args *args, char **pass)
+{
+ int pamret = PAM_AUTHTOK_ERR;
+ char *pass1 = NULL;
+ char *pass2;
+ PAM_CONST void *tmp;
+
+ /* Use the password from a previous module, if so configured. */
+ if (pass != NULL)
+ *pass = NULL;
+ if (args->config->use_authtok) {
+ pamret = pam_get_item(args->pamh, PAM_AUTHTOK, &tmp);
+ if (tmp == NULL) {
+ putil_debug_pam(args, pamret, "no stored password");
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ if (strlen(tmp) > PAM_MAX_RESP_SIZE - 1) {
+ putil_debug(args, "rejecting password longer than %d",
+ PAM_MAX_RESP_SIZE - 1);
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ pass1 = strdup((const char *) tmp);
+ }
+
+ /* Prompt for the new password if necessary. */
+ if (pass1 == NULL) {
+ pamret = pamk5_get_password(args, "Enter new", &pass1);
+ if (pamret != PAM_SUCCESS) {
+ putil_debug_pam(args, pamret, "error getting new password");
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ if (strlen(pass1) > PAM_MAX_RESP_SIZE - 1) {
+ putil_debug(args, "rejecting password longer than %d",
+ PAM_MAX_RESP_SIZE - 1);
+ pamret = PAM_AUTHTOK_ERR;
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ goto done;
+ }
+ pamret = pamk5_get_password(args, "Retype new", &pass2);
+ if (pamret != PAM_SUCCESS) {
+ putil_debug_pam(args, pamret, "error getting new password");
+ pamret = PAM_AUTHTOK_ERR;
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ goto done;
+ }
+ if (strcmp(pass1, pass2) != 0) {
+ putil_debug(args, "new passwords don't match");
+ pamk5_conv(args, "Passwords don't match", PAM_ERROR_MSG, NULL);
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ explicit_bzero(pass2, strlen(pass2));
+ free(pass2);
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ explicit_bzero(pass2, strlen(pass2));
+ free(pass2);
+
+ /* Save the new password for other modules. */
+ pamret = pam_set_item(args->pamh, PAM_AUTHTOK, pass1);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "error storing password");
+ pamret = PAM_AUTHTOK_ERR;
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ goto done;
+ }
+ }
+ if (pass != NULL)
+ *pass = pass1;
+ else {
+ explicit_bzero(pass1, strlen(pass1));
+ free(pass1);
+ }
+
+done:
+ return pamret;
+}
+
+
+/*
+ * We've obtained credentials for the password changing interface and gotten
+ * the new password, so do the work of actually changing the password.
+ */
+static int
+change_password(struct pam_args *args, const char *pass)
+{
+ struct context *ctx;
+ int retval = PAM_SUCCESS;
+ int result_code;
+ krb5_data result_code_string, result_string;
+ const char *message;
+
+ /* Sanity check. */
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL
+ || args->config->ctx->creds == NULL)
+ return PAM_AUTHTOK_ERR;
+ ctx = args->config->ctx;
+
+ /*
+ * The actual change.
+ *
+ * There are two password protocols in use: the change password protocol,
+ * which doesn't allow specification of the principal, and the newer set
+ * password protocol, which does. For our purposes, either will do.
+ *
+ * Both Heimdal and MIT provide krb5_set_password. With Heimdal,
+ * krb5_change_password is deprecated and krb5_set_password tries both
+ * protocols in turn, so will work with new and old servers. With MIT,
+ * krb5_set_password will use the old protocol if the principal is NULL
+ * and the new protocol if it is not.
+ *
+ * We would like to just use krb5_set_password with a NULL principal
+ * argument, but Heimdal 1.5 uses the default principal for the local user
+ * rather than the principal from the credentials, so we need to pass in a
+ * principal for Heimdal. So we're stuck with an #ifdef.
+ */
+#ifdef HAVE_KRB5_MIT
+ retval =
+ krb5_set_password(ctx->context, ctx->creds, (char *) pass, NULL,
+ &result_code, &result_code_string, &result_string);
+#else
+ retval =
+ krb5_set_password(ctx->context, ctx->creds, (char *) pass, ctx->princ,
+ &result_code, &result_code_string, &result_string);
+#endif
+
+ /* Everything from here on is just handling diagnostics and output. */
+ if (retval != 0) {
+ putil_debug_krb5(args, retval, "krb5_change_password failed");
+ message = krb5_get_error_message(ctx->context, retval);
+ pamk5_conv(args, message, PAM_ERROR_MSG, NULL);
+ krb5_free_error_message(ctx->context, message);
+ retval = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ if (result_code != 0) {
+ char *output;
+ int status;
+
+ putil_debug(args, "krb5_change_password: %s",
+ (char *) result_code_string.data);
+ retval = PAM_AUTHTOK_ERR;
+ status =
+ asprintf(&output, "%.*s%s%.*s", (int) result_code_string.length,
+ (char *) result_code_string.data,
+ result_string.length == 0 ? "" : ": ",
+ (int) result_string.length, (char *) result_string.data);
+ if (status < 0)
+ putil_crit(args, "asprintf failed: %s", strerror(errno));
+ else {
+ pamk5_conv(args, output, PAM_ERROR_MSG, NULL);
+ free(output);
+ }
+ }
+ krb5_free_data_contents(ctx->context, &result_string);
+ krb5_free_data_contents(ctx->context, &result_code_string);
+
+done:
+ /*
+ * On failure, when clear_on_fail is set, we set the new password to NULL
+ * so that subsequent password change PAM modules configured with
+ * use_authtok will also fail. Otherwise, since the order of the stack is
+ * fixed once the pre-check function runs, subsequent modules would
+ * continue even when we failed.
+ */
+ if (retval != PAM_SUCCESS && args->config->clear_on_fail) {
+ if (pam_set_item(args->pamh, PAM_AUTHTOK, NULL))
+ putil_err(args, "error clearing password");
+ }
+ return retval;
+}
+
+
+/*
+ * Change a user's password. Returns a PAM status code for success or
+ * failure. This does the work of pam_sm_chauthtok, but also needs to be
+ * called from pam_sm_authenticate if we're working around a library that
+ * can't handle password change during authentication.
+ *
+ * If the second argument is true, only do the authentication without actually
+ * doing the password change (PAM_PRELIM_CHECK).
+ */
+int
+pamk5_password_change(struct pam_args *args, bool only_auth)
+{
+ struct context *ctx = args->config->ctx;
+ int pamret = PAM_SUCCESS;
+ char *pass = NULL;
+
+ /*
+ * Authenticate to the password changing service using the old password.
+ */
+ if (ctx->creds == NULL) {
+ pamret = pamk5_password_auth(args, "kadmin/changepw", &ctx->creds);
+ if (pamret == PAM_SERVICE_ERR || pamret == PAM_AUTH_ERR)
+ pamret = PAM_AUTHTOK_RECOVER_ERR;
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ }
+
+ /*
+ * Now, get the new password and change it unless we're just doing the
+ * first check.
+ */
+ if (only_auth)
+ goto done;
+ pamret = pamk5_password_prompt(args, &pass);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ pamret = change_password(args, pass);
+ if (pamret == PAM_SUCCESS)
+ pam_syslog(args->pamh, LOG_INFO, "user %s changed Kerberos password",
+ ctx->name);
+
+done:
+ if (pass != NULL) {
+ explicit_bzero(pass, strlen(pass));
+ free(pass);
+ }
+ return pamret;
+}
+
+
+/*
+ * The function underlying the main PAM interface for password changing.
+ * Performs preliminary checks, user notification, and any reauthentication
+ * that's required.
+ *
+ * If the second argument is true, only do the authentication without actually
+ * doing the password change (PAM_PRELIM_CHECK).
+ */
+int
+pamk5_password(struct pam_args *args, bool only_auth)
+{
+ struct context *ctx = NULL;
+ int pamret, status;
+ PAM_CONST char *user;
+ char *pass = NULL;
+ bool set_context = false;
+
+ /*
+ * Check whether we should ignore this user.
+ *
+ * If we do ignore this user, and we're not in the preliminary check
+ * phase, still prompt the user for the new password, but suppress our
+ * banner. This is a little strange, but it allows another module to be
+ * stacked behind pam-krb5 with use_authtok and have it still work for
+ * ignored users.
+ *
+ * We ignore the return status when prompting for the new password in this
+ * case. The worst thing that can happen is to fail to get the password,
+ * in which case the other module will fail (or might even not care).
+ */
+ if (args->config->ignore_root || args->config->minimum_uid > 0) {
+ status = pam_get_user(args->pamh, &user, NULL);
+ if (status == PAM_SUCCESS && pamk5_should_ignore(args, user)) {
+ if (!only_auth) {
+ if (args->config->banner != NULL) {
+ free(args->config->banner);
+ args->config->banner = NULL;
+ }
+ pamk5_password_prompt(args, NULL);
+ }
+ pamret = PAM_IGNORE;
+ goto done;
+ }
+ }
+
+ /*
+ * If we weren't able to find an existing context to use, we're going
+ * into this fresh and need to create a new context.
+ */
+ if (args->config->ctx == NULL) {
+ pamret = pamk5_context_new(args);
+ if (pamret != PAM_SUCCESS) {
+ putil_debug_pam(args, pamret, "creating context failed");
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ pamret = pam_set_data(args->pamh, "pam_krb5", args->config->ctx,
+ pamk5_context_destroy);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "cannot set context data");
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ set_context = true;
+ }
+ ctx = args->config->ctx;
+
+ /*
+ * Tell the user what's going on if we're handling an expiration, but not
+ * if we were configured to use the same password as an earlier module in
+ * the stack. The correct behavior here is not clear (what if the
+ * Kerberos password expired but the other one didn't?), but warning
+ * unconditionally leads to a strange message in the middle of doing the
+ * password change.
+ */
+ if (ctx->expired && ctx->creds == NULL)
+ if (!args->config->force_first_pass && !args->config->use_first_pass)
+ pamk5_conv(args, "Password expired. You must change it now.",
+ PAM_TEXT_INFO, NULL);
+
+ /*
+ * Do the password change. This may only get tickets if we're doing the
+ * preliminary check phase.
+ */
+ pamret = pamk5_password_change(args, only_auth);
+ if (only_auth)
+ goto done;
+
+ /*
+ * If we were handling a forced password change for an expired password,
+ * now try to get a ticket cache with the new password. If this succeeds,
+ * clear the expired flag in the context.
+ */
+ if (pamret == PAM_SUCCESS && ctx->expired) {
+ krb5_creds *creds = NULL;
+ char *principal;
+ krb5_error_code retval;
+
+ putil_debug(args, "obtaining credentials with new password");
+ args->config->force_first_pass = 1;
+ pamret = pamk5_password_auth(args, NULL, &creds);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ retval = krb5_unparse_name(ctx->context, ctx->princ, &principal);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "krb5_unparse_name failed");
+ pam_syslog(args->pamh, LOG_INFO,
+ "user %s authenticated as UNKNOWN", ctx->name);
+ } else {
+ pam_syslog(args->pamh, LOG_INFO, "user %s authenticated as %s",
+ ctx->name, principal);
+ krb5_free_unparsed_name(ctx->context, principal);
+ }
+ ctx->expired = false;
+ pamret = pamk5_cache_init_random(args, creds);
+ krb5_free_cred_contents(ctx->context, creds);
+ free(creds);
+ }
+
+done:
+ if (pass != NULL) {
+ explicit_bzero(pass, strlen(pass));
+ free(pass);
+ }
+
+ /*
+ * Don't free our Kerberos context if we set a context, since the context
+ * will take care of that.
+ */
+ if (set_context)
+ args->ctx = NULL;
+
+ if (pamret != PAM_SUCCESS) {
+ if (pamret == PAM_SERVICE_ERR || pamret == PAM_AUTH_ERR)
+ pamret = PAM_AUTHTOK_ERR;
+ if (pamret == PAM_AUTHINFO_UNAVAIL)
+ pamret = PAM_AUTHTOK_ERR;
+ }
+ return pamret;
+}
diff --git a/module/prompting.c b/module/prompting.c
new file mode 100644
index 000000000000..506fb8fd2b22
--- /dev/null
+++ b/module/prompting.c
@@ -0,0 +1,481 @@
+/*
+ * Prompt users for information.
+ *
+ * Handles all interaction with the PAM conversation, either directly or
+ * indirectly through the Kerberos libraries.
+ *
+ * Copyright 2005-2007, 2009, 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <assert.h>
+#include <errno.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Build a password prompt.
+ *
+ * The default prompt is simply "Password:". Optionally, a string describing
+ * the type of password is passed in as prefix. In this case, the prompts is:
+ *
+ * <prefix> <banner> password:
+ *
+ * where <prefix> is the argument passed and <banner> is the value of
+ * args->banner (defaulting to "Kerberos").
+ *
+ * If args->config->expose_account is set, we append the principal name (taken
+ * from args->config->ctx->princ) before the colon, so the prompts are:
+ *
+ * Password for <principal>:
+ * <prefix> <banner> password for <principal>:
+ *
+ * Normally this is not done because it exposes the realm and possibly any
+ * username to principal mappings, plus may confuse some ssh clients if sshd
+ * passes the prompt back to the client.
+ *
+ * Returns newly-allocated memory or NULL on failure. The caller is
+ * responsible for freeing.
+ */
+static char *
+build_password_prompt(struct pam_args *args, const char *prefix)
+{
+ struct context *ctx = args->config->ctx;
+ char *principal = NULL;
+ const char *banner, *bspace;
+ char *prompt, *tmp;
+ bool expose_account;
+ krb5_error_code k5_errno;
+ int retval;
+
+ /* If we're exposing the account, format the principal name. */
+ if (args->config->expose_account || prefix != NULL)
+ if (ctx != NULL && ctx->context != NULL && ctx->princ != NULL) {
+ k5_errno = krb5_unparse_name(ctx->context, ctx->princ, &principal);
+ if (k5_errno != 0)
+ putil_debug_krb5(args, k5_errno, "krb5_unparse_name failed");
+ }
+
+ /* Build the part of the prompt without the principal name. */
+ if (prefix == NULL)
+ tmp = strdup("Password");
+ else {
+ banner = (args->config->banner == NULL) ? "" : args->config->banner;
+ bspace = (args->config->banner == NULL) ? "" : " ";
+ retval = asprintf(&tmp, "%s%s%s password", prefix, bspace, banner);
+ if (retval < 0)
+ tmp = NULL;
+ }
+ if (tmp == NULL)
+ goto fail;
+
+ /* Add the principal, if desired, and the colon and space. */
+ expose_account = args->config->expose_account && principal != NULL;
+ if (expose_account)
+ retval = asprintf(&prompt, "%s for %s: ", tmp, principal);
+ else
+ retval = asprintf(&prompt, "%s: ", tmp);
+ free(tmp);
+ if (retval < 0)
+ goto fail;
+
+ /* Clean up and return. */
+ if (principal != NULL)
+ krb5_free_unparsed_name(ctx->context, principal);
+ return prompt;
+
+fail:
+ if (principal != NULL)
+ krb5_free_unparsed_name(ctx->context, principal);
+ return NULL;
+}
+
+
+/*
+ * Prompt for a password.
+ *
+ * The entered password is stored in password. The memory is allocated by the
+ * application and returned as part of the PAM conversation. It must be freed
+ * by the caller.
+ *
+ * Returns a PAM success or error code.
+ */
+int
+pamk5_get_password(struct pam_args *args, const char *prefix, char **password)
+{
+ char *prompt;
+ int retval;
+
+ prompt = build_password_prompt(args, prefix);
+ if (prompt == NULL)
+ return PAM_BUF_ERR;
+ retval = pamk5_conv(args, prompt, PAM_PROMPT_ECHO_OFF, password);
+ free(prompt);
+ return retval;
+}
+
+
+/*
+ * Get information from the user or display a message to the user, as
+ * determined by type. If PAM_SILENT was given, don't pass any text or error
+ * messages to the application.
+ *
+ * The response variable is set to the response returned by the conversation
+ * function on a successful return if a response was desired. Caller is
+ * responsible for freeing it.
+ */
+int
+pamk5_conv(struct pam_args *args, const char *message, int type,
+ char **response)
+{
+ int pamret;
+ struct pam_message msg;
+ PAM_CONST struct pam_message *pmsg;
+ struct pam_response *resp = NULL;
+ struct pam_conv *conv;
+ int want_reply;
+
+ if (args->silent && (type == PAM_ERROR_MSG || type == PAM_TEXT_INFO))
+ return PAM_SUCCESS;
+ pamret = pam_get_item(args->pamh, PAM_CONV, (PAM_CONST void **) &conv);
+ if (pamret != PAM_SUCCESS)
+ return pamret;
+ if (conv->conv == NULL)
+ return PAM_CONV_ERR;
+ pmsg = &msg;
+ msg.msg_style = type;
+ msg.msg = (PAM_CONST char *) message;
+ pamret = conv->conv(1, &pmsg, &resp, conv->appdata_ptr);
+ if (pamret != PAM_SUCCESS)
+ return pamret;
+
+ /*
+ * Only expect a response for PAM_PROMPT_ECHO_OFF or PAM_PROMPT_ECHO_ON
+ * message types. This mildly annoying logic makes sure that everything
+ * is freed properly (except the response itself, if wanted, which is
+ * returned for the caller to free) and that the success status is set
+ * based on whether the reply matched our expectations.
+ *
+ * If we got a reply even though we didn't want one, still overwrite the
+ * reply before freeing in case it was a password.
+ */
+ want_reply = (type == PAM_PROMPT_ECHO_OFF || type == PAM_PROMPT_ECHO_ON);
+ if (resp == NULL || resp->resp == NULL)
+ pamret = want_reply ? PAM_CONV_ERR : PAM_SUCCESS;
+ else if (want_reply && response != NULL) {
+ *response = resp->resp;
+ pamret = PAM_SUCCESS;
+ } else {
+ explicit_bzero(resp->resp, strlen(resp->resp));
+ free(resp->resp);
+ pamret = want_reply ? PAM_SUCCESS : PAM_CONV_ERR;
+ }
+ free(resp);
+ return pamret;
+}
+
+
+/*
+ * Allocate memory to copy all of the prompts into a pam_message.
+ *
+ * Linux PAM and Solaris PAM expect different things here. Solaris PAM
+ * expects to receive a pointer to a pointer to an array of pam_message
+ * structs. Linux PAM expects to receive a pointer to an array of pointers to
+ * pam_message structs. In order for the module to work with either PAM
+ * implementation, we need to set up a structure that is valid either way you
+ * look at it.
+ *
+ * We do this by making msg point to the array of struct pam_message pointers
+ * (what Linux PAM expects), and then make the first one of those pointers
+ * point to the array of pam_message structs. Solaris will then be happy,
+ * looking at only the first element of the outer array and finding it
+ * pointing to the inner array. Then, for Linux, we point the other elements
+ * of the outer array to the storage allocated in the inner array.
+ *
+ * All this also means we have to be careful how we free the resulting
+ * structure since it's double-linked in a subtle way. Thankfully, we get to
+ * free it ourselves.
+ */
+static struct pam_message **
+allocate_pam_message(size_t total_prompts)
+{
+ struct pam_message **msg;
+ size_t i;
+
+ msg = calloc(total_prompts, sizeof(struct pam_message *));
+ if (msg == NULL)
+ return NULL;
+ *msg = calloc(total_prompts, sizeof(struct pam_message));
+ if (*msg == NULL) {
+ free(msg);
+ return NULL;
+ }
+ for (i = 1; i < total_prompts; i++)
+ msg[i] = msg[0] + i;
+ return msg;
+}
+
+
+/*
+ * Free the structure created by allocate_pam_message.
+ */
+static void
+free_pam_message(struct pam_message **msg, size_t total_prompts)
+{
+ size_t i;
+
+ for (i = 0; i < total_prompts; i++)
+ free((char *) msg[i]->msg);
+ free(*msg);
+ free(msg);
+}
+
+
+/*
+ * Free the responses returned by the conversation function. These may
+ * contain passwords, so we overwrite them before we free them.
+ */
+static void
+free_pam_responses(struct pam_response *resp, size_t total_prompts)
+{
+ size_t i;
+
+ if (resp == NULL)
+ return;
+ for (i = 0; i < total_prompts; i++) {
+ if (resp[i].resp != NULL) {
+ explicit_bzero(resp[i].resp, strlen(resp[i].resp));
+ free(resp[i].resp);
+ }
+ }
+ free(resp);
+}
+
+
+/*
+ * Format a Kerberos prompt into a PAM prompt. Takes a krb5_prompt as input
+ * and writes the resulting PAM prompt into a struct pam_message.
+ */
+static krb5_error_code
+format_prompt(krb5_prompt *prompt, struct pam_message *message)
+{
+ size_t len = strlen(prompt->prompt);
+ bool has_colon;
+ const char *colon;
+ int retval, style;
+
+ /*
+ * Heimdal adds the trailing colon and space, while MIT does not.
+ * Work around the difference by looking to see if there's a trailing
+ * colon and space already and only adding it if there is not.
+ */
+ has_colon = (len > 2 && memcmp(&prompt->prompt[len - 2], ": ", 2) == 0);
+ colon = has_colon ? "" : ": ";
+ retval = asprintf((char **) &message->msg, "%s%s", prompt->prompt, colon);
+ if (retval < 0)
+ return retval;
+ style = prompt->hidden ? PAM_PROMPT_ECHO_OFF : PAM_PROMPT_ECHO_ON;
+ message->msg_style = style;
+ return 0;
+}
+
+
+/*
+ * Given an array of struct pam_response elements, record the responses in the
+ * corresponding krb5_prompt structures.
+ */
+static krb5_error_code
+record_prompt_answers(struct pam_response *resp, int num_prompts,
+ krb5_prompt *prompts)
+{
+ int i;
+
+ for (i = 0; i < num_prompts; i++) {
+ size_t len, allowed;
+
+ if (resp[i].resp == NULL)
+ return KRB5_LIBOS_CANTREADPWD;
+ len = strlen(resp[i].resp);
+ allowed = prompts[i].reply->length;
+ if (allowed == 0 || len > allowed - 1)
+ return KRB5_LIBOS_CANTREADPWD;
+
+ /*
+ * Since the first version of this module, it has copied a nul
+ * character into the prompt data buffer for MIT Kerberos with the
+ * note that "other applications expect it to be there." I suspect
+ * this is incorrect and nothing cares about this nul, but have
+ * preserved this behavior out of an abundance of caution.
+ *
+ * Note that it shortens the maximum response length we're willing to
+ * accept by one (implemented above) and is the source of one prior
+ * security vulnerability.
+ */
+ memcpy(prompts[i].reply->data, resp[i].resp, len + 1);
+ prompts[i].reply->length = (unsigned int) len;
+ }
+ return 0;
+}
+
+
+/*
+ * This is the generic prompting function called by both MIT Kerberos and
+ * Heimdal prompting implementations.
+ *
+ * There are a lot of structures and different layers of code at work here,
+ * making this code quite confusing. This function is a prompter function to
+ * pass into the Kerberos library, in particular krb5_get_init_creds_password.
+ * It is used by the Kerberos library to prompt for a password if need be, and
+ * also to prompt for password changes if the password was expired.
+ *
+ * The purpose of this function is to serve as glue between the Kerberos
+ * library and the application (by way of the PAM glue). PAM expects us to
+ * pass back to the conversation function an array of prompts and receive from
+ * the application an array of responses to those prompts. We pass the
+ * application an array of struct pam_message pointers, and the application
+ * passes us an array of struct pam_response pointers.
+ *
+ * Kerberos, meanwhile, passes us in an array of krb5_prompt structs. This
+ * struct contains the prompt, a flag saying whether to suppress echoing of
+ * what the user types for that prompt, and a buffer into which to store the
+ * response.
+ *
+ * Therefore, what we're doing here is copying the prompts from the
+ * krb5_prompt structs into pam_message structs, calling the conversation
+ * function, and then copying the responses back out of pam_response structs
+ * into the krb5_prompt structs to return to the Kerberos library.
+ */
+krb5_error_code
+pamk5_prompter_krb5(krb5_context context UNUSED, void *data, const char *name,
+ const char *banner, int num_prompts, krb5_prompt *prompts)
+{
+ struct pam_args *args = data;
+ int current_prompt, retval, pamret, i, offset;
+ int total_prompts = num_prompts;
+ struct pam_message **msg;
+ struct pam_response *resp = NULL;
+ struct pam_conv *conv;
+
+ /* Treat the name and banner as prompts that doesn't need input. */
+ if (name != NULL && !args->silent)
+ total_prompts++;
+ if (banner != NULL && !args->silent)
+ total_prompts++;
+
+ /* If we have zero prompts, do nothing, silently. */
+ if (total_prompts == 0)
+ return 0;
+
+ /* Obtain the conversation function from the application. */
+ pamret = pam_get_item(args->pamh, PAM_CONV, (PAM_CONST void **) &conv);
+ if (pamret != 0)
+ return KRB5_LIBOS_CANTREADPWD;
+ if (conv->conv == NULL)
+ return KRB5_LIBOS_CANTREADPWD;
+
+ /* Allocate memory to copy all of the prompts into a pam_message. */
+ msg = allocate_pam_message(total_prompts);
+ if (msg == NULL)
+ return ENOMEM;
+
+ /* current_prompt is an index into msg and a count when we're done. */
+ current_prompt = 0;
+ if (name != NULL && !args->silent) {
+ msg[current_prompt]->msg = strdup(name);
+ if (msg[current_prompt]->msg == NULL) {
+ retval = ENOMEM;
+ goto cleanup;
+ }
+ msg[current_prompt]->msg_style = PAM_TEXT_INFO;
+ current_prompt++;
+ }
+ if (banner != NULL && !args->silent) {
+ assert(current_prompt < total_prompts);
+ msg[current_prompt]->msg = strdup(banner);
+ if (msg[current_prompt]->msg == NULL) {
+ retval = ENOMEM;
+ goto cleanup;
+ }
+ msg[current_prompt]->msg_style = PAM_TEXT_INFO;
+ current_prompt++;
+ }
+ for (i = 0; i < num_prompts; i++) {
+ assert(current_prompt < total_prompts);
+ retval = format_prompt(&prompts[i], msg[current_prompt]);
+ if (retval < 0)
+ goto cleanup;
+ current_prompt++;
+ }
+
+ /* Call into the application conversation function. */
+ pamret = conv->conv(total_prompts, (PAM_CONST struct pam_message **) msg,
+ &resp, conv->appdata_ptr);
+ if (pamret != 0 || resp == NULL) {
+ retval = KRB5_LIBOS_CANTREADPWD;
+ goto cleanup;
+ }
+
+ /*
+ * Record the answers in the Kerberos data structure. If name or banner
+ * were provided, skip over the initial PAM responses that correspond to
+ * those messages.
+ */
+ offset = 0;
+ if (name != NULL && !args->silent)
+ offset++;
+ if (banner != NULL && !args->silent)
+ offset++;
+ retval = record_prompt_answers(resp + offset, num_prompts, prompts);
+
+cleanup:
+ free_pam_message(msg, total_prompts);
+ free_pam_responses(resp, total_prompts);
+ return retval;
+}
+
+
+/*
+ * This is a special version of krb5_prompter_krb5 that returns an error if
+ * the Kerberos library asks for a password. It is only used with MIT
+ * Kerberos as part of the implementation of try_pkinit and use_pkinit.
+ * (Heimdal has a different API for PKINIT authentication.)
+ */
+#ifdef HAVE_KRB5_GET_PROMPT_TYPES
+krb5_error_code
+pamk5_prompter_krb5_no_password(krb5_context context, void *data,
+ const char *name, const char *banner,
+ int num_prompts, krb5_prompt *prompts)
+{
+ krb5_prompt_type *ptypes;
+ int i;
+
+ ptypes = krb5_get_prompt_types(context);
+ for (i = 0; i < num_prompts; i++)
+ if (ptypes != NULL && ptypes[i] == KRB5_PROMPT_TYPE_PASSWORD)
+ return KRB5_LIBOS_CANTREADPWD;
+ return pamk5_prompter_krb5(context, data, name, banner, num_prompts,
+ prompts);
+}
+#else /* !HAVE_KRB5_GET_PROMPT_TYPES */
+krb5_error_code
+pamk5_prompter_krb5_no_password(krb5_context context, void *data,
+ const char *name, const char *banner,
+ int num_prompts, krb5_prompt *prompts)
+{
+ return pamk5_prompter_krb5(context, data, name, banner, num_prompts,
+ prompts);
+}
+#endif /* !HAVE_KRB5_GET_PROMPT_TYPES */
diff --git a/module/public.c b/module/public.c
new file mode 100644
index 000000000000..44d5f7736794
--- /dev/null
+++ b/module/public.c
@@ -0,0 +1,260 @@
+/*
+ * The public APIs of the pam-afs-session PAM module.
+ *
+ * Provides the public pam_sm_authenticate, pam_sm_setcred,
+ * pam_sm_open_session, pam_sm_close_session, and pam_sm_chauthtok functions.
+ * These must all be specified in the same file to work with the symbol export
+ * and linking mechanism used in OpenPAM, since OpenPAM will mark them all as
+ * static functions and export a function table instead.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2005-2009, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+/* Get prototypes for all of the functions. */
+#define PAM_SM_ACCOUNT
+#define PAM_SM_AUTH
+#define PAM_SM_PASSWORD
+#define PAM_SM_SESSION
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * The main PAM interface for authorization checking.
+ */
+PAM_EXTERN int
+pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_AUTH_ERR;
+ goto done;
+ }
+ pamret = pamk5_context_fetch(args);
+ ENTRY(args, flags);
+
+ /*
+ * Succeed if the user did not use krb5 to login. Ideally, we should
+ * probably fail and require that the user set up policy properly in their
+ * PAM configuration, but it's not common for the user to do so and that's
+ * not how other krb5 PAM modules work. If we don't do this, root logins
+ * with the system root password fail, which is a bad failure mode.
+ */
+ if (pamret != PAM_SUCCESS || args->config->ctx == NULL) {
+ pamret = PAM_IGNORE;
+ putil_debug(args, "skipping non-Kerberos login");
+ goto done;
+ }
+
+ pamret = pamk5_account(args);
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface for authentication. We also do authorization checks
+ * here, since many applications don't call pam_acct_mgmt.
+ */
+PAM_EXTERN int
+pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ENTRY(args, flags);
+
+ pamret = pamk5_authenticate(args);
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface, in the auth stack, for establishing credentials
+ * obtained during authentication.
+ */
+PAM_EXTERN int
+pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ bool refresh = false;
+ int pamret, allow;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ENTRY(args, flags);
+
+ /*
+ * Special case. Just free the context data, which will destroy the
+ * ticket cache as well.
+ */
+ if (flags & PAM_DELETE_CRED) {
+ pamret = pam_set_data(pamh, "pam_krb5", NULL, NULL);
+ if (pamret != PAM_SUCCESS)
+ putil_err_pam(args, pamret, "cannot clear context data");
+ goto done;
+ }
+
+ /*
+ * Reinitialization requested, which means that rather than creating a new
+ * ticket cache and setting KRB5CCNAME, we should figure out the existing
+ * ticket cache and just refresh its tickets.
+ */
+ if (flags & (PAM_REINITIALIZE_CRED | PAM_REFRESH_CRED))
+ refresh = true;
+ if (refresh && (flags & PAM_ESTABLISH_CRED)) {
+ putil_err(args, "requested establish and refresh at the same time");
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ allow = PAM_REINITIALIZE_CRED | PAM_REFRESH_CRED | PAM_ESTABLISH_CRED;
+ if (!(flags & allow)) {
+ putil_err(args, "invalid pam_setcred flags %d", flags);
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+
+ /* Do the work. */
+ pamret = pamk5_setcred(args, refresh);
+
+ /*
+ * Never return PAM_IGNORE from pam_setcred since this can confuse the
+ * Linux PAM library, at least for applications that call pam_setcred
+ * without pam_authenticate (possibly because authentication was done
+ * some other way), when used with jumps with the [] syntax. Since we
+ * do nothing in this case, and since the stack is already frozen from
+ * the auth group, success makes sense.
+ *
+ * Don't return an error here or the PAM stack will fail if pam-krb5 is
+ * used with [success=ok default=1], since jumps are treated as required
+ * during the second pass with pam_setcred.
+ */
+ if (pamret == PAM_IGNORE)
+ pamret = PAM_SUCCESS;
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface for password changing.
+ */
+PAM_EXTERN int
+pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+ pamk5_context_fetch(args);
+ ENTRY(args, flags);
+
+ /* We only support password changes. */
+ if (!(flags & PAM_UPDATE_AUTHTOK) && !(flags & PAM_PRELIM_CHECK)) {
+ putil_err(args, "invalid pam_chauthtok flags %d", flags);
+ pamret = PAM_AUTHTOK_ERR;
+ goto done;
+ }
+
+ pamret = pamk5_password(args, (flags & PAM_PRELIM_CHECK) != 0);
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface for opening a session.
+ */
+PAM_EXTERN int
+pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ENTRY(args, flags);
+ pamret = pamk5_setcred(args, 0);
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/*
+ * The main PAM interface for closing a session.
+ */
+PAM_EXTERN int
+pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,
+ const char **argv)
+{
+ struct pam_args *args;
+ int pamret;
+
+ args = pamk5_init(pamh, flags, argc, argv);
+ if (args == NULL) {
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ ENTRY(args, flags);
+ pamret = pam_set_data(pamh, "pam_krb5", NULL, NULL);
+ if (pamret != PAM_SUCCESS)
+ putil_err_pam(args, pamret, "cannot clear context data");
+
+done:
+ EXIT(args, pamret);
+ pamk5_free(args);
+ return pamret;
+}
+
+
+/* OpenPAM uses this macro to set up a table of entry points. */
+#ifdef PAM_MODULE_ENTRY
+PAM_MODULE_ENTRY("pam_krb5");
+#endif
diff --git a/module/setcred.c b/module/setcred.c
new file mode 100644
index 000000000000..5b98b2919c88
--- /dev/null
+++ b/module/setcred.c
@@ -0,0 +1,474 @@
+/*
+ * Ticket creation routines for pam-krb5.
+ *
+ * pam_setcred and pam_open_session need to do similar but not identical work
+ * to create the user's ticket cache. The shared code is abstracted here into
+ * the pamk5_setcred function.
+ *
+ * Copyright 2005-2009, 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <assert.h>
+#include <errno.h>
+#include <pwd.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Given a cache name and an existing cache, initialize a new cache, store the
+ * credentials from the existing cache in it, and return a pointer to the new
+ * cache in the cache argument. Returns either PAM_SUCCESS or
+ * PAM_SERVICE_ERR.
+ */
+static int
+cache_init_from_cache(struct pam_args *args, const char *ccname,
+ krb5_ccache old, krb5_ccache *cache)
+{
+ struct context *ctx;
+ krb5_creds creds;
+ krb5_cc_cursor cursor;
+ int pamret;
+ krb5_error_code status;
+
+ *cache = NULL;
+ memset(&creds, 0, sizeof(creds));
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL
+ || args->config->ctx->context == NULL)
+ return PAM_SERVICE_ERR;
+ if (old == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+ status = krb5_cc_start_seq_get(ctx->context, old, &cursor);
+ if (status != 0) {
+ putil_err_krb5(args, status, "cannot open new credentials");
+ return PAM_SERVICE_ERR;
+ }
+ status = krb5_cc_next_cred(ctx->context, old, &cursor, &creds);
+ if (status != 0) {
+ putil_err_krb5(args, status, "cannot read new credentials");
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ pamret = pamk5_cache_init(args, ccname, &creds, cache);
+ if (pamret != PAM_SUCCESS) {
+ krb5_free_cred_contents(ctx->context, &creds);
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ krb5_free_cred_contents(ctx->context, &creds);
+
+ /*
+ * There probably won't be any additional credentials, but check for them
+ * and copy them just in case.
+ */
+ while (krb5_cc_next_cred(ctx->context, old, &cursor, &creds) == 0) {
+ status = krb5_cc_store_cred(ctx->context, *cache, &creds);
+ krb5_free_cred_contents(ctx->context, &creds);
+ if (status != 0) {
+ putil_err_krb5(args, status,
+ "cannot store additional credentials"
+ " in %s",
+ ccname);
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ }
+ pamret = PAM_SUCCESS;
+
+done:
+ krb5_cc_end_seq_get(ctx->context, ctx->cache, &cursor);
+ if (pamret != PAM_SUCCESS && *cache != NULL) {
+ krb5_cc_destroy(ctx->context, *cache);
+ *cache = NULL;
+ }
+ return pamret;
+}
+
+
+/*
+ * Determine the name of a new ticket cache. Handles ccache and ccache_dir
+ * PAM options and returns newly allocated memory.
+ *
+ * The ccache option, if set, contains a string with possible %u and %p
+ * escapes. The former is replaced by the UID and the latter is replaced by
+ * the PID (a suitable unique string).
+ */
+static char *
+build_ccache_name(struct pam_args *args, uid_t uid)
+{
+ char *cache_name = NULL;
+ int retval;
+
+ if (args->config->ccache == NULL) {
+ retval = asprintf(&cache_name, "%s/krb5cc_%d_XXXXXX",
+ args->config->ccache_dir, (int) uid);
+ if (retval < 0) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ return NULL;
+ }
+ } else {
+ size_t len = 0, delta;
+ char *p, *q;
+
+ for (p = args->config->ccache; *p != '\0'; p++) {
+ if (p[0] == '%' && p[1] == 'u') {
+ len += snprintf(NULL, 0, "%ld", (long) uid);
+ p++;
+ } else if (p[0] == '%' && p[1] == 'p') {
+ len += snprintf(NULL, 0, "%ld", (long) getpid());
+ p++;
+ } else {
+ len++;
+ }
+ }
+ len++;
+ cache_name = malloc(len);
+ if (cache_name == NULL) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ return NULL;
+ }
+ for (p = args->config->ccache, q = cache_name; *p != '\0'; p++) {
+ if (p[0] == '%' && p[1] == 'u') {
+ delta = snprintf(q, len, "%ld", (long) uid);
+ q += delta;
+ len -= delta;
+ p++;
+ } else if (p[0] == '%' && p[1] == 'p') {
+ delta = snprintf(q, len, "%ld", (long) getpid());
+ q += delta;
+ len -= delta;
+ p++;
+ } else {
+ *q = *p;
+ q++;
+ len--;
+ }
+ }
+ *q = '\0';
+ }
+ return cache_name;
+}
+
+
+/*
+ * Create a new context for a session if we've lost the context created during
+ * authentication (such as when running under OpenSSH). Return PAM_IGNORE if
+ * we're ignoring this user or if apparently our pam_authenticate never
+ * succeeded.
+ */
+static int
+create_session_context(struct pam_args *args)
+{
+ struct context *ctx = NULL;
+ PAM_CONST char *user;
+ const char *tmpname;
+ int status, pamret;
+
+ /* If we're going to ignore the user anyway, don't even bother. */
+ if (args->config->ignore_root || args->config->minimum_uid > 0) {
+ pamret = pam_get_user(args->pamh, &user, NULL);
+ if (pamret == PAM_SUCCESS && pamk5_should_ignore(args, user)) {
+ pamret = PAM_IGNORE;
+ goto fail;
+ }
+ }
+
+ /*
+ * Create the context and locate the temporary ticket cache. Load the
+ * ticket cache back into the context and flush out the other data that
+ * would have been set if we'd kept our original context.
+ */
+ pamret = pamk5_context_new(args);
+ if (pamret != PAM_SUCCESS) {
+ putil_crit_pam(args, pamret, "creating session context failed");
+ goto fail;
+ }
+ ctx = args->config->ctx;
+ tmpname = pamk5_get_krb5ccname(args, "PAM_KRB5CCNAME");
+ if (tmpname == NULL) {
+ putil_debug(args, "unable to get PAM_KRB5CCNAME, assuming"
+ " non-Kerberos login");
+ pamret = PAM_IGNORE;
+ goto fail;
+ }
+ putil_debug(args, "found initial ticket cache at %s", tmpname);
+ status = krb5_cc_resolve(ctx->context, tmpname, &ctx->cache);
+ if (status != 0) {
+ putil_err_krb5(args, status, "cannot resolve cache %s", tmpname);
+ pamret = PAM_SERVICE_ERR;
+ goto fail;
+ }
+ status = krb5_cc_get_principal(ctx->context, ctx->cache, &ctx->princ);
+ if (status != 0) {
+ putil_err_krb5(args, status, "cannot retrieve principal");
+ pamret = PAM_SERVICE_ERR;
+ goto fail;
+ }
+
+ /*
+ * We've rebuilt the context. Push it back into the PAM state for any
+ * further calls to session or account management, which OpenSSH does keep
+ * the context for.
+ */
+ pamret = pam_set_data(args->pamh, "pam_krb5", ctx, pamk5_context_destroy);
+ if (pamret != PAM_SUCCESS) {
+ putil_err_pam(args, pamret, "cannot set context data");
+ goto fail;
+ }
+ return PAM_SUCCESS;
+
+fail:
+ pamk5_context_free(args);
+ return pamret;
+}
+
+
+/*
+ * Sets user credentials by creating the permanent ticket cache and setting
+ * the proper ownership. This function may be called by either pam_sm_setcred
+ * or pam_sm_open_session. The refresh flag should be set to true if we
+ * should reinitialize an existing ticket cache instead of creating a new one.
+ */
+int
+pamk5_setcred(struct pam_args *args, bool refresh)
+{
+ struct context *ctx = NULL;
+ krb5_ccache cache = NULL;
+ char *cache_name = NULL;
+ bool set_context = false;
+ int status = 0;
+ int pamret;
+ struct passwd *pw = NULL;
+ uid_t uid;
+ gid_t gid;
+
+ /* If configured not to create a cache, we have nothing to do. */
+ if (args->config->no_ccache) {
+ pamret = PAM_SUCCESS;
+ goto done;
+ }
+
+ /*
+ * If we weren't able to obtain a context, we were probably run by OpenSSH
+ * with its weird PAM handling, so we're going to cobble up a new context
+ * for ourselves.
+ */
+ pamret = pamk5_context_fetch(args);
+ if (pamret != PAM_SUCCESS) {
+ putil_debug(args, "no context found, creating one");
+ pamret = create_session_context(args);
+ if (pamret != PAM_SUCCESS || args->config->ctx == NULL)
+ goto done;
+ set_context = true;
+ }
+ ctx = args->config->ctx;
+
+ /*
+ * Some programs (xdm, for instance) appear to call setcred over and over
+ * again, so avoid doing useless work.
+ */
+ if (ctx->initialized) {
+ pamret = PAM_SUCCESS;
+ goto done;
+ }
+
+ /*
+ * Get the uid. The user is not required to be a local account for
+ * pam_authenticate, but for either pam_setcred (other than DELETE) or for
+ * pam_open_session, the user must be a local account.
+ */
+ pw = pam_modutil_getpwnam(args->pamh, ctx->name);
+ if (pw == NULL) {
+ putil_err(args, "getpwnam failed for %s", ctx->name);
+ pamret = PAM_USER_UNKNOWN;
+ goto done;
+ }
+ uid = pw->pw_uid;
+ gid = pw->pw_gid;
+
+ /* Get the cache name. If reinitializing, this is our existing cache. */
+ if (refresh) {
+ const char *name, *k5name;
+
+ /*
+ * Solaris su calls pam_setcred as root with PAM_REINITIALIZE_CREDS,
+ * preserving the user-supplied environment. An xlock program may
+ * also do this if it's setuid root and doesn't drop credentials
+ * before calling pam_setcred.
+ *
+ * There isn't any safe way of reinitializing the exiting ticket cache
+ * for the user if we're setuid without calling setreuid(). Calling
+ * setreuid() is possible, but if the calling application is threaded,
+ * it will change credentials for the whole application, with possibly
+ * bizarre and unintended (and insecure) results. Trying to verify
+ * ownership of the existing ticket cache before using it fails under
+ * various race conditions (for example, having one of the elements of
+ * the path be a symlink and changing the target of that symlink
+ * between our check and the call to krb5_cc_resolve). Without
+ * calling setreuid(), we run the risk of replacing a file owned by
+ * another user with a credential cache.
+ *
+ * We could fail with an error in the setuid case, which would be
+ * maximally safe, but it would prevent use of the module for
+ * authentication with programs such as Solaris su. Failure to
+ * reinitialize the cache is normally not a serious problem, just a
+ * missing feature. We therefore log an error and exit with
+ * PAM_SUCCESS for the setuid case.
+ *
+ * We do not use issetugid here since it always returns true if setuid
+ * was was involved anywhere in the process of running the binary.
+ * This would prevent a setuid screensaver that drops permissions from
+ * refreshing a credential cache. The issetugid behavior is safer,
+ * since the environment should ideally not be trusted even if the
+ * binary completely changed users away from the original user, but in
+ * that case the binary needs to take some responsibility for either
+ * sanitizing the environment or being certain that the calling user
+ * is permitted to act as the target user.
+ */
+ if (getuid() != geteuid() || getgid() != getegid()) {
+ putil_err(args, "credential reinitialization in a setuid context"
+ " ignored");
+ pamret = PAM_SUCCESS;
+ goto done;
+ }
+ name = pamk5_get_krb5ccname(args, "KRB5CCNAME");
+ if (name == NULL)
+ name = krb5_cc_default_name(ctx->context);
+ if (name == NULL) {
+ putil_err(args, "unable to get ticket cache name");
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ if (strncmp(name, "FILE:", strlen("FILE:")) == 0)
+ name += strlen("FILE:");
+
+ /*
+ * If the cache we have in the context and the cache we're
+ * reinitializing are the same cache, don't do anything; otherwise,
+ * we'll end up destroying the cache. This should never happen; this
+ * case triggering is a sign of a bug, probably in the calling
+ * application.
+ */
+ if (ctx->cache != NULL) {
+ k5name = krb5_cc_get_name(ctx->context, ctx->cache);
+ if (k5name != NULL) {
+ if (strncmp(k5name, "FILE:", strlen("FILE:")) == 0)
+ k5name += strlen("FILE:");
+ if (strcmp(name, k5name) == 0) {
+ pamret = PAM_SUCCESS;
+ goto done;
+ }
+ }
+ }
+
+ cache_name = strdup(name);
+ if (cache_name == NULL) {
+ putil_crit(args, "malloc failure: %s", strerror(errno));
+ pamret = PAM_BUF_ERR;
+ goto done;
+ }
+ putil_debug(args, "refreshing ticket cache %s", cache_name);
+
+ /*
+ * If we're refreshing the cache, we didn't really create it and the
+ * user's open session created by login is probably still managing
+ * it. Thus, don't remove it when PAM is shut down.
+ */
+ ctx->dont_destroy_cache = 1;
+ } else {
+ char *cache_name_tmp;
+ size_t len;
+
+ cache_name = build_ccache_name(args, uid);
+ if (cache_name == NULL) {
+ pamret = PAM_BUF_ERR;
+ goto done;
+ }
+ len = strlen(cache_name);
+ if (len > 6 && strncmp("XXXXXX", cache_name + len - 6, 6) == 0) {
+ if (strncmp(cache_name, "FILE:", strlen("FILE:")) == 0)
+ cache_name_tmp = cache_name + strlen("FILE:");
+ else
+ cache_name_tmp = cache_name;
+ pamret = pamk5_cache_mkstemp(args, cache_name_tmp);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ }
+ putil_debug(args, "initializing ticket cache %s", cache_name);
+ }
+
+ /*
+ * Initialize the new ticket cache and point the environment at it. Only
+ * chown the cache if the cache is of type FILE or has no type (making the
+ * assumption that the default cache type is FILE; otherwise, due to the
+ * type prefix, we'd end up with an invalid path.
+ */
+ pamret = cache_init_from_cache(args, cache_name, ctx->cache, &cache);
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ if (strncmp(cache_name, "FILE:", strlen("FILE:")) == 0)
+ status = chown(cache_name + strlen("FILE:"), uid, gid);
+ else if (strchr(cache_name, ':') == NULL)
+ status = chown(cache_name, uid, gid);
+ if (status == -1) {
+ putil_crit(args, "chown of ticket cache failed: %s", strerror(errno));
+ pamret = PAM_SERVICE_ERR;
+ goto done;
+ }
+ pamret = pamk5_set_krb5ccname(args, cache_name, "KRB5CCNAME");
+ if (pamret != PAM_SUCCESS) {
+ putil_crit(args, "setting KRB5CCNAME failed: %s", strerror(errno));
+ goto done;
+ }
+
+ /*
+ * If we had a temporary ticket cache, delete the environment variable so
+ * that we won't get confused and think we still have a temporary ticket
+ * cache when called again.
+ *
+ * FreeBSD PAM, at least as of 7.2, doesn't support deleting environment
+ * variables using the syntax supported by Solaris and Linux. Work
+ * around that by setting the variable to an empty value if deleting it
+ * fails.
+ */
+ if (pam_getenv(args->pamh, "PAM_KRB5CCNAME") != NULL) {
+ pamret = pam_putenv(args->pamh, "PAM_KRB5CCNAME");
+ if (pamret != PAM_SUCCESS)
+ pamret = pam_putenv(args->pamh, "PAM_KRB5CCNAME=");
+ if (pamret != PAM_SUCCESS)
+ goto done;
+ }
+
+ /* Destroy the temporary cache and put the new cache in the context. */
+ krb5_cc_destroy(ctx->context, ctx->cache);
+ ctx->cache = cache;
+ cache = NULL;
+ ctx->initialized = 1;
+ if (args->config->retain_after_close)
+ ctx->dont_destroy_cache = 1;
+
+done:
+ if (ctx != NULL && cache != NULL)
+ krb5_cc_destroy(ctx->context, cache);
+ free(cache_name);
+
+ /* If we stored our Kerberos context in PAM data, don't free it. */
+ if (set_context)
+ args->ctx = NULL;
+
+ return pamret;
+}
diff --git a/module/support.c b/module/support.c
new file mode 100644
index 000000000000..79b654ed2f32
--- /dev/null
+++ b/module/support.c
@@ -0,0 +1,141 @@
+/*
+ * Support functions for pam-krb5.
+ *
+ * Some general utility functions used by multiple PAM groups that aren't
+ * associated with any particular chunk of functionality.
+ *
+ * Copyright 2005-2007, 2009, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ * Copyright 2005 Andres Salomon <dilinger@debian.org>
+ * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+#include <pwd.h>
+
+#include <module/internal.h>
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Given the PAM arguments and the user we're authenticating, see if we should
+ * ignore that user because they're root or have a low-numbered UID and we
+ * were configured to ignore such users. Returns true if we should ignore
+ * them, false otherwise. Ignores any fully-qualified principal names.
+ */
+int
+pamk5_should_ignore(struct pam_args *args, PAM_CONST char *username)
+{
+ struct passwd *pwd;
+
+ if (args->config->ignore_root && strcmp("root", username) == 0) {
+ putil_debug(args, "ignoring root user");
+ return 1;
+ }
+ if (args->config->minimum_uid > 0 && strchr(username, '@') == NULL) {
+ pwd = pam_modutil_getpwnam(args->pamh, username);
+ if (pwd != NULL && pwd->pw_uid < (uid_t) args->config->minimum_uid) {
+ putil_debug(args, "ignoring low-UID user (%lu < %ld)",
+ (unsigned long) pwd->pw_uid,
+ args->config->minimum_uid);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+
+/*
+ * Verify the user authorization. Call krb5_kuserok if this is a local
+ * account, or do the krb5_aname_to_localname verification if ignore_k5login
+ * was requested. For non-local accounts, the principal must match the
+ * authentication identity.
+ */
+int
+pamk5_authorized(struct pam_args *args)
+{
+ struct context *ctx;
+ krb5_context c;
+ krb5_error_code retval;
+ int status;
+ struct passwd *pwd;
+ char kuser[65]; /* MAX_USERNAME == 65 (MIT Kerberos 1.4.1). */
+
+ if (args == NULL || args->config == NULL || args->config->ctx == NULL
+ || args->config->ctx->context == NULL)
+ return PAM_SERVICE_ERR;
+ ctx = args->config->ctx;
+ if (ctx->name == NULL)
+ return PAM_SERVICE_ERR;
+ c = ctx->context;
+
+ /*
+ * If alt_auth_map was set, authorize the user if the authenticated
+ * principal matches the mapped principal. alt_auth_map essentially
+ * serves as a supplemental .k5login. PAM_SERVICE_ERR indicates fatal
+ * errors that should abort remaining processing; PAM_AUTH_ERR indicates
+ * that it just didn't match, in which case we continue to try other
+ * authorization methods.
+ */
+ if (args->config->alt_auth_map != NULL) {
+ status = pamk5_alt_auth_verify(args);
+ if (status == PAM_SUCCESS || status == PAM_SERVICE_ERR)
+ return status;
+ }
+
+ /*
+ * If the name to which we're authenticating contains @ (is fully
+ * qualified), it must match the principal exactly.
+ */
+ if (strchr(ctx->name, '@') != NULL) {
+ char *principal;
+
+ retval = krb5_unparse_name(c, ctx->princ, &principal);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "krb5_unparse_name failed");
+ return PAM_SERVICE_ERR;
+ }
+ if (strcmp(principal, ctx->name) != 0) {
+ putil_err(args, "user %s does not match principal %s", ctx->name,
+ principal);
+ krb5_free_unparsed_name(c, principal);
+ return PAM_AUTH_ERR;
+ }
+ krb5_free_unparsed_name(c, principal);
+ return PAM_SUCCESS;
+ }
+
+ /*
+ * Otherwise, apply either krb5_aname_to_localname or krb5_kuserok
+ * depending on the situation.
+ */
+ pwd = pam_modutil_getpwnam(args->pamh, ctx->name);
+ if (args->config->ignore_k5login || pwd == NULL) {
+ retval = krb5_aname_to_localname(c, ctx->princ, sizeof(kuser), kuser);
+ if (retval != 0) {
+ putil_err_krb5(args, retval, "cannot convert principal to user");
+ return PAM_AUTH_ERR;
+ }
+ if (strcmp(kuser, ctx->name) != 0) {
+ putil_err(args, "user %s does not match local name %s", ctx->name,
+ kuser);
+ return PAM_AUTH_ERR;
+ }
+ } else {
+ if (!krb5_kuserok(c, ctx->princ, ctx->name)) {
+ putil_err(args, "krb5_kuserok for user %s failed", ctx->name);
+ return PAM_AUTH_ERR;
+ }
+ }
+
+ return PAM_SUCCESS;
+}
diff --git a/pam-util/args.c b/pam-util/args.c
new file mode 100644
index 000000000000..293988b8cd1a
--- /dev/null
+++ b/pam-util/args.c
@@ -0,0 +1,105 @@
+/*
+ * Constructor and destructor for PAM data.
+ *
+ * The PAM utility functions often need an initial argument that encapsulates
+ * the PAM handle, some configuration information, and possibly a Kerberos
+ * context. This implements a constructor and destructor for that data
+ * structure.
+ *
+ * The individual PAM modules should provide a definition of the pam_config
+ * struct appropriate to that module. None of the PAM utility functions need
+ * to know what that configuration struct looks like, and it must be freed
+ * before calling putil_args_free().
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010, 2012-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+
+/*
+ * Allocate a new pam_args struct and return it, or NULL on memory allocation
+ * or Kerberos initialization failure. If HAVE_KRB5 is defined, we also
+ * allocate a Kerberos context.
+ */
+struct pam_args *
+putil_args_new(pam_handle_t *pamh, int flags)
+{
+ struct pam_args *args;
+#ifdef HAVE_KRB5
+ krb5_error_code status;
+#endif
+
+ args = calloc(1, sizeof(struct pam_args));
+ if (args == NULL) {
+ putil_crit(NULL, "cannot allocate memory: %s", strerror(errno));
+ return NULL;
+ }
+ args->pamh = pamh;
+ args->silent = ((flags & PAM_SILENT) == PAM_SILENT);
+
+#ifdef HAVE_KRB5
+ if (issetugid())
+ status = krb5_init_secure_context(&args->ctx);
+ else
+ status = krb5_init_context(&args->ctx);
+ if (status != 0) {
+ putil_err_krb5(args, status, "cannot create Kerberos context");
+ free(args);
+ return NULL;
+ }
+#endif /* HAVE_KRB5 */
+ return args;
+}
+
+
+/*
+ * Free a pam_args struct. The config member must be freed separately.
+ */
+void
+putil_args_free(struct pam_args *args)
+{
+ if (args == NULL)
+ return;
+#ifdef HAVE_KRB5
+ free(args->realm);
+ if (args->ctx != NULL)
+ krb5_free_context(args->ctx);
+#endif
+ free(args);
+}
diff --git a/pam-util/args.h b/pam-util/args.h
new file mode 100644
index 000000000000..79b5d046ab60
--- /dev/null
+++ b/pam-util/args.h
@@ -0,0 +1,84 @@
+/*
+ * Standard structure for PAM data.
+ *
+ * The PAM utility functions often need an initial argument that encapsulates
+ * the PAM handle, some configuration information, and possibly a Kerberos
+ * context. This header provides a standard structure definition.
+ *
+ * The individual PAM modules should provide a definition of the pam_config
+ * struct appropriate to that module. None of the PAM utility functions need
+ * to know what that configuration struct looks like.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef PAM_UTIL_ARGS_H
+#define PAM_UTIL_ARGS_H 1
+
+#include <config.h>
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+#include <portable/pam.h>
+#include <portable/stdbool.h>
+
+/* Opaque struct from the PAM utility perspective. */
+struct pam_config;
+
+struct pam_args {
+ pam_handle_t *pamh; /* Pointer back to the PAM handle. */
+ struct pam_config *config; /* Per-module PAM configuration. */
+ bool debug; /* Log debugging information. */
+ bool silent; /* Do not pass text to the application. */
+ const char *user; /* User being authenticated. */
+
+#ifdef HAVE_KRB5
+ krb5_context ctx; /* Context for Kerberos operations. */
+ char *realm; /* Kerberos realm for configuration. */
+#endif
+};
+
+BEGIN_DECLS
+
+/* Default to a hidden visibility for all internal functions. */
+#pragma GCC visibility push(hidden)
+
+/*
+ * Allocate and free the pam_args struct. We assume that user is a pointer to
+ * a string maintained elsewhere and don't free it here. config must be freed
+ * separately by the caller.
+ */
+struct pam_args *putil_args_new(pam_handle_t *, int flags);
+void putil_args_free(struct pam_args *);
+
+/* Undo default visibility change. */
+#pragma GCC visibility pop
+
+END_DECLS
+
+#endif /* !PAM_UTIL_ARGS_H */
diff --git a/pam-util/logging.c b/pam-util/logging.c
new file mode 100644
index 000000000000..460993315870
--- /dev/null
+++ b/pam-util/logging.c
@@ -0,0 +1,345 @@
+/*
+ * Logging functions for PAM modules.
+ *
+ * Logs errors and debugging messages from PAM modules. The debug versions
+ * only log anything if debugging was enabled; the crit and err versions
+ * always log.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2015, 2018, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2005-2007, 2009-2010, 2012-2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <syslog.h>
+
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+
+#ifndef LOG_AUTHPRIV
+# define LOG_AUTHPRIV LOG_AUTH
+#endif
+
+/* Used for iterating through arrays. */
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
+/*
+ * Mappings of PAM flags to symbolic names for logging when entering a PAM
+ * module function.
+ */
+static const struct {
+ int flag;
+ const char *name;
+} FLAGS[] = {
+ /* clang-format off */
+ {PAM_CHANGE_EXPIRED_AUTHTOK, "expired" },
+ {PAM_DELETE_CRED, "delete" },
+ {PAM_DISALLOW_NULL_AUTHTOK, "nonull" },
+ {PAM_ESTABLISH_CRED, "establish"},
+ {PAM_PRELIM_CHECK, "prelim" },
+ {PAM_REFRESH_CRED, "refresh" },
+ {PAM_REINITIALIZE_CRED, "reinit" },
+ {PAM_SILENT, "silent" },
+ {PAM_UPDATE_AUTHTOK, "update" },
+ /* clang-format on */
+};
+
+
+/*
+ * Utility function to format a message into newly allocated memory, reporting
+ * an error via syslog if vasprintf fails.
+ */
+static char *__attribute__((__format__(printf, 1, 0)))
+format(const char *fmt, va_list args)
+{
+ char *msg;
+
+ if (vasprintf(&msg, fmt, args) < 0) {
+ syslog(LOG_CRIT | LOG_AUTHPRIV, "vasprintf failed: %m");
+ return NULL;
+ }
+ return msg;
+}
+
+
+/*
+ * Log wrapper function that adds the user. Log a message with the given
+ * priority, prefixed by (user <user>) with the account name being
+ * authenticated if known.
+ */
+static void __attribute__((__format__(printf, 3, 0)))
+log_vplain(struct pam_args *pargs, int priority, const char *fmt, va_list args)
+{
+ char *msg;
+
+ if (priority == LOG_DEBUG && (pargs == NULL || !pargs->debug))
+ return;
+ if (pargs != NULL && pargs->user != NULL) {
+ msg = format(fmt, args);
+ if (msg == NULL)
+ return;
+ pam_syslog(pargs->pamh, priority, "(user %s) %s", pargs->user, msg);
+ free(msg);
+ } else if (pargs != NULL) {
+ pam_vsyslog(pargs->pamh, priority, fmt, args);
+ } else {
+ msg = format(fmt, args);
+ if (msg == NULL)
+ return;
+ syslog(priority | LOG_AUTHPRIV, "%s", msg);
+ free(msg);
+ }
+}
+
+
+/*
+ * Wrapper around log_vplain with variadic arguments.
+ */
+static void __attribute__((__format__(printf, 3, 4)))
+log_plain(struct pam_args *pargs, int priority, const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ log_vplain(pargs, priority, fmt, args);
+ va_end(args);
+}
+
+
+/*
+ * Log wrapper function for reporting a PAM error. Log a message with the
+ * given priority, prefixed by (user <user>) with the account name being
+ * authenticated if known, followed by a colon and the formatted PAM error.
+ * However, do not include the colon and the PAM error if the PAM status is
+ * PAM_SUCCESS.
+ */
+static void __attribute__((__format__(printf, 4, 0)))
+log_pam(struct pam_args *pargs, int priority, int status, const char *fmt,
+ va_list args)
+{
+ char *msg;
+
+ if (priority == LOG_DEBUG && (pargs == NULL || !pargs->debug))
+ return;
+ msg = format(fmt, args);
+ if (msg == NULL)
+ return;
+ if (pargs == NULL)
+ log_plain(NULL, priority, "%s", msg);
+ else if (status == PAM_SUCCESS)
+ log_plain(pargs, priority, "%s", msg);
+ else
+ log_plain(pargs, priority, "%s: %s", msg,
+ pam_strerror(pargs->pamh, status));
+ free(msg);
+}
+
+
+/*
+ * The public interfaces. For each common log level (crit, err, and debug),
+ * generate a putil_<level> function and one for _pam. Do this with the
+ * preprocessor to save duplicate code.
+ */
+/* clang-format off */
+#define LOG_FUNCTION(level, priority) \
+ void __attribute__((__format__(printf, 2, 3))) \
+ putil_ ## level(struct pam_args *pargs, const char *fmt, ...) \
+ { \
+ va_list args; \
+ \
+ va_start(args, fmt); \
+ log_vplain(pargs, priority, fmt, args); \
+ va_end(args); \
+ } \
+ void __attribute__((__format__(printf, 3, 4))) \
+ putil_ ## level ## _pam(struct pam_args *pargs, int status, \
+ const char *fmt, ...) \
+ { \
+ va_list args; \
+ \
+ va_start(args, fmt); \
+ log_pam(pargs, priority, status, fmt, args); \
+ va_end(args); \
+ }
+LOG_FUNCTION(crit, LOG_CRIT)
+LOG_FUNCTION(err, LOG_ERR)
+LOG_FUNCTION(notice, LOG_NOTICE)
+LOG_FUNCTION(debug, LOG_DEBUG)
+/* clang-format on */
+
+
+/*
+ * Report entry into a function. Takes the PAM arguments, the function name,
+ * and the flags and maps the flags to symbolic names.
+ */
+void
+putil_log_entry(struct pam_args *pargs, const char *func, int flags)
+{
+ size_t i, length, offset;
+ char *out = NULL, *nout;
+
+ if (!pargs->debug)
+ return;
+ if (flags != 0)
+ for (i = 0; i < ARRAY_SIZE(FLAGS); i++) {
+ if (!(flags & FLAGS[i].flag))
+ continue;
+ if (out == NULL) {
+ out = strdup(FLAGS[i].name);
+ if (out == NULL)
+ break;
+ } else {
+ length = strlen(FLAGS[i].name);
+ nout = realloc(out, strlen(out) + length + 2);
+ if (nout == NULL) {
+ free(out);
+ out = NULL;
+ break;
+ }
+ out = nout;
+ offset = strlen(out);
+ out[offset] = '|';
+ memcpy(out + offset + 1, FLAGS[i].name, length);
+ out[offset + 1 + length] = '\0';
+ }
+ }
+ if (out == NULL)
+ pam_syslog(pargs->pamh, LOG_DEBUG, "%s: entry", func);
+ else {
+ pam_syslog(pargs->pamh, LOG_DEBUG, "%s: entry (%s)", func, out);
+ free(out);
+ }
+}
+
+
+/*
+ * Report an authentication failure. This is a separate function since we
+ * want to include various PAM metadata in the log message and put it in a
+ * standard format. The format here is modeled after the pam_unix
+ * authentication failure message from Linux PAM.
+ */
+void __attribute__((__format__(printf, 2, 3)))
+putil_log_failure(struct pam_args *pargs, const char *fmt, ...)
+{
+ char *msg;
+ va_list args;
+ const char *ruser = NULL;
+ const char *rhost = NULL;
+ const char *tty = NULL;
+ const char *name = NULL;
+
+ if (pargs->user != NULL)
+ name = pargs->user;
+ va_start(args, fmt);
+ msg = format(fmt, args);
+ va_end(args);
+ if (msg == NULL)
+ return;
+ pam_get_item(pargs->pamh, PAM_RUSER, (PAM_CONST void **) &ruser);
+ pam_get_item(pargs->pamh, PAM_RHOST, (PAM_CONST void **) &rhost);
+ pam_get_item(pargs->pamh, PAM_TTY, (PAM_CONST void **) &tty);
+
+ /* clang-format off */
+ pam_syslog(pargs->pamh, LOG_NOTICE, "%s; logname=%s uid=%ld euid=%ld"
+ " tty=%s ruser=%s rhost=%s", msg,
+ (name != NULL) ? name : "",
+ (long) getuid(), (long) geteuid(),
+ (tty != NULL) ? tty : "",
+ (ruser != NULL) ? ruser : "",
+ (rhost != NULL) ? rhost : "");
+ /* clang-format on */
+
+ free(msg);
+}
+
+
+/*
+ * Below are the additional logging functions enabled if built with Kerberos
+ * support, used to report Kerberos errors.
+ */
+#ifdef HAVE_KRB5
+
+
+/*
+ * Log wrapper function for reporting a Kerberos error. Log a message with
+ * the given priority, prefixed by (user <user>) with the account name being
+ * authenticated if known, followed by a colon and the formatted Kerberos
+ * error.
+ */
+__attribute__((__format__(printf, 4, 0))) static void
+log_krb5(struct pam_args *pargs, int priority, int status, const char *fmt,
+ va_list args)
+{
+ char *msg;
+ const char *k5_msg = NULL;
+
+ if (priority == LOG_DEBUG && (pargs == NULL || !pargs->debug))
+ return;
+ msg = format(fmt, args);
+ if (msg == NULL)
+ return;
+ if (pargs != NULL && pargs->ctx != NULL) {
+ k5_msg = krb5_get_error_message(pargs->ctx, status);
+ log_plain(pargs, priority, "%s: %s", msg, k5_msg);
+ } else {
+ log_plain(pargs, priority, "%s", msg);
+ }
+ free(msg);
+ if (k5_msg != NULL)
+ krb5_free_error_message(pargs->ctx, k5_msg);
+}
+
+
+/*
+ * The public interfaces. Do this with the preprocessor to save duplicate
+ * code.
+ */
+/* clang-format off */
+#define LOG_FUNCTION_KRB5(level, priority) \
+ void __attribute__((__format__(printf, 3, 4))) \
+ putil_ ## level ## _krb5(struct pam_args *pargs, int status, \
+ const char *fmt, ...) \
+ { \
+ va_list args; \
+ \
+ va_start(args, fmt); \
+ log_krb5(pargs, priority, status, fmt, args); \
+ va_end(args); \
+ }
+LOG_FUNCTION_KRB5(crit, LOG_CRIT)
+LOG_FUNCTION_KRB5(err, LOG_ERR)
+LOG_FUNCTION_KRB5(notice, LOG_NOTICE)
+LOG_FUNCTION_KRB5(debug, LOG_DEBUG)
+/* clang-format on */
+
+#endif /* HAVE_KRB5 */
diff --git a/pam-util/logging.h b/pam-util/logging.h
new file mode 100644
index 000000000000..bf95ea520ae2
--- /dev/null
+++ b/pam-util/logging.h
@@ -0,0 +1,131 @@
+/*
+ * Interface to standard PAM logging.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006-2010, 2012-2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef PAM_UTIL_LOGGING_H
+#define PAM_UTIL_LOGGING_H 1
+
+#include <config.h>
+#include <portable/macros.h>
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+#include <portable/pam.h>
+
+#include <stddef.h>
+#include <syslog.h>
+
+/* Forward declarations to avoid extra includes. */
+struct pam_args;
+
+BEGIN_DECLS
+
+/* Default to a hidden visibility for all internal functions. */
+#pragma GCC visibility push(hidden)
+
+/*
+ * Error reporting and debugging functions. For each log level, there are two
+ * functions. The _log function just prints out the message it's given. The
+ * _log_pam function does the same but appends the pam_strerror results for
+ * the provided status code if it is not PAM_SUCCESS.
+ */
+void putil_crit(struct pam_args *, const char *, ...)
+ __attribute__((__format__(printf, 2, 3)));
+void putil_crit_pam(struct pam_args *, int, const char *, ...)
+ __attribute__((__format__(printf, 3, 4)));
+void putil_err(struct pam_args *, const char *, ...)
+ __attribute__((__format__(printf, 2, 3)));
+void putil_err_pam(struct pam_args *, int, const char *, ...)
+ __attribute__((__format__(printf, 3, 4)));
+void putil_notice(struct pam_args *, const char *, ...)
+ __attribute__((__format__(printf, 2, 3)));
+void putil_notice_pam(struct pam_args *, int, const char *, ...)
+ __attribute__((__format__(printf, 3, 4)));
+void putil_debug(struct pam_args *, const char *, ...)
+ __attribute__((__format__(printf, 2, 3)));
+void putil_debug_pam(struct pam_args *, int, const char *, ...)
+ __attribute__((__format__(printf, 3, 4)));
+
+/*
+ * The Kerberos versions of the PAM logging and debugging functions, which
+ * report the last Kerberos error. These are only available if built with
+ * Kerberos support.
+ */
+#ifdef HAVE_KRB5
+void putil_crit_krb5(struct pam_args *, int, const char *, ...)
+ __attribute__((__format__(printf, 3, 4)));
+void putil_err_krb5(struct pam_args *, int, const char *, ...)
+ __attribute__((__format__(printf, 3, 4)));
+void putil_notice_krb5(struct pam_args *, int, const char *, ...)
+ __attribute__((__format__(printf, 3, 4)));
+void putil_debug_krb5(struct pam_args *, int, const char *, ...)
+ __attribute__((__format__(printf, 3, 4)));
+#endif
+
+/* Log entry to a PAM function. */
+void putil_log_entry(struct pam_args *, const char *, int flags)
+ __attribute__((__nonnull__));
+
+/* Log an authentication failure. */
+void putil_log_failure(struct pam_args *, const char *, ...)
+ __attribute__((__nonnull__, __format__(printf, 2, 3)));
+
+/* Undo default visibility change. */
+#pragma GCC visibility pop
+
+END_DECLS
+
+/* __func__ is C99, but not provided by all implementations. */
+#if (__STDC_VERSION__ < 199901L) && !defined(__func__)
+# if (__GNUC__ >= 2)
+# define __func__ __FUNCTION__
+# else
+# define __func__ "<unknown>"
+# endif
+#endif
+
+/* Macros to record entry and exit from the main PAM functions. */
+#define ENTRY(args, flags) \
+ do { \
+ if (args->debug) \
+ putil_log_entry((args), __func__, (flags)); \
+ } while (0)
+#define EXIT(args, pamret) \
+ do { \
+ if (args != NULL && args->debug) \
+ pam_syslog( \
+ (args)->pamh, LOG_DEBUG, "%s: exit (%s)", __func__, \
+ ((pamret) == PAM_SUCCESS) \
+ ? "success" \
+ : (((pamret) == PAM_IGNORE) ? "ignore" : "failure")); \
+ } while (0)
+
+#endif /* !PAM_UTIL_LOGGING_H */
diff --git a/pam-util/options.c b/pam-util/options.c
new file mode 100644
index 000000000000..052e528a5be4
--- /dev/null
+++ b/pam-util/options.c
@@ -0,0 +1,720 @@
+/*
+ * Parse PAM options into a struct.
+ *
+ * Given a struct in which to store options and a specification for what
+ * options go where, parse both the PAM configuration options and any options
+ * from a Kerberos krb5.conf file and fill out the struct.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006-2008, 2010-2011, 2013-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+#include <pam-util/options.h>
+#include <pam-util/vector.h>
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+/*
+ * Macros used to resolve a void * pointer to the configuration struct and an
+ * offset into a pointer to the appropriate type. Scary violations of the C
+ * type system lurk here.
+ */
+/* clang-format off */
+#define CONF_BOOL(c, o) (bool *) (void *)((char *) (c) + (o))
+#define CONF_NUMBER(c, o) (long *) (void *)((char *) (c) + (o))
+#define CONF_STRING(c, o) (char **) (void *)((char *) (c) + (o))
+#define CONF_LIST(c, o) (struct vector **)(void *)((char *) (c) + (o))
+/* clang-format on */
+
+/*
+ * We can only process times properly if we have Kerberos. If not, they fall
+ * back to longs and we convert them as numbers.
+ */
+/* clang-format off */
+#ifdef HAVE_KRB5
+# define CONF_TIME(c, o) (krb5_deltat *)(void *)((char *) (c) + (o))
+#else
+# define CONF_TIME(c, o) (long *) (void *)((char *) (c) + (o))
+#endif
+/* clang-format on */
+
+
+/*
+ * Set a vector argument to its default. This needs to do a deep copy of the
+ * vector so that we can safely free it when freeing the configuration. Takes
+ * the PAM argument struct, the pointer in which to store the vector, and the
+ * default vector. Returns true if the default was set correctly and false on
+ * memory allocation failure, which is also reported with putil_crit().
+ */
+static bool
+copy_default_list(struct pam_args *args, struct vector **setting,
+ const struct vector *defval)
+{
+ struct vector *result = NULL;
+
+ *setting = NULL;
+ if (defval != NULL && defval->strings != NULL) {
+ result = vector_copy(defval);
+ if (result == NULL) {
+ putil_crit(args, "cannot allocate memory: %s", strerror(errno));
+ return false;
+ }
+ *setting = result;
+ }
+ return true;
+}
+
+
+/*
+ * Set a vector argument to a default based on a string. Takes the PAM
+ * argument struct,t he pointer into which to store the vector, and the
+ * default string. Returns true if the default was set correctly and false on
+ * memory allocation failure, which is also reported with putil_crit().
+ */
+static bool
+default_list_string(struct pam_args *args, struct vector **setting,
+ const char *defval)
+{
+ struct vector *result = NULL;
+
+ *setting = NULL;
+ if (defval != NULL) {
+ result = vector_split_multi(defval, " \t,", NULL);
+ if (result == NULL) {
+ putil_crit(args, "cannot allocate memory: %s", strerror(errno));
+ return false;
+ }
+ *setting = result;
+ }
+ return true;
+}
+
+
+/*
+ * Set the defaults for the PAM configuration. Takes the PAM arguments, an
+ * option table defined as above, and the number of entries in the table. The
+ * config member of the args struct must already be allocated. Returns true
+ * on success and false on error (generally out of memory). Errors will
+ * already be reported using putil_crit().
+ *
+ * This function must be called before either putil_args_krb5() or
+ * putil_args_parse(), since neither of those functions set defaults.
+ */
+bool
+putil_args_defaults(struct pam_args *args, const struct option options[],
+ size_t optlen)
+{
+ size_t opt;
+
+ for (opt = 0; opt < optlen; opt++) {
+ bool *bp;
+ long *lp;
+#ifdef HAVE_KRB5
+ krb5_deltat *tp;
+#else
+ long *tp;
+#endif
+ char **sp;
+ struct vector **vp;
+
+ switch (options[opt].type) {
+ case TYPE_BOOLEAN:
+ bp = CONF_BOOL(args->config, options[opt].location);
+ *bp = options[opt].defaults.boolean;
+ break;
+ case TYPE_NUMBER:
+ lp = CONF_NUMBER(args->config, options[opt].location);
+ *lp = options[opt].defaults.number;
+ break;
+ case TYPE_TIME:
+ tp = CONF_TIME(args->config, options[opt].location);
+ *tp = (krb5_deltat) options[opt].defaults.number;
+ break;
+ case TYPE_STRING:
+ sp = CONF_STRING(args->config, options[opt].location);
+ if (options[opt].defaults.string == NULL)
+ *sp = NULL;
+ else {
+ *sp = strdup(options[opt].defaults.string);
+ if (*sp == NULL) {
+ putil_crit(args, "cannot allocate memory: %s",
+ strerror(errno));
+ return false;
+ }
+ }
+ break;
+ case TYPE_LIST:
+ vp = CONF_LIST(args->config, options[opt].location);
+ if (!copy_default_list(args, vp, options[opt].defaults.list))
+ return false;
+ break;
+ case TYPE_STRLIST:
+ vp = CONF_LIST(args->config, options[opt].location);
+ if (!default_list_string(args, vp, options[opt].defaults.string))
+ return false;
+ break;
+ }
+ }
+ return true;
+}
+
+
+#ifdef HAVE_KRB5
+/*
+ * Load a boolean option from Kerberos appdefaults. Takes the PAM argument
+ * struct, the section name, the realm, the option, and the result location.
+ *
+ * The stupidity of rewriting the realm argument into a krb5_data is required
+ * by MIT Kerberos.
+ */
+static void
+default_boolean(struct pam_args *args, const char *section, const char *realm,
+ const char *opt, bool *result)
+{
+ int tmp;
+# ifdef HAVE_KRB5_REALM
+ krb5_const_realm rdata = realm;
+# else
+ krb5_data realm_struct;
+ const krb5_data *rdata;
+
+ if (realm == NULL)
+ rdata = NULL;
+ else {
+ rdata = &realm_struct;
+ realm_struct.magic = KV5M_DATA;
+ realm_struct.data = (void *) realm;
+ realm_struct.length = (unsigned int) strlen(realm);
+ }
+# endif
+
+ /*
+ * The MIT version of krb5_appdefault_boolean takes an int * and the
+ * Heimdal version takes a krb5_boolean *, so hope that Heimdal always
+ * defines krb5_boolean to int or this will require more portability work.
+ */
+ krb5_appdefault_boolean(args->ctx, section, rdata, opt, *result, &tmp);
+ *result = tmp;
+}
+
+
+/*
+ * Load a number option from Kerberos appdefaults. Takes the PAM argument
+ * struct, the section name, the realm, the option, and the result location.
+ * The native interface doesn't support numbers, so we actually read a string
+ * and then convert.
+ */
+static void
+default_number(struct pam_args *args, const char *section, const char *realm,
+ const char *opt, long *result)
+{
+ char *tmp = NULL;
+ char *end;
+ long value;
+# ifdef HAVE_KRB5_REALM
+ krb5_const_realm rdata = realm;
+# else
+ krb5_data realm_struct;
+ const krb5_data *rdata;
+
+ if (realm == NULL)
+ rdata = NULL;
+ else {
+ rdata = &realm_struct;
+ realm_struct.magic = KV5M_DATA;
+ realm_struct.data = (void *) realm;
+ realm_struct.length = (unsigned int) strlen(realm);
+ }
+# endif
+
+ krb5_appdefault_string(args->ctx, section, rdata, opt, "", &tmp);
+ if (tmp != NULL && tmp[0] != '\0') {
+ errno = 0;
+ value = strtol(tmp, &end, 10);
+ if (errno != 0 || *end != '\0')
+ putil_err(args, "invalid number in krb5.conf setting for %s: %s",
+ opt, tmp);
+ else
+ *result = value;
+ }
+ free(tmp);
+}
+
+
+/*
+ * Load a time option from Kerberos appdefaults. Takes the PAM argument
+ * struct, the section name, the realm, the option, and the result location.
+ * The native interface doesn't support numbers, so we actually read a string
+ * and then convert using krb5_string_to_deltat.
+ */
+static void
+default_time(struct pam_args *args, const char *section, const char *realm,
+ const char *opt, krb5_deltat *result)
+{
+ char *tmp = NULL;
+ krb5_deltat value;
+ krb5_error_code retval;
+# ifdef HAVE_KRB5_REALM
+ krb5_const_realm rdata = realm;
+# else
+ krb5_data realm_struct;
+ const krb5_data *rdata;
+
+ if (realm == NULL)
+ rdata = NULL;
+ else {
+ rdata = &realm_struct;
+ realm_struct.magic = KV5M_DATA;
+ realm_struct.data = (void *) realm;
+ realm_struct.length = (unsigned int) strlen(realm);
+ }
+# endif
+
+ krb5_appdefault_string(args->ctx, section, rdata, opt, "", &tmp);
+ if (tmp != NULL && tmp[0] != '\0') {
+ retval = krb5_string_to_deltat(tmp, &value);
+ if (retval != 0)
+ putil_err(args, "invalid time in krb5.conf setting for %s: %s",
+ opt, tmp);
+ else
+ *result = value;
+ }
+ free(tmp);
+}
+
+
+/*
+ * Load a string option from Kerberos appdefaults. Takes the PAM argument
+ * struct, the section name, the realm, the option, and the result location.
+ *
+ * This requires an annoying workaround because one cannot specify a default
+ * value of NULL with MIT Kerberos, since MIT Kerberos unconditionally calls
+ * strdup on the default value. There's also no way to determine if memory
+ * allocation failed while parsing or while setting the default value, so we
+ * don't return an error code.
+ */
+static void
+default_string(struct pam_args *args, const char *section, const char *realm,
+ const char *opt, char **result)
+{
+ char *value = NULL;
+# ifdef HAVE_KRB5_REALM
+ krb5_const_realm rdata = realm;
+# else
+ krb5_data realm_struct;
+ const krb5_data *rdata;
+
+ if (realm == NULL)
+ rdata = NULL;
+ else {
+ rdata = &realm_struct;
+ realm_struct.magic = KV5M_DATA;
+ realm_struct.data = (void *) realm;
+ realm_struct.length = (unsigned int) strlen(realm);
+ }
+# endif
+
+ krb5_appdefault_string(args->ctx, section, rdata, opt, "", &value);
+ if (value != NULL) {
+ if (value[0] == '\0')
+ free(value);
+ else {
+ if (*result != NULL)
+ free(*result);
+ *result = value;
+ }
+ }
+}
+
+
+/*
+ * Load a list option from Kerberos appdefaults. Takes the PAM arguments, the
+ * context, the section name, the realm, the option, and the result location.
+ *
+ * We may fail here due to memory allocation problems, in which case we return
+ * false to indicate that PAM setup should abort.
+ */
+static bool
+default_list(struct pam_args *args, const char *section, const char *realm,
+ const char *opt, struct vector **result)
+{
+ char *tmp = NULL;
+ struct vector *value;
+
+ default_string(args, section, realm, opt, &tmp);
+ if (tmp != NULL) {
+ value = vector_split_multi(tmp, " \t,", NULL);
+ if (value == NULL) {
+ free(tmp);
+ putil_crit(args, "cannot allocate vector: %s", strerror(errno));
+ return false;
+ }
+ if (*result != NULL)
+ vector_free(*result);
+ *result = value;
+ free(tmp);
+ }
+ return true;
+}
+
+
+/*
+ * The public interface for getting configuration information from krb5.conf.
+ * Takes the PAM arguments, the krb5.conf section, the options specification,
+ * and the number of options in the options table. The config member of the
+ * args struct must already be allocated. Iterate through the option list
+ * and, for every option where krb5_config is true, see if it's set in the
+ * Kerberos configuration.
+ *
+ * This looks obviously slow, but there haven't been any reports of problems
+ * and there's no better interface. But if you wonder where the cycles in
+ * your computer are getting wasted, well, here's one place.
+ */
+bool
+putil_args_krb5(struct pam_args *args, const char *section,
+ const struct option options[], size_t optlen)
+{
+ size_t i;
+ char *realm;
+ bool free_realm = false;
+
+ /* Having no local realm may be intentional, so don't report an error. */
+ if (args->realm != NULL)
+ realm = args->realm;
+ else {
+ if (krb5_get_default_realm(args->ctx, &realm) < 0)
+ realm = NULL;
+ else
+ free_realm = true;
+ }
+ for (i = 0; i < optlen; i++) {
+ const struct option *opt = &options[i];
+
+ if (!opt->krb5_config)
+ continue;
+ switch (opt->type) {
+ case TYPE_BOOLEAN:
+ default_boolean(args, section, realm, opt->name,
+ CONF_BOOL(args->config, opt->location));
+ break;
+ case TYPE_NUMBER:
+ default_number(args, section, realm, opt->name,
+ CONF_NUMBER(args->config, opt->location));
+ break;
+ case TYPE_TIME:
+ default_time(args, section, realm, opt->name,
+ CONF_TIME(args->config, opt->location));
+ break;
+ case TYPE_STRING:
+ default_string(args, section, realm, opt->name,
+ CONF_STRING(args->config, opt->location));
+ break;
+ case TYPE_LIST:
+ case TYPE_STRLIST:
+ if (!default_list(args, section, realm, opt->name,
+ CONF_LIST(args->config, opt->location)))
+ return false;
+ break;
+ }
+ }
+ if (free_realm)
+ krb5_free_default_realm(args->ctx, realm);
+ return true;
+}
+
+#else /* !HAVE_KRB5 */
+
+/*
+ * Stub function for getting configuration information from krb5.conf used
+ * when the PAM module is not built with Kerberos support so that the function
+ * can be called unconditionally.
+ */
+bool
+putil_args_krb5(struct pam_args *args UNUSED, const char *section UNUSED,
+ const struct option options[] UNUSED, size_t optlen UNUSED)
+{
+ return true;
+}
+
+#endif /* !HAVE_KRB5 */
+
+
+/*
+ * bsearch comparison function for finding PAM arguments in an array of struct
+ * options. We only compare up to the first '=' in the key so that we don't
+ * have to munge the string before searching.
+ */
+static int
+option_compare(const void *key, const void *member)
+{
+ const char *string = key;
+ const struct option *option = member;
+ const char *p;
+ size_t length;
+ int result;
+
+ p = strchr(string, '=');
+ if (p == NULL)
+ return strcmp(string, option->name);
+ else {
+ length = (size_t)(p - string);
+ if (length == 0)
+ return -1;
+ result = strncmp(string, option->name, length);
+ if (result == 0 && strlen(option->name) > length)
+ return -1;
+ return result;
+ }
+}
+
+
+/*
+ * Given a PAM argument, convert the value portion of the argument to a
+ * boolean and store it in the provided location. If the value is missing,
+ * that's equivalent to a true value. If the value is invalid, report an
+ * error and leave the location unchanged.
+ */
+static void
+convert_boolean(struct pam_args *args, const char *arg, bool *setting)
+{
+ const char *value;
+
+ value = strchr(arg, '=');
+ if (value == NULL)
+ *setting = true;
+ else {
+ value++;
+ /* clang-format off */
+ if ( strcasecmp(value, "true") == 0
+ || strcasecmp(value, "yes") == 0
+ || strcasecmp(value, "on") == 0
+ || strcmp (value, "1") == 0)
+ *setting = true;
+ else if ( strcasecmp(value, "false") == 0
+ || strcasecmp(value, "no") == 0
+ || strcasecmp(value, "off") == 0
+ || strcmp (value, "0") == 0)
+ *setting = false;
+ else
+ putil_err(args, "invalid boolean in setting: %s", arg);
+ /* clang-format on */
+ }
+}
+
+
+/*
+ * Given a PAM argument, convert the value portion of the argument to a number
+ * and store it in the provided location. If the value is missing or isn't a
+ * number, report an error and leave the location unchanged.
+ */
+static void
+convert_number(struct pam_args *args, const char *arg, long *setting)
+{
+ const char *value;
+ char *end;
+ long result;
+
+ value = strchr(arg, '=');
+ if (value == NULL || value[1] == '\0') {
+ putil_err(args, "value missing for option %s", arg);
+ return;
+ }
+ errno = 0;
+ result = strtol(value + 1, &end, 10);
+ if (errno != 0 || *end != '\0') {
+ putil_err(args, "invalid number in setting: %s", arg);
+ return;
+ }
+ *setting = result;
+}
+
+
+/*
+ * Given a PAM argument, convert the value portion of the argument from a
+ * Kerberos time string to a krb5_deltat and store it in the provided
+ * location. If the value is missing or isn't a number, report an error and
+ * leave the location unchanged.
+ */
+#ifdef HAVE_KRB5
+static void
+convert_time(struct pam_args *args, const char *arg, krb5_deltat *setting)
+{
+ const char *value;
+ krb5_deltat result;
+ krb5_error_code retval;
+
+ value = strchr(arg, '=');
+ if (value == NULL || value[1] == '\0') {
+ putil_err(args, "value missing for option %s", arg);
+ return;
+ }
+ retval = krb5_string_to_deltat((char *) value + 1, &result);
+ if (retval != 0)
+ putil_err(args, "bad time value in setting: %s", arg);
+ else
+ *setting = result;
+}
+
+#else /* HAVE_KRB5 */
+
+static void
+convert_time(struct pam_args *args, const char *arg, long *setting)
+{
+ convert_number(args, arg, setting);
+}
+
+#endif /* !HAVE_KRB5 */
+
+
+/*
+ * Given a PAM argument, convert the value portion of the argument to a string
+ * and store it in the provided location. If the value is missing, report an
+ * error and leave the location unchanged, returning true since that's a
+ * non-fatal error. If memory allocation fails, return false, since PAM setup
+ * should abort.
+ */
+static bool
+convert_string(struct pam_args *args, const char *arg, char **setting)
+{
+ const char *value;
+ char *result;
+
+ value = strchr(arg, '=');
+ if (value == NULL) {
+ putil_err(args, "value missing for option %s", arg);
+ return true;
+ }
+ result = strdup(value + 1);
+ if (result == NULL) {
+ putil_crit(args, "cannot allocate memory: %s", strerror(errno));
+ return false;
+ }
+ free(*setting);
+ *setting = result;
+ return true;
+}
+
+
+/*
+ * Given a PAM argument, convert the value portion of the argument to a vector
+ * and store it in the provided location. If the value is missing, report an
+ * error and leave the location unchanged, returning true since that's a
+ * non-fatal error. If memory allocation fails, return false, since PAM setup
+ * should abort.
+ */
+static bool
+convert_list(struct pam_args *args, const char *arg, struct vector **setting)
+{
+ const char *value;
+ struct vector *result;
+
+ value = strchr(arg, '=');
+ if (value == NULL) {
+ putil_err(args, "value missing for option %s", arg);
+ return true;
+ }
+ result = vector_split_multi(value + 1, " \t,", NULL);
+ if (result == NULL) {
+ putil_crit(args, "cannot allocate vector: %s", strerror(errno));
+ return false;
+ }
+ vector_free(*setting);
+ *setting = result;
+ return true;
+}
+
+
+/*
+ * Parse the PAM arguments. Takes the PAM argument struct, the argument count
+ * and vector, the option table, and the number of elements in the option
+ * table. The config member of the args struct must already be allocated.
+ * Returns true on success and false on error. An error return should be
+ * considered fatal. Report errors using putil_crit(). Unknown options will
+ * also be diagnosed (to syslog at LOG_ERR using putil_err()), but are not
+ * considered fatal errors and will still return true.
+ *
+ * If options should be retrieved from krb5.conf, call putil_args_krb5()
+ * first, before calling this function.
+ */
+bool
+putil_args_parse(struct pam_args *args, int argc, const char *argv[],
+ const struct option options[], size_t optlen)
+{
+ int i;
+ const struct option *option;
+
+ /*
+ * Second pass: find each option we were given and set the corresponding
+ * configuration parameter.
+ */
+ for (i = 0; i < argc; i++) {
+ option = bsearch(argv[i], options, optlen, sizeof(struct option),
+ option_compare);
+ if (option == NULL) {
+ putil_err(args, "unknown option %s", argv[i]);
+ continue;
+ }
+ switch (option->type) {
+ case TYPE_BOOLEAN:
+ convert_boolean(args, argv[i],
+ CONF_BOOL(args->config, option->location));
+ break;
+ case TYPE_NUMBER:
+ convert_number(args, argv[i],
+ CONF_NUMBER(args->config, option->location));
+ break;
+ case TYPE_TIME:
+ convert_time(args, argv[i],
+ CONF_TIME(args->config, option->location));
+ break;
+ case TYPE_STRING:
+ if (!convert_string(args, argv[i],
+ CONF_STRING(args->config, option->location)))
+ return false;
+ break;
+ case TYPE_LIST:
+ case TYPE_STRLIST:
+ if (!convert_list(args, argv[i],
+ CONF_LIST(args->config, option->location)))
+ return false;
+ break;
+ }
+ }
+ return true;
+}
diff --git a/pam-util/options.h b/pam-util/options.h
new file mode 100644
index 000000000000..062d095e8e7e
--- /dev/null
+++ b/pam-util/options.h
@@ -0,0 +1,205 @@
+/*
+ * Interface to PAM option parsing.
+ *
+ * This interface defines a lot of macros and types with very short names, and
+ * hence without a lot of namespace protection. It should be included only in
+ * the file that's doing the option parsing and not elsewhere to remove the
+ * risk of clashes.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef PAM_UTIL_OPTIONS_H
+#define PAM_UTIL_OPTIONS_H 1
+
+#include <config.h>
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+#include <portable/macros.h>
+#include <portable/stdbool.h>
+
+#include <stddef.h>
+
+/* Forward declarations to avoid additional includes. */
+struct vector;
+
+/*
+ * The types of configuration values possible. STRLIST is a list data type
+ * that takes its default from a string value instead of a vector. For
+ * STRLIST, the default string value will be turned into a vector by splitting
+ * on comma, space, and tab. (This is the same as would be done with the
+ * value of a PAM setting when the target variable type is a list.)
+ */
+enum type
+{
+ TYPE_BOOLEAN,
+ TYPE_NUMBER,
+ TYPE_TIME,
+ TYPE_STRING,
+ TYPE_LIST,
+ TYPE_STRLIST
+};
+
+/*
+ * Each configuration option is defined by a struct option. This specifies
+ * the name of the option, its offset into the configuration struct, whether
+ * it can be specified in a krb5.conf file, its type, and its default value if
+ * not set. Note that PAM configuration options are specified as strings, so
+ * there's no native way of representing a list argument. List values are
+ * always initialized by splitting a string on whitespace or commas.
+ *
+ * The default value should really be a union, but you can't initialize unions
+ * properly in C in a static initializer without C99 named initializer
+ * support, which we can't (yet) assume. So use a struct instead, and
+ * initialize all the members, even though we'll only care about one of them.
+ *
+ * Note that numbers set in the configuration struct created by this interface
+ * must be longs, not ints. There is currently no provision for unsigned
+ * numbers.
+ *
+ * Times take their default from defaults.number. The difference between time
+ * and number is in the parsing of a user-supplied value and the type of the
+ * stored attribute.
+ */
+struct option {
+ const char *name;
+ size_t location;
+ bool krb5_config;
+ enum type type;
+ struct {
+ bool boolean;
+ long number;
+ const char *string;
+ const struct vector *list;
+ } defaults;
+};
+
+/*
+ * The following macros are helpers to make it easier to define the table that
+ * specifies how to convert the configuration into a struct. They provide an
+ * initializer for the type and default fields.
+ */
+/* clang-format off */
+#define BOOL(def) TYPE_BOOLEAN, { (def), 0, NULL, NULL }
+#define NUMBER(def) TYPE_NUMBER, { 0, (def), NULL, NULL }
+#define TIME(def) TYPE_TIME, { 0, (def), NULL, NULL }
+#define STRING(def) TYPE_STRING, { 0, 0, (def), NULL }
+#define LIST(def) TYPE_LIST, { 0, 0, NULL, (def) }
+#define STRLIST(def) TYPE_STRLIST, { 0, 0, (def), NULL }
+/* clang-format on */
+
+/*
+ * The user of this file should also define a macro of the following form:
+ *
+ * #define K(name) (#name), offsetof(struct pam_config, name)
+ *
+ * Then, the definition of the necessary table for building the configuration
+ * will look something like this:
+ *
+ * const struct option options[] = {
+ * { K(aklog_homedir), true, BOOL (false) },
+ * { K(cells), true, LIST (NULL) },
+ * { K(debug), false, BOOL (false) },
+ * { K(minimum_uid), true, NUMBER (0) },
+ * { K(program), true, STRING (NULL) },
+ * };
+ *
+ * which provides a nice, succinct syntax for creating the table. The options
+ * MUST be in sorted order, since the options parsing code does a binary
+ * search.
+ */
+
+BEGIN_DECLS
+
+/* Default to a hidden visibility for all internal functions. */
+#pragma GCC visibility push(hidden)
+
+/*
+ * Set the defaults for the PAM configuration. Takes the PAM arguments, an
+ * option table defined as above, and the number of entries in the table. The
+ * config member of the args struct must already be allocated. Returns true
+ * on success and false on error (generally out of memory). Errors will
+ * already be reported using putil_crit().
+ *
+ * This function must be called before either putil_args_krb5() or
+ * putil_args_parse(), since neither of those functions set defaults.
+ */
+bool putil_args_defaults(struct pam_args *, const struct option options[],
+ size_t optlen) __attribute__((__nonnull__));
+
+/*
+ * Fill out options from krb5.conf. Takes the PAM args structure, the name of
+ * the section for the software being configured, an option table defined as
+ * above, and the number of entries in the table. The config member of the
+ * args struct must already be allocated. Only those options whose
+ * krb5_config attribute is true will be considered.
+ *
+ * This code automatically checks for configuration settings scoped to the
+ * local realm, so the default realm should be set before calling this
+ * function. If that's done based on a configuration option, one may need to
+ * pre-parse the configuration options.
+ *
+ * Returns true on success and false on an error. An error return should be
+ * considered fatal. Errors will already be reported using putil_crit*() or
+ * putil_err*() as appropriate. If Kerberos is not available, returns without
+ * doing anything.
+ *
+ * putil_args_defaults() should be called before this function.
+ */
+bool putil_args_krb5(struct pam_args *, const char *section,
+ const struct option options[], size_t optlen)
+ __attribute__((__nonnull__));
+
+/*
+ * Parse the PAM arguments and fill out the provided struct. Takes the PAM
+ * arguments, the argument count and vector, an option table defined as above,
+ * and the number of entries in the table. The config member of the args
+ * struct must already be allocated. Returns true on success and false on
+ * error. An error return should be considered fatal. Errors will already be
+ * reported using putil_crit(). Unknown options will also be diagnosed (to
+ * syslog at LOG_ERR using putil_err()), but are not considered fatal errors
+ * and will still return true.
+ *
+ * The krb5_config option of the option configuration is ignored by this
+ * function. If options should be retrieved from krb5.conf, call
+ * putil_args_krb5() first, before calling this function.
+ *
+ * putil_args_defaults() should be called before this function.
+ */
+bool putil_args_parse(struct pam_args *, int argc, const char *argv[],
+ const struct option options[], size_t optlen)
+ __attribute__((__nonnull__));
+
+/* Undo default visibility change. */
+#pragma GCC visibility pop
+
+END_DECLS
+
+#endif /* !PAM_UTIL_OPTIONS_H */
diff --git a/pam-util/vector.c b/pam-util/vector.c
new file mode 100644
index 000000000000..012a9aef24a3
--- /dev/null
+++ b/pam-util/vector.c
@@ -0,0 +1,289 @@
+/*
+ * Vector handling (counted lists of char *'s).
+ *
+ * A vector is a table for handling a list of strings with less overhead than
+ * linked list. The intention is for vectors, once allocated, to be reused;
+ * this saves on memory allocations once the array of char *'s reaches a
+ * stable size.
+ *
+ * This is based on the util/vector.c library, but that library uses xmalloc
+ * routines to exit the program if memory allocation fails. This is a
+ * modified version of the vector library that instead returns false on
+ * failure to allocate memory, allowing the caller to do appropriate recovery.
+ *
+ * Vectors require list of strings, not arbitrary binary data, and cannot
+ * handle data elements containing nul characters.
+ *
+ * Only the portions of the vector library used by PAM modules are
+ * implemented.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017-2018 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <pam-util/vector.h>
+
+
+/*
+ * Allocate a new, empty vector. Returns NULL if memory allocation fails.
+ */
+struct vector *
+vector_new(void)
+{
+ struct vector *vector;
+
+ vector = calloc(1, sizeof(struct vector));
+ vector->allocated = 1;
+ vector->strings = calloc(1, sizeof(char *));
+ return vector;
+}
+
+
+/*
+ * Allocate a new vector that's a copy of an existing vector. Returns NULL if
+ * memory allocation fails.
+ */
+struct vector *
+vector_copy(const struct vector *old)
+{
+ struct vector *vector;
+ size_t i;
+
+ vector = vector_new();
+ if (!vector_resize(vector, old->count)) {
+ vector_free(vector);
+ return NULL;
+ }
+ vector->count = old->count;
+ for (i = 0; i < old->count; i++) {
+ vector->strings[i] = strdup(old->strings[i]);
+ if (vector->strings[i] == NULL) {
+ vector_free(vector);
+ return NULL;
+ }
+ }
+ return vector;
+}
+
+
+/*
+ * Resize a vector (using reallocarray to resize the table). Return false if
+ * memory allocation fails.
+ */
+bool
+vector_resize(struct vector *vector, size_t size)
+{
+ size_t i;
+ char **strings;
+
+ if (vector->count > size) {
+ for (i = size; i < vector->count; i++)
+ free(vector->strings[i]);
+ vector->count = size;
+ }
+ if (size == 0)
+ size = 1;
+ strings = reallocarray(vector->strings, size, sizeof(char *));
+ if (strings == NULL)
+ return false;
+ vector->strings = strings;
+ vector->allocated = size;
+ return true;
+}
+
+
+/*
+ * Add a new string to the vector, resizing the vector as necessary. The
+ * vector is resized an element at a time; if a lot of resizes are expected,
+ * vector_resize should be called explicitly with a more suitable size.
+ * Return false if memory allocation fails.
+ */
+bool
+vector_add(struct vector *vector, const char *string)
+{
+ size_t next = vector->count;
+
+ if (vector->count == vector->allocated)
+ if (!vector_resize(vector, vector->allocated + 1))
+ return false;
+ vector->strings[next] = strdup(string);
+ if (vector->strings[next] == NULL)
+ return false;
+ vector->count++;
+ return true;
+}
+
+
+/*
+ * Empty a vector but keep the allocated memory for the pointer table.
+ */
+void
+vector_clear(struct vector *vector)
+{
+ size_t i;
+
+ for (i = 0; i < vector->count; i++)
+ if (vector->strings[i] != NULL)
+ free(vector->strings[i]);
+ vector->count = 0;
+}
+
+
+/*
+ * Free a vector completely.
+ */
+void
+vector_free(struct vector *vector)
+{
+ if (vector == NULL)
+ return;
+ vector_clear(vector);
+ free(vector->strings);
+ free(vector);
+}
+
+
+/*
+ * Given a vector that we may be reusing, clear it out. If the first argument
+ * is NULL, allocate a new vector. Used by vector_split*. Returns NULL if
+ * memory allocation fails.
+ */
+static struct vector *
+vector_reuse(struct vector *vector)
+{
+ if (vector == NULL)
+ return vector_new();
+ else {
+ vector_clear(vector);
+ return vector;
+ }
+}
+
+
+/*
+ * Given a string and a set of separators expressed as a string, count the
+ * number of strings that it will split into when splitting on those
+ * separators.
+ */
+static size_t
+split_multi_count(const char *string, const char *seps)
+{
+ const char *p;
+ size_t count;
+
+ if (*string == '\0')
+ return 0;
+ for (count = 1, p = string + 1; *p != '\0'; p++)
+ if (strchr(seps, *p) != NULL && strchr(seps, p[-1]) == NULL)
+ count++;
+
+ /*
+ * If the string ends in separators, we've overestimated the number of
+ * strings by one.
+ */
+ if (strchr(seps, p[-1]) != NULL)
+ count--;
+ return count;
+}
+
+
+/*
+ * Given a string, split it at any of the provided separators to form a
+ * vector, copying each string segment. If the third argument isn't NULL,
+ * reuse that vector; otherwise, allocate a new one. Any number of
+ * consecutive separators are considered a single separator. Returns NULL on
+ * memory allocation failure, after which the provided vector may only have
+ * partial results.
+ */
+struct vector *
+vector_split_multi(const char *string, const char *seps, struct vector *vector)
+{
+ const char *p, *start;
+ size_t i, count;
+ bool created = false;
+
+ if (vector == NULL)
+ created = true;
+ vector = vector_reuse(vector);
+ if (vector == NULL)
+ return NULL;
+
+ count = split_multi_count(string, seps);
+ if (vector->allocated < count && !vector_resize(vector, count))
+ goto fail;
+
+ vector->count = 0;
+ for (start = string, p = string, i = 0; *p != '\0'; p++)
+ if (strchr(seps, *p) != NULL) {
+ if (start != p) {
+ vector->strings[i] = strndup(start, (size_t)(p - start));
+ if (vector->strings[i] == NULL)
+ goto fail;
+ i++;
+ vector->count++;
+ }
+ start = p + 1;
+ }
+ if (start != p) {
+ vector->strings[i] = strndup(start, (size_t)(p - start));
+ if (vector->strings[i] == NULL)
+ goto fail;
+ vector->count++;
+ }
+ return vector;
+
+fail:
+ if (created)
+ vector_free(vector);
+ return NULL;
+}
+
+
+/*
+ * Given a vector and a path to a program, exec that program with the vector
+ * as its arguments. This requires adding a NULL terminator to the vector and
+ * casting it appropriately. Returns 0 on success and -1 on error, like exec
+ * does.
+ */
+int
+vector_exec(const char *path, struct vector *vector)
+{
+ if (vector->allocated == vector->count)
+ if (!vector_resize(vector, vector->count + 1))
+ return -1;
+ vector->strings[vector->count] = NULL;
+ return execv(path, (char *const *) vector->strings);
+}
+
+
+/*
+ * Given a vector, a path to a program, and the environment, exec that program
+ * with the vector as its arguments and the given environment. This requires
+ * adding a NULL terminator to the vector and casting it appropriately.
+ * Returns 0 on success and -1 on error, like exec does.
+ */
+int
+vector_exec_env(const char *path, struct vector *vector,
+ const char *const env[])
+{
+ if (vector->allocated == vector->count)
+ if (!vector_resize(vector, vector->count + 1))
+ return -1;
+ vector->strings[vector->count] = NULL;
+ return execve(path, (char *const *) vector->strings, (char *const *) env);
+}
diff --git a/pam-util/vector.h b/pam-util/vector.h
new file mode 100644
index 000000000000..351c53f8d40b
--- /dev/null
+++ b/pam-util/vector.h
@@ -0,0 +1,120 @@
+/*
+ * Prototypes for vector handling.
+ *
+ * A vector is a list of strings, with dynamic resizing of the list as new
+ * strings are added and support for various operations on strings (such as
+ * splitting them on delimiters).
+ *
+ * Vectors require list of strings, not arbitrary binary data, and cannot
+ * handle data elements containing nul characters.
+ *
+ * This is based on the util/vector.c library, but that library uses xmalloc
+ * routines to exit the program if memory allocation fails. This is a
+ * modified version of the vector library that instead returns false on
+ * failure to allocate memory, allowing the caller to do appropriate recovery.
+ *
+ * Only the portions of the vector library used by PAM modules are
+ * implemented.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#ifndef PAM_UTIL_VECTOR_H
+#define PAM_UTIL_VECTOR_H 1
+
+#include <config.h>
+#include <portable/macros.h>
+#include <portable/stdbool.h>
+
+#include <stddef.h>
+
+struct vector {
+ size_t count;
+ size_t allocated;
+ char **strings;
+};
+
+BEGIN_DECLS
+
+/* Default to a hidden visibility for all util functions. */
+#pragma GCC visibility push(hidden)
+
+/* Create a new, empty vector. Returns NULL on memory allocation failure. */
+struct vector *vector_new(void) __attribute__((__malloc__));
+
+/*
+ * Create a new vector that's a copy of an existing vector. Returns NULL on
+ * memory allocation failure.
+ */
+struct vector *vector_copy(const struct vector *)
+ __attribute__((__malloc__, __nonnull__));
+
+/*
+ * Add a string to a vector. Resizes the vector if necessary. Returns false
+ * on failure to allocate memory.
+ */
+bool vector_add(struct vector *, const char *string)
+ __attribute__((__nonnull__));
+
+/*
+ * Resize the array of strings to hold size entries. Saves reallocation work
+ * in vector_add if it's known in advance how many entries there will be.
+ * Returns false on failure to allocate memory.
+ */
+bool vector_resize(struct vector *, size_t size) __attribute__((__nonnull__));
+
+/*
+ * Reset the number of elements to zero, freeing all of the strings for a
+ * regular vector, but not freeing the strings array (to cut down on memory
+ * allocations if the vector will be reused).
+ */
+void vector_clear(struct vector *) __attribute__((__nonnull__));
+
+/* Free the vector and all resources allocated for it. */
+void vector_free(struct vector *);
+
+/*
+ * Split functions build a vector from a string. vector_split_multi splits on
+ * a set of characters. If the vector argument is NULL, a new vector is
+ * allocated; otherwise, the provided one is reused. Returns NULL on memory
+ * allocation failure, after which the provided vector may have been modified
+ * to only have partial results.
+ *
+ * Empty strings will yield zero-length vectors. Adjacent delimiters are
+ * treated as a single delimiter by vector_split_multi. Any leading or
+ * trailing delimiters are ignored, so this function will never create
+ * zero-length strings (similar to the behavior of strtok).
+ */
+struct vector *vector_split_multi(const char *string, const char *seps,
+ struct vector *)
+ __attribute__((__nonnull__(1, 2)));
+
+/*
+ * Exec the given program with the vector as its arguments. Return behavior
+ * is the same as execv. Note the argument order is different than the other
+ * vector functions (but the same as execv). The vector_exec_env variant
+ * calls execve and passes in the environment for the program.
+ */
+int vector_exec(const char *path, struct vector *)
+ __attribute__((__nonnull__));
+int vector_exec_env(const char *path, struct vector *, const char *const env[])
+ __attribute__((__nonnull__));
+
+/* Undo default visibility change. */
+#pragma GCC visibility pop
+
+END_DECLS
+
+#endif /* UTIL_VECTOR_H */
diff --git a/portable/asprintf.c b/portable/asprintf.c
new file mode 100644
index 000000000000..0451a03ed190
--- /dev/null
+++ b/portable/asprintf.c
@@ -0,0 +1,84 @@
+/*
+ * Replacement for a missing asprintf and vasprintf.
+ *
+ * Provides the same functionality as the standard GNU library routines
+ * asprintf and vasprintf for those platforms that don't have them.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006, 2015 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2008-2009, 2011, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/macros.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+/*
+ * If we're running the test suite, rename the functions to avoid conflicts
+ * with the system versions.
+ */
+#if TESTING
+# undef asprintf
+# undef vasprintf
+# define asprintf test_asprintf
+# define vasprintf test_vasprintf
+int test_asprintf(char **, const char *, ...)
+ __attribute__((__format__(printf, 2, 3)));
+int test_vasprintf(char **, const char *, va_list)
+ __attribute__((__format__(printf, 2, 0)));
+#endif
+
+
+int
+asprintf(char **strp, const char *fmt, ...)
+{
+ va_list args;
+ int status;
+
+ va_start(args, fmt);
+ status = vasprintf(strp, fmt, args);
+ va_end(args);
+ return status;
+}
+
+
+int
+vasprintf(char **strp, const char *fmt, va_list args)
+{
+ va_list args_copy;
+ int status, needed, oerrno;
+
+ va_copy(args_copy, args);
+ needed = vsnprintf(NULL, 0, fmt, args_copy);
+ va_end(args_copy);
+ if (needed < 0) {
+ *strp = NULL;
+ return needed;
+ }
+ *strp = malloc(needed + 1);
+ if (*strp == NULL)
+ return -1;
+ status = vsnprintf(*strp, needed + 1, fmt, args);
+ if (status >= 0)
+ return status;
+ else {
+ oerrno = errno;
+ free(*strp);
+ *strp = NULL;
+ errno = oerrno;
+ return status;
+ }
+}
diff --git a/portable/dummy.c b/portable/dummy.c
new file mode 100644
index 000000000000..121a7343edd0
--- /dev/null
+++ b/portable/dummy.c
@@ -0,0 +1,33 @@
+/*
+ * Dummy symbol to prevent an empty library.
+ *
+ * On platforms that already have all of the functions that libportable would
+ * supply, Automake builds an empty library and then calls ar with nonsensical
+ * arguments. Ensure that libportable always contains at least one symbol.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2008, 2011, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <portable/macros.h>
+
+/* Prototype to avoid gcc warnings and set visibility. */
+int portable_dummy(void) __attribute__((__const__, __visibility__("hidden")));
+
+int
+portable_dummy(void)
+{
+ return 42;
+}
diff --git a/portable/issetugid.c b/portable/issetugid.c
new file mode 100644
index 000000000000..2e37185df520
--- /dev/null
+++ b/portable/issetugid.c
@@ -0,0 +1,35 @@
+/*
+ * Replacement for a missing issetugid.
+ *
+ * Simulates the functionality as the Solaris function issetugid, which
+ * returns true if the running program was setuid or setgid. The replacement
+ * test is not quite as comprehensive as what the Solaris function does, but
+ * it should be good enough.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+int
+issetugid(void)
+{
+ if (getuid() != geteuid())
+ return 1;
+ if (getgid() != getegid())
+ return 1;
+ return 0;
+}
diff --git a/portable/kadmin.h b/portable/kadmin.h
new file mode 100644
index 000000000000..875682d986b5
--- /dev/null
+++ b/portable/kadmin.h
@@ -0,0 +1,82 @@
+/*
+ * Portability wrapper around kadm5/admin.h.
+ *
+ * This header adjusts for differences between the MIT and Heimdal kadmin
+ * client libraries so that the code can be written to a consistent API
+ * (favoring the Heimdal API as the exposed one).
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2015 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#ifndef PORTABLE_KADMIN_H
+#define PORTABLE_KADMIN_H 1
+
+#include <config.h>
+
+#include <kadm5/admin.h>
+#ifdef HAVE_KADM5_KADM5_ERR_H
+# include <kadm5/kadm5_err.h>
+#else
+# include <kadm5/kadm_err.h>
+#endif
+
+/*
+ * MIT as of 1.10 supports version 3. Heimdal as of 1.5 has a maximum version
+ * of 2. Define a KADM5_API_VERSION symbol that holds the maximum version.
+ * (Heimdal does this for us, so we only have to do that with MIT, but be
+ * general just in case.)
+ */
+#ifndef KADM5_API_VERSION
+# ifdef KADM5_API_VERSION_3
+# define KADM5_API_VERSION KADM5_API_VERSION_3
+# else
+# define KADM5_API_VERSION KADM5_API_VERSION_2
+# endif
+#endif
+
+/* Heimdal doesn't define KADM5_PASS_Q_GENERIC. */
+#ifndef KADM5_PASS_Q_GENERIC
+# define KADM5_PASS_Q_GENERIC KADM5_PASS_Q_DICT
+#endif
+
+/* Heimdal doesn't define KADM5_MISSING_KRB5_CONF_PARAMS. */
+#ifndef KADM5_MISSING_KRB5_CONF_PARAMS
+# define KADM5_MISSING_KRB5_CONF_PARAMS KADM5_MISSING_CONF_PARAMS
+#endif
+
+/*
+ * MIT Kerberos provides this function for pure kadmin clients to get a
+ * Kerberos context. With Heimdal, just use krb5_init_context.
+ */
+#ifndef HAVE_KADM5_INIT_KRB5_CONTEXT
+# define kadm5_init_krb5_context(c) krb5_init_context(c)
+#endif
+
+/*
+ * Heimdal provides _ctx functions that take an existing context. MIT always
+ * requires the context be passed in. Code should use the _ctx variant, and
+ * the below will fix it up if built against MIT.
+ *
+ * MIT also doesn't have a const prototype for the server argument, so cast it
+ * so that we can use the KADM5_ADMIN_SERVICE define.
+ */
+#ifndef HAVE_KADM5_INIT_WITH_SKEY_CTX
+# define kadm5_init_with_skey_ctx(c, u, k, s, p, sv, av, h) \
+ kadm5_init_with_skey((c), (u), (k), (char *) (s), (p), (sv), (av), \
+ NULL, (h))
+#endif
+
+#endif /* !PORTABLE_KADMIN_H */
diff --git a/portable/krb5-extra.c b/portable/krb5-extra.c
new file mode 100644
index 000000000000..d819e6635aef
--- /dev/null
+++ b/portable/krb5-extra.c
@@ -0,0 +1,186 @@
+/*
+ * Portability glue functions for Kerberos.
+ *
+ * This file provides definitions of the interfaces that portable/krb5.h
+ * ensures exist if the function wasn't available in the Kerberos libraries.
+ * Everything in this file will be protected by #ifndef. If the native
+ * Kerberos libraries are fully capable, this file will be skipped.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2015-2016, 2018 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/macros.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+/* Figure out what header files to include for error reporting. */
+#if !defined(HAVE_KRB5_GET_ERROR_MESSAGE) && !defined(HAVE_KRB5_GET_ERR_TEXT)
+# if !defined(HAVE_KRB5_GET_ERROR_STRING)
+# if defined(HAVE_IBM_SVC_KRB5_SVC_H)
+# include <ibm_svc/krb5_svc.h>
+# elif defined(HAVE_ET_COM_ERR_H)
+# include <et/com_err.h>
+# elif defined(HAVE_KERBEROSV5_COM_ERR_H)
+# include <kerberosv5/com_err.h>
+# else
+# include <com_err.h>
+# endif
+# endif
+#endif
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+/*
+ * This string is returned for unknown error messages. We use a static
+ * variable so that we can be sure not to free it.
+ */
+#if !defined(HAVE_KRB5_GET_ERROR_MESSAGE) \
+ || !defined(HAVE_KRB5_FREE_ERROR_MESSAGE)
+static const char error_unknown[] = "unknown error";
+#endif
+
+
+#ifndef HAVE_KRB5_CC_GET_FULL_NAME
+/*
+ * Given a Kerberos ticket cache, return the full name (TYPE:name) in
+ * newly-allocated memory. Returns an error code. Avoid asprintf and
+ * snprintf here in case someone wants to use this code without the rest of
+ * the portability layer.
+ */
+krb5_error_code
+krb5_cc_get_full_name(krb5_context ctx, krb5_ccache ccache, char **out)
+{
+ const char *type, *name;
+ size_t length;
+
+ type = krb5_cc_get_type(ctx, ccache);
+ if (type == NULL)
+ type = "FILE";
+ name = krb5_cc_get_name(ctx, ccache);
+ if (name == NULL)
+ return EINVAL;
+ length = strlen(type) + 1 + strlen(name) + 1;
+ *out = malloc(length);
+ if (*out == NULL)
+ return errno;
+ sprintf(*out, "%s:%s", type, name);
+ return 0;
+}
+#endif /* !HAVE_KRB5_CC_GET_FULL_NAME */
+
+
+#ifndef HAVE_KRB5_GET_ERROR_MESSAGE
+/*
+ * Given a Kerberos error code, return the corresponding error. Prefer the
+ * Kerberos interface if available since it will provide context-specific
+ * error information, whereas the error_message() call will only provide a
+ * fixed message.
+ */
+const char *
+krb5_get_error_message(krb5_context ctx UNUSED, krb5_error_code code UNUSED)
+{
+ const char *msg;
+
+# if defined(HAVE_KRB5_GET_ERROR_STRING)
+ msg = krb5_get_error_string(ctx);
+# elif defined(HAVE_KRB5_GET_ERR_TEXT)
+ msg = krb5_get_err_text(ctx, code);
+# elif defined(HAVE_KRB5_SVC_GET_MSG)
+ krb5_svc_get_msg(code, (char **) &msg);
+# else
+ msg = error_message(code);
+# endif
+ if (msg == NULL)
+ return error_unknown;
+ else
+ return msg;
+}
+#endif /* !HAVE_KRB5_GET_ERROR_MESSAGE */
+
+
+#ifndef HAVE_KRB5_FREE_ERROR_MESSAGE
+/*
+ * Free an error string if necessary. If we returned a static string, make
+ * sure we don't free it.
+ *
+ * This code assumes that the set of implementations that have
+ * krb5_free_error_message is a subset of those with krb5_get_error_message.
+ * If this assumption ever breaks, we may call the wrong free function.
+ */
+void
+krb5_free_error_message(krb5_context ctx UNUSED, const char *msg)
+{
+ if (msg == error_unknown)
+ return;
+# if defined(HAVE_KRB5_GET_ERROR_STRING)
+ krb5_free_error_string(ctx, (char *) msg);
+# elif defined(HAVE_KRB5_SVC_GET_MSG)
+ krb5_free_string(ctx, (char *) msg);
+# endif
+}
+#endif /* !HAVE_KRB5_FREE_ERROR_MESSAGE */
+
+
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC
+/*
+ * Allocate and initialize a krb5_get_init_creds_opt struct. This code
+ * assumes that an all-zero bit pattern will create a NULL pointer.
+ */
+krb5_error_code
+krb5_get_init_creds_opt_alloc(krb5_context ctx UNUSED,
+ krb5_get_init_creds_opt **opts)
+{
+ *opts = calloc(1, sizeof(krb5_get_init_creds_opt));
+ if (*opts == NULL)
+ return errno;
+ krb5_get_init_creds_opt_init(*opts);
+ return 0;
+}
+#endif /* !HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC */
+
+
+#ifndef HAVE_KRB5_PRINCIPAL_GET_REALM
+/*
+ * Return the realm of a principal as a const char *.
+ */
+const char *
+krb5_principal_get_realm(krb5_context ctx UNUSED, krb5_const_principal princ)
+{
+ const krb5_data *data;
+
+ data = krb5_princ_realm(ctx, princ);
+ if (data == NULL || data->data == NULL)
+ return NULL;
+ return data->data;
+}
+#endif /* !HAVE_KRB5_PRINCIPAL_GET_REALM */
+
+
+#ifndef HAVE_KRB5_VERIFY_INIT_CREDS_OPT_INIT
+/*
+ * Initialize the option struct for krb5_verify_init_creds.
+ */
+void
+krb5_verify_init_creds_opt_init(krb5_verify_init_creds_opt *opt)
+{
+ opt->flags = 0;
+ opt->ap_req_nofail = 0;
+}
+#endif
diff --git a/portable/krb5-profile.c b/portable/krb5-profile.c
new file mode 100644
index 000000000000..582e7ac76672
--- /dev/null
+++ b/portable/krb5-profile.c
@@ -0,0 +1,237 @@
+/*
+ * Kerberos compatibility functions for AIX's NAS libraries.
+ *
+ * AIX for some reason doesn't provide the krb5_appdefault_* functions, but
+ * does provide the underlying profile library functions (as a separate
+ * libk5profile with a separate k5profile.h header file).
+ *
+ * This file is therefore (apart from the includes, opening and closing
+ * comments, and the spots marked with an rra-c-util comment) a verbatim copy
+ * of src/lib/krb5/krb/appdefault.c from MIT Kerberos 1.4.4.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Copyright 1985-2005 by the Massachusetts Institute of Technology.
+ * For license information, see the end of this file.
+ */
+
+#include <config.h>
+
+#include <krb5.h>
+#ifdef HAVE_K5PROFILE_H
+# include <k5profile.h>
+#endif
+#ifdef HAVE_PROFILE_H
+# include <profile.h>
+#endif
+#include <stdio.h>
+#include <string.h>
+
+ /*xxx Duplicating this is annoying; try to work on a better way.*/
+static const char *const conf_yes[] = {
+ "y", "yes", "true", "t", "1", "on",
+ 0,
+};
+
+static const char *const conf_no[] = {
+ "n", "no", "false", "nil", "0", "off",
+ 0,
+};
+
+static int conf_boolean(char *s)
+{
+ const char * const *p;
+ for(p=conf_yes; *p; p++) {
+ if (!strcasecmp(*p,s))
+ return 1;
+ }
+ for(p=conf_no; *p; p++) {
+ if (!strcasecmp(*p,s))
+ return 0;
+ }
+ /* Default to "no" */
+ return 0;
+}
+
+static krb5_error_code appdefault_get(krb5_context context, const char *appname, const krb5_data *realm, const char *option, char **ret_value)
+{
+ profile_t profile;
+ const char *names[5];
+ char **nameval = NULL;
+ krb5_error_code retval;
+ const char * realmstr = realm?realm->data:NULL;
+
+ /*
+ * rra-c-util: The magic values are internal, so a magic check for the
+ * context struct was removed here. Call krb5_get_profile if it's
+ * available since the krb5_context struct may be opaque.
+ */
+ if (!context)
+ return KV5M_CONTEXT;
+
+#ifdef HAVE_KRB5_GET_PROFILE
+ krb5_get_profile(context, &profile);
+#else
+ profile = context->profile;
+#endif
+
+ /*
+ * Try number one:
+ *
+ * [appdefaults]
+ * app = {
+ * SOME.REALM = {
+ * option = <boolean>
+ * }
+ * }
+ */
+
+ names[0] = "appdefaults";
+ names[1] = appname;
+
+ if (realmstr) {
+ names[2] = realmstr;
+ names[3] = option;
+ names[4] = 0;
+ retval = profile_get_values(profile, names, &nameval);
+ if (retval == 0 && nameval && nameval[0]) {
+ *ret_value = strdup(nameval[0]);
+ goto goodbye;
+ }
+ }
+
+ /*
+ * Try number two:
+ *
+ * [appdefaults]
+ * app = {
+ * option = <boolean>
+ * }
+ */
+
+ names[2] = option;
+ names[3] = 0;
+ retval = profile_get_values(profile, names, &nameval);
+ if (retval == 0 && nameval && nameval[0]) {
+ *ret_value = strdup(nameval[0]);
+ goto goodbye;
+ }
+
+ /*
+ * Try number three:
+ *
+ * [appdefaults]
+ * realm = {
+ * option = <boolean>
+ */
+
+ if (realmstr) {
+ names[1] = realmstr;
+ names[2] = option;
+ names[3] = 0;
+ retval = profile_get_values(profile, names, &nameval);
+ if (retval == 0 && nameval && nameval[0]) {
+ *ret_value = strdup(nameval[0]);
+ goto goodbye;
+ }
+ }
+
+ /*
+ * Try number four:
+ *
+ * [appdefaults]
+ * option = <boolean>
+ */
+
+ names[1] = option;
+ names[2] = 0;
+ retval = profile_get_values(profile, names, &nameval);
+ if (retval == 0 && nameval && nameval[0]) {
+ *ret_value = strdup(nameval[0]);
+ } else {
+ return retval;
+ }
+
+goodbye:
+ if (nameval) {
+ char **cpp;
+ for (cpp = nameval; *cpp; cpp++)
+ free(*cpp);
+ free(nameval);
+ }
+ return 0;
+}
+
+void KRB5_CALLCONV
+krb5_appdefault_boolean(krb5_context context, const char *appname, const krb5_data *realm, const char *option, int default_value, int *ret_value)
+{
+ char *string = NULL;
+ krb5_error_code retval;
+
+ retval = appdefault_get(context, appname, realm, option, &string);
+
+ if (! retval && string) {
+ *ret_value = conf_boolean(string);
+ free(string);
+ } else
+ *ret_value = default_value;
+}
+
+void KRB5_CALLCONV
+krb5_appdefault_string(krb5_context context, const char *appname, const krb5_data *realm, const char *option, const char *default_value, char **ret_value)
+{
+ krb5_error_code retval;
+ char *string;
+
+ retval = appdefault_get(context, appname, realm, option, &string);
+
+ if (! retval && string) {
+ *ret_value = string;
+ } else {
+ *ret_value = strdup(default_value);
+ }
+}
+
+/*
+ * Copyright (C) 1985-2005 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Export of this software from the United States of America may require
+ * a specific license from the United States Government. It is the
+ * responsibility of any person or organization contemplating export to
+ * obtain such a license before exporting.
+ *
+ * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
+ * distribute this software and its documentation for any purpose and
+ * without fee is hereby granted, provided that the above copyright
+ * notice appear in all copies and that both that copyright notice and
+ * this permission notice appear in supporting documentation, and that
+ * the name of M.I.T. not be used in advertising or publicity pertaining
+ * to distribution of the software without specific, written prior
+ * permission. Furthermore if you modify this software you must label
+ * your software as modified software and not distribute it in such a
+ * fashion that it might be confused with the original MIT software.
+ * M.I.T. makes no representations about the suitability of this software
+ * for any purpose. It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * Individual source code files are copyright MIT, Cygnus Support,
+ * OpenVision, Oracle, Sun Soft, FundsXpress, and others.
+ *
+ * Project Athena, Athena, Athena MUSE, Discuss, Hesiod, Kerberos, Moira,
+ * and Zephyr are trademarks of the Massachusetts Institute of Technology
+ * (MIT). No commercial use of these trademarks may be made without
+ * prior written permission of MIT.
+ *
+ * "Commercial use" means use of a name in a product or other for-profit
+ * manner. It does NOT prevent a commercial firm from referring to the
+ * MIT trademarks in order to convey information (although in doing so,
+ * recognition of their trademark status should be given).
+ *
+ * There is no SPDX-License-Identifier registered for this license.
+ */
diff --git a/portable/krb5.h b/portable/krb5.h
new file mode 100644
index 000000000000..8c2726987b33
--- /dev/null
+++ b/portable/krb5.h
@@ -0,0 +1,248 @@
+/*
+ * Portability wrapper around krb5.h.
+ *
+ * This header includes krb5.h and then adjusts for various portability
+ * issues, primarily between MIT Kerberos and Heimdal, so that code can be
+ * written to a consistent API.
+ *
+ * Unfortunately, due to the nature of the differences between MIT Kerberos
+ * and Heimdal, it's not possible to write code to either one of the APIs and
+ * adjust for the other one. In general, this header tries to make available
+ * the Heimdal API and fix it for MIT Kerberos, but there are places where MIT
+ * Kerberos requires a more specific call. For those cases, it provides the
+ * most specific interface.
+ *
+ * For example, MIT Kerberos has krb5_free_unparsed_name() whereas Heimdal
+ * prefers the generic krb5_xfree(). In this case, this header provides
+ * krb5_free_unparsed_name() for both APIs since it's the most specific call.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2015, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#ifndef PORTABLE_KRB5_H
+#define PORTABLE_KRB5_H 1
+
+/*
+ * Allow inclusion of config.h to be skipped, since sometimes we have to use a
+ * stripped-down version of config.h with a different name.
+ */
+#ifndef CONFIG_H_INCLUDED
+# include <config.h>
+#endif
+#include <portable/macros.h>
+
+#if defined(HAVE_KRB5_H)
+# include <krb5.h>
+#elif defined(HAVE_KERBEROSV5_KRB5_H)
+# include <kerberosv5/krb5.h>
+#else
+# include <krb5/krb5.h>
+#endif
+#include <stdlib.h>
+
+/* Heimdal: KRB5_WELLKNOWN_NAME, MIT: KRB5_WELLKNOWN_NAMESTR. */
+#ifndef KRB5_WELLKNOWN_NAME
+# ifdef KRB5_WELLKNOWN_NAMESTR
+# define KRB5_WELLKNOWN_NAME KRB5_WELLKNOWN_NAMESTR
+# else
+# define KRB5_WELLKNOWN_NAME "WELLKNOWN"
+# endif
+#endif
+
+/* Heimdal: KRB5_ANON_NAME, MIT: KRB5_ANONYMOUS_PRINCSTR. */
+#ifndef KRB5_ANON_NAME
+# ifdef KRB5_ANONYMOUS_PRINCSTR
+# define KRB5_ANON_NAME KRB5_ANONYMOUS_PRINCSTR
+# else
+# define KRB5_ANON_NAME "ANONYMOUS"
+# endif
+#endif
+
+/* Heimdal: KRB5_ANON_REALM, MIT: KRB5_ANONYMOUS_REALMSTR. */
+#ifndef KRB5_ANON_REALM
+# ifdef KRB5_ANONYMOUS_REALMSTR
+# define KRB5_ANON_REALM KRB5_ANONYMOUS_REALMSTR
+# else
+# define KRB5_ANON_REALM "WELLKNOWN:ANONYMOUS"
+# endif
+#endif
+
+BEGIN_DECLS
+
+/* Default to a hidden visibility for all portability functions. */
+#pragma GCC visibility push(hidden)
+
+/*
+ * AIX included Kerberos includes the profile library but not the
+ * krb5_appdefault functions, so we provide replacements that we have to
+ * prototype.
+ */
+#ifndef HAVE_KRB5_APPDEFAULT_STRING
+void krb5_appdefault_boolean(krb5_context, const char *, const krb5_data *,
+ const char *, int, int *);
+void krb5_appdefault_string(krb5_context, const char *, const krb5_data *,
+ const char *, const char *, char **);
+#endif
+
+/*
+ * Now present in both Heimdal and MIT, but very new in MIT and not present in
+ * older Heimdal.
+ */
+#ifndef HAVE_KRB5_CC_GET_FULL_NAME
+krb5_error_code krb5_cc_get_full_name(krb5_context, krb5_ccache, char **);
+#endif
+
+/* Heimdal: krb5_data_free, MIT: krb5_free_data_contents. */
+#ifdef HAVE_KRB5_DATA_FREE
+# define krb5_free_data_contents(c, d) krb5_data_free(d)
+#endif
+
+/*
+ * MIT-specific. The Heimdal documentation says to use free(), but that
+ * doesn't actually make sense since the memory is allocated inside the
+ * Kerberos library. Use krb5_xfree instead.
+ */
+#ifndef HAVE_KRB5_FREE_DEFAULT_REALM
+# define krb5_free_default_realm(c, r) krb5_xfree(r)
+#endif
+
+/*
+ * Heimdal: krb5_xfree, MIT: krb5_free_string, older MIT uses free(). Note
+ * that we can incorrectly allocate in the library and call free() if
+ * krb5_free_string is not available but something we use that API for is
+ * available, such as krb5_appdefaults_*, but there isn't anything we can
+ * really do about it.
+ */
+#ifndef HAVE_KRB5_FREE_STRING
+# ifdef HAVE_KRB5_XFREE
+# define krb5_free_string(c, s) krb5_xfree(s)
+# else
+# define krb5_free_string(c, s) free(s)
+# endif
+#endif
+
+/* Heimdal: krb5_xfree, MIT: krb5_free_unparsed_name. */
+#ifdef HAVE_KRB5_XFREE
+# define krb5_free_unparsed_name(c, p) krb5_xfree(p)
+#endif
+
+/*
+ * krb5_{get,free}_error_message are the preferred APIs for both current MIT
+ * and current Heimdal, but there are tons of older APIs we may have to fall
+ * back on for earlier versions.
+ *
+ * This function should be called immediately after the corresponding error
+ * without any intervening Kerberos calls. Otherwise, the correct error
+ * message and supporting information may not be returned.
+ */
+#ifndef HAVE_KRB5_GET_ERROR_MESSAGE
+const char *krb5_get_error_message(krb5_context, krb5_error_code);
+#endif
+#ifndef HAVE_KRB5_FREE_ERROR_MESSAGE
+void krb5_free_error_message(krb5_context, const char *);
+#endif
+
+/*
+ * Both current MIT and current Heimdal prefer _opt_alloc and _opt_free, but
+ * older versions of both require allocating your own struct and calling
+ * _opt_init.
+ */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC
+krb5_error_code krb5_get_init_creds_opt_alloc(krb5_context,
+ krb5_get_init_creds_opt **);
+#endif
+#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_FREE
+# ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_FREE_2_ARGS
+# define krb5_get_init_creds_opt_free(c, o) \
+ krb5_get_init_creds_opt_free(o)
+# endif
+#else
+# define krb5_get_init_creds_opt_free(c, o) free(o)
+#endif
+
+/* Not available in versions of Heimdal prior to 7.0.1. */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT
+# define krb5_get_init_creds_opt_set_change_password_prompt(o, f) /* */
+#endif
+
+/* Heimdal-specific. */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_DEFAULT_FLAGS
+# define krb5_get_init_creds_opt_set_default_flags(c, p, r, o) /* empty */
+#endif
+
+/*
+ * Old versions of Heimdal (0.7 and earlier) take only nine arguments to the
+ * krb5_get_init_creds_opt_set_pkinit instead of the 11 arguments that current
+ * versions take. Adjust if needed. This function is Heimdal-specific.
+ */
+#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT
+# ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_PKINIT_9_ARGS
+# define krb5_get_init_creds_opt_set_pkinit(c, o, p, u, a, l, r, f, m, \
+ d, s) \
+ krb5_get_init_creds_opt_set_pkinit((c), (o), (p), (u), (a), (f), \
+ (m), (d), (s));
+# endif
+#endif
+
+/*
+ * MIT-specific. Heimdal automatically ignores environment variables if
+ * called in a setuid context.
+ */
+#ifndef HAVE_KRB5_INIT_SECURE_CONTEXT
+# define krb5_init_secure_context(c) krb5_init_context(c)
+#endif
+
+/*
+ * Heimdal: krb5_kt_free_entry, MIT: krb5_free_keytab_entry_contents. We
+ * check for the declaration rather than the function since the function is
+ * present in older MIT Kerberos libraries but not prototyped.
+ */
+#if !HAVE_DECL_KRB5_KT_FREE_ENTRY
+# define krb5_kt_free_entry(c, e) krb5_free_keytab_entry_contents((c), (e))
+#endif
+
+/*
+ * Heimdal provides a nice function that just returns a const char *. On MIT,
+ * there's an accessor macro that returns the krb5_data pointer, which
+ * requires more work to get at the underlying char *.
+ */
+#ifndef HAVE_KRB5_PRINCIPAL_GET_REALM
+const char *krb5_principal_get_realm(krb5_context, krb5_const_principal);
+#endif
+
+/*
+ * krb5_change_password is deprecated in favor of krb5_set_password in current
+ * Heimdal. Current MIT provides both.
+ */
+#ifndef HAVE_KRB5_SET_PASSWORD
+# define krb5_set_password(c, cr, pw, p, rc, rcs, rs) \
+ krb5_change_password((c), (cr), (pw), (rc), (rcs), (rs))
+#endif
+
+/*
+ * AIX's NAS Kerberos implementation mysteriously provides the struct and the
+ * krb5_verify_init_creds function but not this function.
+ */
+#ifndef HAVE_KRB5_VERIFY_INIT_CREDS_OPT_INIT
+void krb5_verify_init_creds_opt_init(krb5_verify_init_creds_opt *opt);
+#endif
+
+/* Undo default visibility change. */
+#pragma GCC visibility pop
+
+END_DECLS
+
+#endif /* !PORTABLE_KRB5_H */
diff --git a/portable/macros.h b/portable/macros.h
new file mode 100644
index 000000000000..5d77fb75af7b
--- /dev/null
+++ b/portable/macros.h
@@ -0,0 +1,72 @@
+/*
+ * Portability macros used in include files.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2015 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2008, 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#ifndef PORTABLE_MACROS_H
+#define PORTABLE_MACROS_H 1
+
+/*
+ * __attribute__ is available in gcc 2.5 and later, but only with gcc 2.7
+ * could you use the __format__ form of the attributes, which is what we use
+ * (to avoid confusion with other macros).
+ */
+#ifndef __attribute__
+# if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 7)
+# define __attribute__(spec) /* empty */
+# endif
+#endif
+
+/*
+ * We use __alloc_size__, but it was only available in fairly recent versions
+ * of GCC. Suppress warnings about the unknown attribute if GCC is too old.
+ * We know that we're GCC at this point, so we can use the GCC variadic macro
+ * extension, which will still work with versions of GCC too old to have C99
+ * variadic macro support.
+ */
+#if !defined(__attribute__) && !defined(__alloc_size__)
+# if (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 3)) \
+ && !defined(__clang__)
+# define __alloc_size__(spec, args...) /* empty */
+# endif
+#endif
+
+/*
+ * LLVM and Clang pretend to be GCC but don't support all of the __attribute__
+ * settings that GCC does. For them, suppress warnings about unknown
+ * attributes on declarations. This unfortunately will affect the entire
+ * compilation context, but there's no push and pop available.
+ */
+#if !defined(__attribute__) && (defined(__llvm__) || defined(__clang__))
+# pragma GCC diagnostic ignored "-Wattributes"
+#endif
+
+/*
+ * BEGIN_DECLS is used at the beginning of declarations so that C++
+ * compilers don't mangle their names. END_DECLS is used at the end.
+ */
+#undef BEGIN_DECLS
+#undef END_DECLS
+#ifdef __cplusplus
+# define BEGIN_DECLS extern "C" {
+# define END_DECLS }
+#else
+# define BEGIN_DECLS /* empty */
+# define END_DECLS /* empty */
+#endif
+
+#endif /* !PORTABLE_MACROS_H */
diff --git a/portable/mkstemp.c b/portable/mkstemp.c
new file mode 100644
index 000000000000..ebaefc1b5ee6
--- /dev/null
+++ b/portable/mkstemp.c
@@ -0,0 +1,101 @@
+/*
+ * Replacement for a missing mkstemp.
+ *
+ * Provides the same functionality as the library function mkstemp for those
+ * systems that don't have it.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009, 2011, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#ifdef HAVE_SYS_TIME_H
+# include <sys/time.h>
+#endif
+#include <time.h>
+
+/*
+ * If we're running the test suite, rename mkstemp to avoid conflicts with the
+ * system version. #undef it first because some systems may define it to
+ * another name.
+ */
+#if TESTING
+# undef mkstemp
+# define mkstemp test_mkstemp
+int test_mkstemp(char *);
+#endif
+
+/* Pick the longest available integer type. */
+#if HAVE_LONG_LONG_INT
+typedef unsigned long long long_int_type;
+#else
+typedef unsigned long long_int_type;
+#endif
+
+int
+mkstemp(char *template)
+{
+ static const char letters[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ size_t length;
+ char *XXXXXX;
+ struct timeval tv;
+ long_int_type randnum, working;
+ int i, tries, fd;
+
+ /*
+ * Make sure we have a valid template and initialize p to point at the
+ * beginning of the template portion of the string.
+ */
+ length = strlen(template);
+ if (length < 6) {
+ errno = EINVAL;
+ return -1;
+ }
+ XXXXXX = template + length - 6;
+ if (strcmp(XXXXXX, "XXXXXX") != 0) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ /* Get some more-or-less random information. */
+ gettimeofday(&tv, NULL);
+ randnum = ((long_int_type) tv.tv_usec << 16) ^ tv.tv_sec ^ getpid();
+
+ /*
+ * Now, try to find a working file name. We try no more than TMP_MAX file
+ * names.
+ */
+ for (tries = 0; tries < TMP_MAX; tries++) {
+ for (working = randnum, i = 0; i < 6; i++) {
+ XXXXXX[i] = letters[working % 62];
+ working /= 62;
+ }
+ fd = open(template, O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (fd >= 0 || (errno != EEXIST && errno != EISDIR))
+ return fd;
+
+ /*
+ * This is a relatively random increment. Cut off the tail end of
+ * tv_usec since it's often predictable.
+ */
+ randnum += (tv.tv_usec >> 10) & 0xfff;
+ }
+ errno = EEXIST;
+ return -1;
+}
diff --git a/portable/pam.h b/portable/pam.h
new file mode 100644
index 000000000000..b193fbb688ab
--- /dev/null
+++ b/portable/pam.h
@@ -0,0 +1,129 @@
+/*
+ * Portability wrapper around PAM header files.
+ *
+ * This header file includes the various PAM headers, wherever they may be
+ * found on the system, and defines replacements for PAM functions that may
+ * not be available on the local system.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2015, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#ifndef PORTABLE_PAM_H
+#define PORTABLE_PAM_H 1
+
+#include <config.h>
+#include <portable/macros.h>
+
+/* Linux PAM 1.1.0 requires sys/types.h before security/pam_modutil.h. */
+#include <sys/types.h>
+
+#ifndef HAVE_PAM_MODUTIL_GETPWNAM
+# include <pwd.h>
+#endif
+#if defined(HAVE_SECURITY_PAM_APPL_H)
+# include <security/pam_appl.h>
+# include <security/pam_modules.h>
+#elif defined(HAVE_PAM_PAM_APPL_H)
+# include <pam/pam_appl.h>
+# include <pam/pam_modules.h>
+#endif
+#if defined(HAVE_SECURITY_PAM_EXT_H)
+# include <security/pam_ext.h>
+#elif defined(HAVE_PAM_PAM_EXT_H)
+# include <pam/pam_ext.h>
+#endif
+#if defined(HAVE_SECURITY_PAM_MODUTIL_H)
+# include <security/pam_modutil.h>
+#elif defined(HAVE_PAM_PAM_MODUTIL_H)
+# include <pam/pam_modutil.h>
+#endif
+#include <stdarg.h>
+
+/* Solaris doesn't have these. */
+#ifndef PAM_CONV_AGAIN
+# define PAM_CONV_AGAIN 0
+# define PAM_INCOMPLETE PAM_SERVICE_ERR
+#endif
+
+/* Solaris 8 has deficient PAM. */
+#ifndef PAM_AUTHTOK_RECOVER_ERR
+# define PAM_AUTHTOK_RECOVER_ERR PAM_AUTHTOK_ERR
+#endif
+
+/*
+ * Mac OS X 10 doesn't define these. They're meant to be logically or'd with
+ * an exit status in pam_set_data, so define them to 0 if not defined to
+ * deactivate them.
+ */
+#ifndef PAM_DATA_REPLACE
+# define PAM_DATA_REPLACE 0
+#endif
+#ifndef PAM_DATA_SILENT
+# define PAM_DATA_SILENT 0
+#endif
+
+/*
+ * Mac OS X 10 apparently doesn't use PAM_BAD_ITEM and returns PAM_SYMBOL_ERR
+ * instead.
+ */
+#ifndef PAM_BAD_ITEM
+# define PAM_BAD_ITEM PAM_SYMBOL_ERR
+#endif
+
+/* We use this as a limit on password length, so make sure it's defined. */
+#ifndef PAM_MAX_RESP_SIZE
+# define PAM_MAX_RESP_SIZE 512
+#endif
+
+/*
+ * Some PAM implementations support building the module static and exporting
+ * the call points via a struct instead. (This is the default in OpenPAM, for
+ * example.) To support this, the pam_sm_* functions are declared PAM_EXTERN.
+ * Ensure that's defined for implementations that don't have this.
+ */
+#ifndef PAM_EXTERN
+# define PAM_EXTERN
+#endif
+
+BEGIN_DECLS
+
+/* Default to a hidden visibility for all portability functions. */
+#pragma GCC visibility push(hidden)
+
+/*
+ * If pam_modutil_getpwnam is missing, ideally we should roll our own using
+ * getpwnam_r. However, this is a fair bit of work, since we have to stash
+ * the allocated memory in the PAM data so that it will be freed properly.
+ * Bail for right now.
+ */
+#if !HAVE_PAM_MODUTIL_GETPWNAM
+# define pam_modutil_getpwnam(h, u) getpwnam(u)
+#endif
+
+/* Prototype missing optional PAM functions. */
+#if !HAVE_PAM_SYSLOG
+void pam_syslog(const pam_handle_t *, int, const char *, ...);
+#endif
+#if !HAVE_PAM_VSYSLOG
+void pam_vsyslog(const pam_handle_t *, int, const char *, va_list);
+#endif
+
+/* Undo default visibility change. */
+#pragma GCC visibility pop
+
+END_DECLS
+
+#endif /* !PORTABLE_PAM_H */
diff --git a/portable/pam_syslog.c b/portable/pam_syslog.c
new file mode 100644
index 000000000000..13d21c8428ac
--- /dev/null
+++ b/portable/pam_syslog.c
@@ -0,0 +1,36 @@
+/*
+ * Replacement for a missing pam_syslog.
+ *
+ * Implements pam_syslog in terms of pam_vsyslog (which itself may be a
+ * replacement) if the PAM implementation does not provide it. This is a
+ * Linux PAM extension.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+
+#include <stdarg.h>
+
+void
+pam_syslog(const pam_handle_t *pamh, int priority, const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ pam_vsyslog(pamh, priority, fmt, args);
+ va_end(args);
+}
diff --git a/portable/pam_vsyslog.c b/portable/pam_vsyslog.c
new file mode 100644
index 000000000000..07e143b5dd7c
--- /dev/null
+++ b/portable/pam_vsyslog.c
@@ -0,0 +1,63 @@
+/*
+ * Replacement for a missing pam_vsyslog.
+ *
+ * Provides close to the same functionality as the Linux PAM function
+ * pam_vsyslog for other PAM implementations. The logging prefix will not be
+ * quite as good, since we don't have access to the PAM group name.
+ *
+ * To use this replacement, the Autoconf script for the package must define
+ * MODULE_NAME to the name of the PAM module. (PACKAGE isn't used since it
+ * may use dashes where the module uses underscores.)
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <syslog.h>
+
+#ifndef LOG_AUTHPRIV
+# define LOG_AUTHPRIV LOG_AUTH
+#endif
+
+void
+pam_vsyslog(const pam_handle_t *pamh, int priority, const char *fmt,
+ va_list args)
+{
+ char *msg = NULL;
+ const char *service = NULL;
+ int retval;
+
+ retval = pam_get_item(pamh, PAM_SERVICE, (PAM_CONST void **) &service);
+ if (retval != PAM_SUCCESS)
+ service = NULL;
+ if (vasprintf(&msg, fmt, args) < 0) {
+ syslog(LOG_CRIT | LOG_AUTHPRIV,
+ "cannot allocate memory in vasprintf: %m");
+ return;
+ }
+ /* clang-format off */
+ syslog(priority | LOG_AUTHPRIV, MODULE_NAME "%s%s%s: %s",
+ (service == NULL) ? "" : "(",
+ (service == NULL) ? "" : service,
+ (service == NULL) ? "" : ")", msg);
+ /* clang-format on */
+ free(msg);
+}
diff --git a/portable/reallocarray.c b/portable/reallocarray.c
new file mode 100644
index 000000000000..635041ebe22b
--- /dev/null
+++ b/portable/reallocarray.c
@@ -0,0 +1,64 @@
+/*
+ * Replacement for a missing reallocarray.
+ *
+ * Provides the same functionality as the OpenBSD library function
+ * reallocarray for those systems that don't have it. This function is the
+ * same as realloc, but takes the size arguments in the same form as calloc
+ * and checks for overflow so that the caller doesn't need to.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+/*
+ * If we're running the test suite, rename reallocarray to avoid conflicts
+ * with the system version. #undef it first because some systems may define
+ * it to another name.
+ */
+#if TESTING
+# undef reallocarray
+# define reallocarray test_reallocarray
+void *test_reallocarray(void *, size_t, size_t);
+#endif
+
+/*
+ * nmemb * size cannot overflow if both are smaller than sqrt(SIZE_MAX). We
+ * can calculate that value statically by using 2^(sizeof(size_t) * 8) as the
+ * value of SIZE_MAX and then taking the square root, which gives
+ * 2^(sizeof(size_t) * 4). Compute the exponentiation with shift.
+ */
+#define CHECK_THRESHOLD (1UL << (sizeof(size_t) * 4))
+
+void *
+reallocarray(void *ptr, size_t nmemb, size_t size)
+{
+ if (nmemb >= CHECK_THRESHOLD || size >= CHECK_THRESHOLD)
+ if (nmemb > 0 && SIZE_MAX / nmemb <= size) {
+ errno = ENOMEM;
+ return NULL;
+ }
+
+ /* Avoid a zero-size allocation. */
+ if (nmemb == 0 || size == 0) {
+ nmemb = 1;
+ size = 1;
+ }
+ return realloc(ptr, nmemb * size);
+}
diff --git a/portable/stdbool.h b/portable/stdbool.h
new file mode 100644
index 000000000000..749052a61aa9
--- /dev/null
+++ b/portable/stdbool.h
@@ -0,0 +1,63 @@
+/*
+ * Portability wrapper around <stdbool.h>.
+ *
+ * Provides the bool and _Bool types and the true and false constants,
+ * following the C99 specification, on hosts that don't have stdbool.h. This
+ * logic is based heavily on the example in the Autoconf manual.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2008, 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#ifndef PORTABLE_STDBOOL_H
+#define PORTABLE_STDBOOL_H 1
+
+/*
+ * Allow inclusion of config.h to be skipped, since sometimes we have to use a
+ * stripped-down version of config.h with a different name.
+ */
+#ifndef CONFIG_H_INCLUDED
+# include <config.h>
+#endif
+
+#if HAVE_STDBOOL_H
+# include <stdbool.h>
+#else
+# if HAVE__BOOL
+# define bool _Bool
+# else
+# ifdef __cplusplus
+typedef bool _Bool;
+# elif _WIN32
+# include <windef.h>
+# define bool BOOL
+# else
+typedef unsigned char _Bool;
+# define bool _Bool
+# endif
+# endif
+# define false 0
+# define true 1
+# define __bool_true_false_are_defined 1
+#endif
+
+/*
+ * If we define bool and don't tell Perl, it will try to define its own and
+ * fail. Only of interest for programs that also include Perl headers.
+ */
+#ifndef HAS_BOOL
+# define HAS_BOOL 1
+#endif
+
+#endif /* !PORTABLE_STDBOOL_H */
diff --git a/portable/strndup.c b/portable/strndup.c
new file mode 100644
index 000000000000..9ddcbc130c33
--- /dev/null
+++ b/portable/strndup.c
@@ -0,0 +1,56 @@
+/*
+ * Replacement for a missing strndup.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+/*
+ * If we're running the test suite, rename the functions to avoid conflicts
+ * with the system versions.
+ */
+#if TESTING
+# undef strndup
+# define strndup test_strndup
+char *test_strndup(const char *, size_t);
+#endif
+
+char *
+strndup(const char *s, size_t n)
+{
+ const char *p;
+ size_t length;
+ char *copy;
+
+ if (s == NULL) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ /* Don't assume that the source string is nul-terminated. */
+ for (p = s; (size_t)(p - s) < n && *p != '\0'; p++)
+ ;
+ length = p - s;
+ copy = malloc(length + 1);
+ if (copy == NULL)
+ return NULL;
+ memcpy(copy, s, length);
+ copy[length] = '\0';
+ return copy;
+}
diff --git a/portable/system.h b/portable/system.h
new file mode 100644
index 000000000000..d5cb693d9eb4
--- /dev/null
+++ b/portable/system.h
@@ -0,0 +1,154 @@
+/*
+ * Standard system includes and portability adjustments.
+ *
+ * Declarations of routines and variables in the C library. Including this
+ * file is the equivalent of including all of the following headers,
+ * portably:
+ *
+ * #include <inttypes.h>
+ * #include <limits.h>
+ * #include <stdarg.h>
+ * #include <stdbool.h>
+ * #include <stddef.h>
+ * #include <stdio.h>
+ * #include <stdlib.h>
+ * #include <stdint.h>
+ * #include <string.h>
+ * #include <strings.h>
+ * #include <sys/types.h>
+ * #include <unistd.h>
+ *
+ * Missing functions are provided via #define or prototyped if available from
+ * the portable helper library. Also provides some standard #defines.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2014, 2016, 2018, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006-2011, 2013-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#ifndef PORTABLE_SYSTEM_H
+#define PORTABLE_SYSTEM_H 1
+
+/* Make sure we have our configuration information. */
+#include <config.h>
+
+/* BEGIN_DECL and __attribute__. */
+#include <portable/macros.h>
+
+/* A set of standard ANSI C headers. We don't care about pre-ANSI systems. */
+#if HAVE_INTTYPES_H
+# include <inttypes.h>
+#endif
+#include <limits.h>
+#include <stdarg.h>
+#include <stddef.h>
+#if HAVE_STDINT_H
+# include <stdint.h>
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#if HAVE_STRINGS_H
+# include <strings.h>
+#endif
+#include <sys/types.h>
+#if HAVE_UNISTD_H
+# include <unistd.h>
+#endif
+
+/* SCO OpenServer gets int32_t from here. */
+#if HAVE_SYS_BITYPES_H
+# include <sys/bitypes.h>
+#endif
+
+/* Get the bool type. */
+#include <portable/stdbool.h>
+
+/* Windows provides snprintf under a different name. */
+#ifdef _WIN32
+# define snprintf _snprintf
+#endif
+
+/* Windows does not define ssize_t. */
+#ifndef HAVE_SSIZE_T
+typedef ptrdiff_t ssize_t;
+#endif
+
+/*
+ * POSIX requires that these be defined in <unistd.h>. If one of them has
+ * been defined, all the rest almost certainly have.
+ */
+#ifndef STDIN_FILENO
+# define STDIN_FILENO 0
+# define STDOUT_FILENO 1
+# define STDERR_FILENO 2
+#endif
+
+/*
+ * C99 requires va_copy. Older versions of GCC provide __va_copy. Per the
+ * Autoconf manual, memcpy is a generally portable fallback.
+ */
+#ifndef va_copy
+# ifdef __va_copy
+# define va_copy(d, s) __va_copy((d), (s))
+# else
+# define va_copy(d, s) memcpy(&(d), &(s), sizeof(va_list))
+# endif
+#endif
+
+/*
+ * If explicit_bzero is not available, fall back on memset. This does NOT
+ * provide any of the security guarantees of explicit_bzero and will probably
+ * be optimized away by the compiler. It just ensures that code will compile
+ * and function on systems without explicit_bzero.
+ */
+#if !HAVE_EXPLICIT_BZERO
+# define explicit_bzero(s, n) memset((s), 0, (n))
+#endif
+
+BEGIN_DECLS
+
+/* Default to a hidden visibility for all portability functions. */
+#pragma GCC visibility push(hidden)
+
+/*
+ * Provide prototypes for functions not declared in system headers. Use the
+ * HAVE_DECL macros for those functions that may be prototyped but implemented
+ * incorrectly or implemented without a prototype.
+ */
+#if !HAVE_ASPRINTF
+extern int asprintf(char **, const char *, ...)
+ __attribute__((__format__(printf, 2, 3)));
+extern int vasprintf(char **, const char *, va_list)
+ __attribute__((__format__(printf, 2, 0)));
+#endif
+#if !HAVE_ISSETUGID
+extern int issetugid(void);
+#endif
+#if !HAVE_MKSTEMP
+extern int mkstemp(char *);
+#endif
+#if !HAVE_DECL_REALLOCARRAY
+extern void *reallocarray(void *, size_t, size_t);
+#endif
+#if !HAVE_STRNDUP
+extern char *strndup(const char *, size_t);
+#endif
+
+/* Undo default visibility change. */
+#pragma GCC visibility pop
+
+END_DECLS
+
+#endif /* !PORTABLE_SYSTEM_H */
diff --git a/tests/README b/tests/README
new file mode 100644
index 000000000000..186d2d5699b1
--- /dev/null
+++ b/tests/README
@@ -0,0 +1,252 @@
+ Writing TAP Tests
+
+Introduction
+
+ This is a guide for users of the C TAP Harness package or similar
+ TAP-based test harnesses explaining how to write tests. If your
+ package uses C TAP Harness as the test suite driver, you may want to
+ copy this document to an appropriate file name in your test suite as
+ documentation for contributors.
+
+About TAP
+
+ TAP is the Test Anything Protocol, a protocol for communication
+ between test cases and a test harness. This is the protocol used by
+ Perl for its internal test suite and for nearly all Perl modules,
+ since it's the format used by the build tools for Perl modules to run
+ tests and report their results.
+
+ A TAP-based test suite works with a somewhat different set of
+ assumptions than an xUnit test suite. In TAP, each test case is a
+ separate program. That program, when run, must produce output in the
+ following format:
+
+ 1..4
+ ok 1 - the first test
+ ok 2
+ # a diagnostic, ignored by the harness
+ not ok 3 - a failing test
+ ok 4 # skip a skipped test
+
+ The output should all go to standard output. The first line specifies
+ the number of tests to be run, and then each test produces output that
+ looks like either "ok <n>" or "not ok <n>" depending on whether the
+ test succeeded or failed. Additional information about the test can
+ be provided after the "ok <n>" or "not ok <n>", but is optional.
+ Additional diagnostics and information can be provided in lines
+ beginning with a "#".
+
+ Processing directives are supported after the "ok <n>" or "not ok <n>"
+ and start with a "#". The main one of interest is "# skip" which says
+ that the test was skipped rather than successful and optionally gives
+ the reason. Also supported is "# todo", which normally annotates a
+ failing test and indicates that test is expected to fail, optionally
+ providing a reason for why.
+
+ There are three more special cases. First, the initial line stating
+ the number of tests to run, called the plan, may appear at the end of
+ the output instead of the beginning. This can be useful if the number
+ of tests to run is not known in advance. Second, a plan in the form:
+
+ 1..0 # skip entire test case skipped
+
+ can be given instead, which indicates that this entire test case has
+ been skipped (generally because it depends on facilities or optional
+ configuration which is not present). Finally, if the test case
+ encounters a fatal error, it should print the text:
+
+ Bail out!
+
+ on standard output, optionally followed by an error message, and then
+ exit. This tells the harness that the test aborted unexpectedly.
+
+ The exit status of a successful test case should always be 0. The
+ harness will report the test as "dubious" if all the tests appeared to
+ succeed but it exited with a non-zero status.
+
+Writing TAP Tests
+
+ Environment
+
+ One of the special features of C TAP Harness is the environment that
+ it sets up for your test cases. If your test program is called under
+ the runtests driver, the environment variables C_TAP_SOURCE and
+ C_TAP_BUILD will be set to the top of the test directory in the source
+ tree and the top of the build tree, respectively. You can use those
+ environment variables to locate additional test data, programs and
+ libraries built as part of your software build, and other supporting
+ information needed by tests.
+
+ The C and shell TAP libraries support a test_file_path() function,
+ which looks for a file under the build tree and then under the source
+ tree, using the C_TAP_BUILD and C_TAP_SOURCE environment variables,
+ and return the full path to the file. This can be used to locate
+ supporting data files. They also support a test_tmpdir() function
+ that returns a directory that can be used for temporary files during
+ tests.
+
+ Perl
+
+ Since TAP is the native test framework for Perl, writing TAP tests in
+ Perl is very easy and extremely well-supported. If you've never
+ written tests in Perl before, start by reading the documentation for
+ Test::Tutorial and Test::Simple, which walks you through the basics,
+ including the TAP output syntax. Then, the best Perl module to use
+ for serious testing is Test::More, which provides a lot of additional
+ functions over Test::Simple including support for skipping tests,
+ bailing out, and not planning tests in advance. See the documentation
+ of Test::More for all the details and lots of examples.
+
+ C TAP Harness can run Perl test scripts directly and interpret the
+ results correctly, and similarly the Perl Test::Harness module and
+ prove command can run TAP tests written in other languages using, for
+ example, the TAP library that comes with C TAP Harness. You can, if
+ you wish, use the library that comes with C TAP Harness but use prove
+ instead of runtests for running the test suite.
+
+ C
+
+ C TAP Harness provides a basic TAP library that takes away most of the
+ pain of writing TAP test cases in C. A C test case should start with
+ a call to plan(), passing in the number of tests to run. Then, each
+ test should use is_int(), is_string(), is_double(), or is_hex() as
+ appropriate to compare expected and seen values, or ok() to do a
+ simpler boolean test. The is_*() functions take expected and seen
+ values and then a printf-style format string explaining the test
+ (which may be NULL). ok() takes a boolean and then the printf-style
+ string.
+
+ Here's a complete example test program that uses the C TAP library:
+
+ #include <stddef.h>
+ #include <tap/basic.h>
+
+ int
+ main(void)
+ {
+ plan(4);
+
+ ok(1, "the first test");
+ is_int(42, 42, NULL);
+ diag("a diagnostic, ignored by the harness");
+ ok(0, "a failing test");
+ skip("a skipped test");
+
+ return 0;
+ }
+
+ This test program produces the output shown above in the section on
+ TAP and demonstrates most of the functions. The other functions of
+ interest are sysdiag() (like diag() but adds strerror() results),
+ bail() and sysbail() for fatal errors, skip_block() to skip a whole
+ block of tests, and skip_all() which is called instead of plan() to
+ skip an entire test case.
+
+ The C TAP library also provides plan_lazy(), which can be called
+ instead of plan(). If plan_lazy() is called, the library will keep
+ track of how many test results are reported and will print out the
+ plan at the end of execution of the program. This should normally be
+ avoided since the test may appear to be successful even if it exits
+ prematurely, but it can make writing tests easier in some
+ circumstances.
+
+ Complete API documentation for the basic C TAP library that comes with
+ C TAP Harness is available at:
+
+ <https://www.eyrie.org/~eagle/software/c-tap-harness/>
+
+ It's common to need additional test functions and utility functions
+ for your C tests, particularly if you have to set up and tear down a
+ test environment for your test programs, and it's useful to have them
+ all in the libtap library so that you only have to link your test
+ programs with one library. Rather than editing tap/basic.c and
+ tap/basic.h to add those additional functions, add additional *.c and
+ *.h files into the tap directory with the function implementations and
+ prototypes, and then add those additional objects to the library.
+ That way, you can update tap/basic.c and tap/basic.h from subsequent
+ releases of C TAP Harness without having to merge changes with your
+ own code.
+
+ Libraries of additional useful TAP test functions are available in
+ rra-c-util at:
+
+ <https://www.eyrie.org/~eagle/software/rra-c-util/>
+
+ Some of the code there is particularly useful when testing programs
+ that require Kerberos keys.
+
+ If you implement new test functions that compare an expected and seen
+ value, it's best to name them is_<something> and take the expected
+ value, the seen value, and then a printf-style format string and
+ possible arguments to match the calling convention of the functions
+ provided by C TAP Harness.
+
+ Shell
+
+ C TAP Harness provides a library of shell functions to make it easier
+ to write TAP tests in shell. That library includes much of the same
+ functionality as the C TAP library, but takes its parameters in a
+ somewhat different order to make better use of shell features.
+
+ The libtap.sh file should be installed in a directory named tap in
+ your test suite area. It can then be loaded by tests written in shell
+ using the environment set up by runtests with:
+
+ . "$C_TAP_SOURCE"/tap/libtap.sh
+
+ Here is a complete test case written in shell which produces the same
+ output as the TAP sample above:
+
+ #!/bin/sh
+
+ . "$C_TAP_SOURCE"/tap/libtap.sh
+ cd "$C_TAP_BUILD"
+
+ plan 4
+ ok 'the first test' true
+ ok '' [ 42 -eq 42 ]
+ diag a diagnostic, ignored by the harness
+ ok '' false
+ skip 'a skipped test'
+
+ The shell framework doesn't provide the is_* functions, so you'll use
+ the ok function more. It takes a string describing the text and then
+ treats all of its remaining arguments as a condition, evaluated the
+ same way as the arguments to the "if" statement. If that condition
+ evaluates to true, the test passes; otherwise, the test fails.
+
+ The plan, plan_lazy, diag, and bail functions work the same as with
+ the C library. skip takes a string and skips the next test with that
+ explanation. skip_block takes a count and a string and skips that
+ many tests with that explanation. skip_all takes an optional reason
+ and skips the entire test case.
+
+ Since it's common for shell programs to want to test the output of
+ commands, there's an additional function ok_program provided by the
+ shell test library. It takes the test description string, the
+ expected exit status, the expected program output, and then treats the
+ rest of its arguments as the program to run. That program is run with
+ standard error and standard output combined, and then its exit status
+ and output are tested against the provided values.
+
+ A utility function, strip_colon_error, is provided that runs the
+ command given as its arguments and strips text following a colon and a
+ space from the output (unless there is no whitespace on the line
+ before the colon and the space, normally indicating a prefix of the
+ program name). This function can be used to wrap commands that are
+ expected to fail with output that has a system- or locale-specific
+ error message appended, such as the output of strerror().
+
+License
+
+ This file is part of the documentation of C TAP Harness, which can be
+ found at <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+
+ Copyright 2010, 2016 Russ Allbery <eagle@eyrie.org>
+
+ Copying and distribution of this file, with or without modification,
+ are permitted in any medium without royalty provided the copyright
+ notice and this notice are preserved. This file is offered as-is,
+ without any warranty.
+
+ SPDX-License-Identifier: FSFAP
diff --git a/tests/TESTS b/tests/TESTS
new file mode 100644
index 000000000000..f9036b1569c4
--- /dev/null
+++ b/tests/TESTS
@@ -0,0 +1,46 @@
+# Test list for pam-krb5. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2011-2012
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice and
+# this notice are preserved. This file is offered as-is, without any
+# warranty.
+#
+# SPDX-License-Identifier: FSFAP
+
+# Exclude the tests that use the pkinit.so MIT Kerberos module from valgrind
+# testing (module/fast-anon and module/pkinit) because they cause valgrind to
+# go into an infinite loop.
+
+docs/pod
+docs/pod-spelling
+docs/spdx-license
+module/alt-auth valgrind
+module/bad-authtok valgrind
+module/basic valgrind
+module/cache valgrind
+module/cache-cleanup valgrind
+module/expired valgrind
+module/fast valgrind
+module/fast-anon
+module/long valgrind
+module/no-cache valgrind
+module/pam-user valgrind
+module/password valgrind
+module/pkinit
+module/realm valgrind
+module/stacked valgrind
+pam-util/args valgrind
+pam-util/fakepam valgrind
+pam-util/logging valgrind
+pam-util/options valgrind
+pam-util/vector valgrind
+portable/asprintf valgrind
+portable/mkstemp valgrind
+portable/strndup valgrind
+style/obsolete-strings
+valgrind/logs
diff --git a/tests/config/README b/tests/config/README
new file mode 100644
index 000000000000..a034b35b6b0d
--- /dev/null
+++ b/tests/config/README
@@ -0,0 +1,70 @@
+This directory contains configuration required to run the complete
+pam-krb5 test suite. If there is no configuration in this directory, many
+of the tests will be skipped. To enable the full test suite, create the
+following files:
+
+admin-keytab
+
+ A keytab for a principal (in the same realm as the test principal
+ configured in password) that has admin access to inspect and modify
+ that test principal. For an MIT Kerberos KDC, it needs "mci"
+ permissions in kadm5.acl for that principal. For a Heimdal KDC, it
+ needs "cpw,list,modify" permissions (obviously, "all" will do). This
+ file is optional; if not present, the tests requiring admin
+ modification of a principal will be skipped.
+
+krb5.conf
+
+ This is optional and not required if the Kerberos realm used for
+ testing is configured in DNS or your system krb5.conf file and that
+ file is in either /etc/krb5.conf or /usr/local/etc/krb5.conf.
+ Otherwise, create a krb5.conf file that contains the realm information
+ (KDC, kpasswd server, and admin server) for the realm you're using for
+ testing. You don't need to worry about setting the default realm;
+ this will be done automatically in the generated file used by the test
+ suite.
+
+keytab
+
+ An optional keytab for a principal, which generally should be in the
+ same realm as the user configured in the password file. This is used
+ to test FAST support with a ticket cache.
+
+password
+
+ This file should contain two lines. The first line is the
+ fully-qualified principal (including the realm) of a Kerberos
+ principal to use for testing authentication. The second line is the
+ password for that principal.
+
+ If the realm of the principal is not configured in either DNS or in
+ your system krb5.conf file (/usr/local/etc/krb5.conf or
+ /etc/krb5.conf) with the KDC, kpasswd server, and admin server, you
+ will need to also provide a krb5.conf file in this directory. See
+ below.
+
+pkinit-cert
+
+ Certificate and private key (concatenated together) for PKINIT
+ authentication for the user listed in the pkinit-principal file.
+ Optional; PKINIT checks will be skipped if this file isn't present.
+
+pkinit-principal
+
+ Principal to use to test PKINIT authentication. Must be the Kerberos
+ identity corresponding to the certificate and private key given in
+ pkinit-cert. Optional; PKINIT checks will be skipped if this file
+ isn't present.
+
+-----
+
+Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+Copyright 2011-2012
+ The Board of Trustees of the Leland Stanford Junior University
+
+Copying and distribution of this file, with or without modification, are
+permitted in any medium without royalty provided the copyright notice and
+this notice are preserved. This file is offered as-is, without any
+warranty.
+
+SPDX-License-Identifier: FSFAP
diff --git a/tests/data/cppcheck.supp b/tests/data/cppcheck.supp
new file mode 100644
index 000000000000..00734778b256
--- /dev/null
+++ b/tests/data/cppcheck.supp
@@ -0,0 +1,72 @@
+// Suppressions file for cppcheck. -*- conf -*-
+//
+// This includes suppressions for all of my projects, including files that
+// aren't in rra-c-util, for ease of sharing between projects. The ones that
+// don't apply to a particular project should hopefully be harmless.
+//
+// To determine the correct suppression to add for a new error, run cppcheck
+// with the --xml flag and then add a suppression for the error id, file
+// location, and line.
+//
+// Copyright 2018-2021 Russ Allbery <eagle@eyrie.org>
+//
+// Copying and distribution of this file, with or without modification, are
+// permitted in any medium without royalty provided the copyright notice and
+// this notice are preserved. This file is offered as-is, without any
+// warranty.
+//
+// SPDX-License-Identifier: FSFAP
+
+// I like declaring variables at the top of a function rather than cluttering
+// every if and loop body with declarations.
+variableScope
+
+// strlen of a constant string is more maintainable code than hard-coding the
+// string length.
+constArgument:tests/runtests.c:804
+
+// False positive due to recursive function.
+knownConditionTrueFalse:portable/getopt.c:146
+
+// Bug in cppcheck 2.3. cppcheck can't see the assignment because of the
+// void * cast.
+knownConditionTrueFalse:portable/k_haspag.c:61
+
+// False positive since the string comes from a command-line define.
+knownConditionTrueFalse:tests/tap/process.c:415
+knownConditionTrueFalse:tests/tap/remctl.c:79
+
+// Stored in the returned ai struct, but cppcheck can't see the assignment
+// because of the struct sockaddr * cast.
+memleak:portable/getaddrinfo.c:236
+
+// Bug in cppcheck 1.89 (fixed in 2.3). The address of this variable is
+// passed to a Windows function (albeit through a cast).
+nullPointer:portable/winsock.c:61
+
+// Bug in cppcheck 2.3.
+nullPointerRedundantCheck:portable/krb5-profile.c:61
+
+// Bug in cppcheck 2.3.
+nullPointerRedundantCheck:portable/krb5-renew.c:82
+nullPointerRedundantCheck:portable/krb5-renew.c:83
+
+// Setting the variable to NULL explicitly after deallocation.
+redundantAssignment:tests/pam-util/options-t.c
+
+// (remctl) Bug in cppcheck 1.89 (fixed in 2.3). The address of these
+// variables are passed to a PHP function.
+uninitvar:php/php_remctl.c:119
+uninitvar:php/php_remctl.c:123
+uninitvar:php/php_remctl.c:315
+uninitvar:php/php5_remctl.c:125
+uninitvar:php/php5_remctl.c:129
+uninitvar:php/php5_remctl.c:321
+
+// (remctl) Bug in cppcheck 1.82. A pointer to this array is stored in a
+// struct that's passed to another function.
+redundantAssignment:tests/server/acl-t.c
+
+// (pam-krb5) cppcheck doesn't recognize the unused attribute on labels.
+unusedLabel:module/auth.c:895
+unusedLabelConfiguration:module/auth.c:895
diff --git a/tests/data/generate-krb5-conf b/tests/data/generate-krb5-conf
new file mode 100755
index 000000000000..712a933d40ba
--- /dev/null
+++ b/tests/data/generate-krb5-conf
@@ -0,0 +1,86 @@
+#!/bin/sh
+
+# Generate a krb5.conf file in the current directory for testing purposes.
+# Takes one command-line argument: the default realm to use. Strips out the
+# entire [appdefaults] section to avoid picking up any local configuration and
+# sets the default realm as indicated.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2016, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2006-2008, 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+set -e
+
+# Load the test library.
+. "$C_TAP_SOURCE/tap/libtap.sh"
+cd "$C_TAP_BUILD"
+
+# If there is no default realm specified on the command line, we leave the
+# realm information alone.
+realm="$1"
+
+# Locate the krb5.conf file to use as a base. Prefer the one in the test
+# configuration area, if it exists.
+krb5conf=`test_file_path config/krb5.conf`
+if [ -z "$krb5conf" ] ; then
+ for p in /etc/krb5.conf /usr/local/etc/krb5.conf ; do
+ if [ -r "$p" ] ; then
+ krb5conf="$p"
+ break
+ fi
+ done
+fi
+if [ -z "$krb5conf" ] ; then
+ echo 'no krb5.conf found, see test instructions' >&2
+ exit 1
+fi
+
+# We found a krb5.conf file. Generate our munged one.
+mkdir -p tmp
+awk '
+ BEGIN { skip = 0 }
+ /^ *\[appdefaults\]/ { skip = 1 }
+ !/^ *\[appdefaults\]/ && / *\[/ { skip = 0 }
+
+ { if (skip == 0) print }
+' "$krb5conf" > tmp/krb5.conf.tmp
+if [ -n "$realm" ] ; then
+ pattern='^[ ]*default_realm.*='
+ if grep "$pattern" tmp/krb5.conf.tmp >/dev/null 2>/dev/null; then
+ sed -e "s/\\(default_realm.*=\\) .*/\\1 $realm/" \
+ tmp/krb5.conf.tmp >tmp/krb5.conf
+ else
+ (
+ cat tmp/krb5.conf.tmp
+ echo "[libdefaults]"
+ echo " default_realm = $realm"
+ ) >tmp/krb5.conf
+ fi
+ rm tmp/krb5.conf.tmp
+else
+ mv tmp/krb5.conf.tmp tmp/krb5.conf
+fi
diff --git a/tests/data/krb5-pam.conf b/tests/data/krb5-pam.conf
new file mode 100644
index 000000000000..57887882c954
--- /dev/null
+++ b/tests/data/krb5-pam.conf
@@ -0,0 +1,30 @@
+# Test krb5.conf file for PAM option parsing.
+
+[appdefaults]
+ FOO.COM = {
+ program = /bin/false
+ }
+ BAR.COM = {
+ program = echo /bin/true
+ }
+ testing = {
+ minimum_uid = 1000
+ ignore_root = false
+ expires = 30m
+ FOO.COM = {
+ cells = foo.com,bar.com
+ }
+ BAR.COM = {
+ cells = bar.com foo.com
+ }
+ }
+ other-test = {
+ minimum_uid = -1000
+ }
+ bad-number = {
+ minimum_uid = 1000foo
+ }
+ bad-time = {
+ expires = ft87
+ }
+ debug = true
diff --git a/tests/data/krb5.conf b/tests/data/krb5.conf
new file mode 100644
index 000000000000..57887882c954
--- /dev/null
+++ b/tests/data/krb5.conf
@@ -0,0 +1,30 @@
+# Test krb5.conf file for PAM option parsing.
+
+[appdefaults]
+ FOO.COM = {
+ program = /bin/false
+ }
+ BAR.COM = {
+ program = echo /bin/true
+ }
+ testing = {
+ minimum_uid = 1000
+ ignore_root = false
+ expires = 30m
+ FOO.COM = {
+ cells = foo.com,bar.com
+ }
+ BAR.COM = {
+ cells = bar.com foo.com
+ }
+ }
+ other-test = {
+ minimum_uid = -1000
+ }
+ bad-number = {
+ minimum_uid = 1000foo
+ }
+ bad-time = {
+ expires = ft87
+ }
+ debug = true
diff --git a/tests/data/perl.conf b/tests/data/perl.conf
new file mode 100644
index 000000000000..699ef3a9123a
--- /dev/null
+++ b/tests/data/perl.conf
@@ -0,0 +1,19 @@
+# Configuration for Perl tests. -*- perl -*-
+
+# Ignore these top-level directories for perlcritic testing.
+@CRITIC_IGNORE = qw();
+
+# Add this directory (or a .libs subdirectory) relative to the top of the
+# source tree to LD_LIBRARY_PATH when checking the syntax of Perl modules.
+# This may be required to pick up libraries that are used by in-tree Perl
+# modules.
+#$LIBRARY_PATH = 'lib';
+
+# Default minimum version requirement for included Perl scripts.
+$MINIMUM_VERSION = '5.006';
+
+# Minimum version exceptions for specific top-level directories.
+%MINIMUM_VERSION = ();
+
+# File must end with this line.
+1;
diff --git a/tests/data/scripts/alt-auth/basic b/tests/data/scripts/alt-auth/basic
new file mode 100644
index 000000000000..92628e98cd8f
--- /dev/null
+++ b/tests/data/scripts/alt-auth/basic
@@ -0,0 +1,19 @@
+# Test simplest case of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 force_first_pass no_ccache
+ account = alt_auth_map=%1 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/alt-auth/basic-debug b/tests/data/scripts/alt-auth/basic-debug
new file mode 100644
index 000000000000..325a8117284c
--- /dev/null
+++ b/tests/data/scripts/alt-auth/basic-debug
@@ -0,0 +1,25 @@
+# Test simplest case of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 force_first_pass no_ccache debug
+ account = alt_auth_map=%1 no_ccache debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) mapping bogus-nonexistent-account to %1
+ DEBUG (user %u) alternate authentication successful
+ INFO user %u authenticated as %1
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG pam_sm_acct_mgmt: exit (success)
diff --git a/tests/data/scripts/alt-auth/fail b/tests/data/scripts/alt-auth/fail
new file mode 100644
index 000000000000..ec2145f3098f
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fail
@@ -0,0 +1,19 @@
+# Test failure of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=bogus force_first_pass no_ccache
+ account = alt_auth_map=bogus no_ccache
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+ acct_mgmt = PAM_IGNORE
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/alt-auth/fail-debug b/tests/data/scripts/alt-auth/fail-debug
new file mode 100644
index 000000000000..ae96bb148e6a
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fail-debug
@@ -0,0 +1,28 @@
+# Test failure of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=bogus force_first_pass no_ccache debug
+ account = alt_auth_map=bogus no_ccache debug
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+ acct_mgmt = PAM_IGNORE
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) mapping bogus-nonexistent-account to bogus@%2
+ DEBUG /^\(user %u\) alternate authentication failed: /
+ DEBUG (user %u) attempting authentication as %u@%2
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
diff --git a/tests/data/scripts/alt-auth/fallback b/tests/data/scripts/alt-auth/fallback
new file mode 100644
index 000000000000..a0ee7a3d4292
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fallback
@@ -0,0 +1,25 @@
+# Test alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s/unknown-user no_ccache
+ account = alt_auth_map=%%s/unknown-user no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/alt-auth/fallback-debug b/tests/data/scripts/alt-auth/fallback-debug
new file mode 100644
index 000000000000..f63741a60a16
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fallback-debug
@@ -0,0 +1,38 @@
+# Test alternative authentication principal with debug logging. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s/unknown-user no_ccache debug
+ account = alt_auth_map=%%s/unknown-user no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) mapping %u to %0/unknown-user@%2
+ DEBUG /^\(user %u\) alternate authentication failed: /
+ DEBUG (user %u) attempting authentication as %u
+ DEBUG (user %u) mapped user %0/unknown-user@%2 does not match principal %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) mapped user %0/unknown-user@%2 does not match principal %u
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/alt-auth/fallback-realm b/tests/data/scripts/alt-auth/fallback-realm
new file mode 100644
index 000000000000..0eef10fd5056
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fallback-realm
@@ -0,0 +1,25 @@
+# Test alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s@BOGUS.EXAMPLE.COM no_ccache
+ account = alt_auth_map=%%s@BOGUS.EXAMPLE.COM no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/alt-auth/force b/tests/data/scripts/alt-auth/force
new file mode 100644
index 000000000000..4ad34f6f1fe4
--- /dev/null
+++ b/tests/data/scripts/alt-auth/force
@@ -0,0 +1,19 @@
+# Test forced alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 force_alt_auth force_first_pass no_ccache
+ account = alt_auth_map=%1 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/alt-auth/force-fail-debug b/tests/data/scripts/alt-auth/force-fail-debug
new file mode 100644
index 000000000000..cc077b1a4743
--- /dev/null
+++ b/tests/data/scripts/alt-auth/force-fail-debug
@@ -0,0 +1,26 @@
+# Test failure of forced authentication principal (no fallback). -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 force_alt_auth force_first_pass no_ccache debug
+ account = alt_auth_map=%1 no_ccache debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) mapping bogus-nonexistent-account to %1
+ DEBUG /^\(user %u\) alternate authentication failed: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
diff --git a/tests/data/scripts/alt-auth/force-fallback b/tests/data/scripts/alt-auth/force-fallback
new file mode 100644
index 000000000000..b93b04175ed5
--- /dev/null
+++ b/tests/data/scripts/alt-auth/force-fallback
@@ -0,0 +1,25 @@
+# Test forced alternative authentication with fallback. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s/unknown-user force_alt_auth no_ccache
+ account = alt_auth_map=%%s/unknown-user no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/alt-auth/only b/tests/data/scripts/alt-auth/only
new file mode 100644
index 000000000000..7761fc7fd0ce
--- /dev/null
+++ b/tests/data/scripts/alt-auth/only
@@ -0,0 +1,19 @@
+# Test required alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 only_alt_auth force_first_pass no_ccache
+ account = alt_auth_map=%1 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/alt-auth/only-fail b/tests/data/scripts/alt-auth/only-fail
new file mode 100644
index 000000000000..5c2831614928
--- /dev/null
+++ b/tests/data/scripts/alt-auth/only-fail
@@ -0,0 +1,22 @@
+# Test failure of required alternative authentication. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=bogus only_alt_auth no_ccache
+ account = alt_auth_map=bogus no_ccache
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ acct_mgmt = PAM_IGNORE
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/alt-auth/username-map b/tests/data/scripts/alt-auth/username-map
new file mode 100644
index 000000000000..7f28a670344b
--- /dev/null
+++ b/tests/data/scripts/alt-auth/username-map
@@ -0,0 +1,19 @@
+# Test username mapping of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s@%2 force_first_pass no_ccache
+ account = alt_auth_map=%%s@%2 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/alt-auth/username-map-prefix b/tests/data/scripts/alt-auth/username-map-prefix
new file mode 100644
index 000000000000..5e83fc888d77
--- /dev/null
+++ b/tests/data/scripts/alt-auth/username-map-prefix
@@ -0,0 +1,19 @@
+# Test username mapping of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%3%%s@%2 force_first_pass no_ccache
+ account = alt_auth_map=%3%%s@%2 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/bad-authtok/no-prompt b/tests/data/scripts/bad-authtok/no-prompt
new file mode 100644
index 000000000000..e0c10cc69804
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/no-prompt
@@ -0,0 +1,25 @@
+# Defer prompting to the Kerberos library after bad authtok. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_prompt try_first_pass
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = /^(%u's Password|Password for %u): $/|%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/bad-authtok/try-first b/tests/data/scripts/bad-authtok/try-first
new file mode 100644
index 000000000000..cde6153efaeb
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/try-first
@@ -0,0 +1,25 @@
+# Test try_first_pass with a bad initial AUTHTOK. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = try_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/bad-authtok/try-first-debug b/tests/data/scripts/bad-authtok/try-first-debug
new file mode 100644
index 000000000000..c76ce7ac89dd
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/try-first-debug
@@ -0,0 +1,36 @@
+# Test try_first_pass with a bad initial AUTHTOK and debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = try_first_pass no_ccache debug
+ account = no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ DEBUG (user %u) attempting authentication as %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/bad-authtok/use-first b/tests/data/scripts/bad-authtok/use-first
new file mode 100644
index 000000000000..62d55ca2146f
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/use-first
@@ -0,0 +1,22 @@
+# Test use_first_pass with a bad initial AUTHTOK. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/bad-authtok/use-first-debug b/tests/data/scripts/bad-authtok/use-first-debug
new file mode 100644
index 000000000000..4346d2395cb0
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/use-first-debug
@@ -0,0 +1,33 @@
+# Test use_first_pass with a bad initial AUTHTOK and debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass no_ccache debug
+ account = no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/basic/force-first b/tests/data/scripts/basic/force-first
new file mode 100644
index 000000000000..792d737ba7c3
--- /dev/null
+++ b/tests/data/scripts/basic/force-first
@@ -0,0 +1,22 @@
+# Test force_first_pass without an authtok. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/basic/force-first-debug b/tests/data/scripts/basic/force-first-debug
new file mode 100644
index 000000000000..539345316183
--- /dev/null
+++ b/tests/data/scripts/basic/force-first-debug
@@ -0,0 +1,32 @@
+# Test force_first_pass without an authtok. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache debug
+ account = no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) no stored password
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/basic/ignore-root b/tests/data/scripts/basic/ignore-root
new file mode 100644
index 000000000000..bfbfee5c86df
--- /dev/null
+++ b/tests/data/scripts/basic/ignore-root
@@ -0,0 +1,16 @@
+# Test account and session behavior for ignored root user. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_root
+ password = ignore_root
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
diff --git a/tests/data/scripts/basic/ignore-root-debug b/tests/data/scripts/basic/ignore-root-debug
new file mode 100644
index 000000000000..2ffd33c16229
--- /dev/null
+++ b/tests/data/scripts/basic/ignore-root-debug
@@ -0,0 +1,24 @@
+# Test account and session behavior for ignored root user. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_root debug
+ password = ignore_root debug
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user root) ignoring root user
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG ignoring root user
+ DEBUG pam_sm_chauthtok: exit (ignore)
diff --git a/tests/data/scripts/basic/minimum-uid b/tests/data/scripts/basic/minimum-uid
new file mode 100644
index 000000000000..e56161041306
--- /dev/null
+++ b/tests/data/scripts/basic/minimum-uid
@@ -0,0 +1,13 @@
+# Test account and session behavior for minimum UID. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = minimum_uid=%1
+ password = minimum_uid=%1
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
diff --git a/tests/data/scripts/basic/minimum-uid-debug b/tests/data/scripts/basic/minimum-uid-debug
new file mode 100644
index 000000000000..c20e43d55ac8
--- /dev/null
+++ b/tests/data/scripts/basic/minimum-uid-debug
@@ -0,0 +1,21 @@
+# Test account and session behavior for minimum UID (debug). -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = minimum_uid=%1 debug
+ password = minimum_uid=%1 debug
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) ignoring low-UID user (%0 < %1)
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG ignoring low-UID user (%0 < %1)
+ DEBUG pam_sm_chauthtok: exit (ignore)
diff --git a/tests/data/scripts/basic/no-context b/tests/data/scripts/basic/no-context
new file mode 100644
index 000000000000..5629422e23d9
--- /dev/null
+++ b/tests/data/scripts/basic/no-context
@@ -0,0 +1,17 @@
+# Test account and session behavior with no context. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[run]
+ acct_mgmt = PAM_IGNORE
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+ open_session = PAM_IGNORE
+ close_session = PAM_SUCCESS
diff --git a/tests/data/scripts/basic/no-context-debug b/tests/data/scripts/basic/no-context-debug
new file mode 100644
index 000000000000..4bdeee727ed7
--- /dev/null
+++ b/tests/data/scripts/basic/no-context-debug
@@ -0,0 +1,47 @@
+# Test account and session behavior with no context. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug
+ account = debug
+ session = debug
+
+[run]
+ acct_mgmt = PAM_IGNORE
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+ open_session = PAM_IGNORE
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG pam_sm_setcred: entry (delete)
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (establish)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (refresh)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (reinit)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_open_session: exit (ignore)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/cache-cleanup/auth-only b/tests/data/scripts/cache-cleanup/auth-only
new file mode 100644
index 000000000000..c29608f3c8da
--- /dev/null
+++ b/tests/data/scripts/cache-cleanup/auth-only
@@ -0,0 +1,17 @@
+# Test authentication only with ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass ignore_k5login ccache_dir=FILE:%1
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/basic b/tests/data/scripts/cache/basic
new file mode 100644
index 000000000000..6b1042f3084b
--- /dev/null
+++ b/tests/data/scripts/cache/basic
@@ -0,0 +1,21 @@
+# Test basic authentication with ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass ignore_k5login
+ account = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/end-data-silent b/tests/data/scripts/cache/end-data-silent
new file mode 100644
index 000000000000..f172008bc574
--- /dev/null
+++ b/tests/data/scripts/cache/end-data-silent
@@ -0,0 +1,27 @@
+# Test pam_end with PAM_DATA_SILENT. -*- conf -*-
+#
+# Passing PAM_DATA_SILENT to pam_end should cause the credential cache to not
+# be deleted (under the assumption that pam_end is being called in a forked
+# process and will be called again in the parent to clean up resources).
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020-2021 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass ignore_k5login
+ account = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+
+[end]
+ flags = PAM_DATA_SILENT
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/open-session b/tests/data/scripts/cache/open-session
new file mode 100644
index 000000000000..83e48c36511e
--- /dev/null
+++ b/tests/data/scripts/cache/open-session
@@ -0,0 +1,20 @@
+# Test authentication with ticket cache, open session. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass ignore_k5login
+ account = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/search-k5login b/tests/data/scripts/cache/search-k5login
new file mode 100644
index 000000000000..b87c28147edb
--- /dev/null
+++ b/tests/data/scripts/cache/search-k5login
@@ -0,0 +1,20 @@
+# Test authentication with search_k5login, open session. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass search_k5login
+ account = search_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/search-k5login-debug b/tests/data/scripts/cache/search-k5login-debug
new file mode 100644
index 000000000000..eb50b9e47eaf
--- /dev/null
+++ b/tests/data/scripts/cache/search-k5login-debug
@@ -0,0 +1,34 @@
+# Test authentication with search_k5login and debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass search_k5login debug
+ account = search_k5login debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/basic-heimdal b/tests/data/scripts/expired/basic-heimdal
new file mode 100644
index 000000000000..2b4f471cf247
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal
@@ -0,0 +1,31 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Password has expired
+ info = Your password will expire at %1
+ info = Changing password
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+ info = Success: Password changed
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-heimdal-debug b/tests/data/scripts/expired/basic-heimdal-debug
new file mode 100644
index 000000000000..a18cc00c71a9
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-debug
@@ -0,0 +1,44 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login debug
+ account = ignore_k5login debug
+ password = ignore_k5login debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Password has expired
+ info = Your password will expire at %1
+ info = Changing password
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+ info = Success: Password changed
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/basic-heimdal-flag-silent b/tests/data/scripts/expired/basic-heimdal-flag-silent
new file mode 100644
index 000000000000..58e065b485bb
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-flag-silent
@@ -0,0 +1,27 @@
+# Test default handling of expired passwords with PAM_SILENT. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate(SILENT) = PAM_SUCCESS
+ acct_mgmt(SILENT) = PAM_SUCCESS
+ open_session(SILENT) = PAM_SUCCESS
+ close_session(SILENT) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-heimdal-old b/tests/data/scripts/expired/basic-heimdal-old
new file mode 100644
index 000000000000..dd67ec44df7c
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-old
@@ -0,0 +1,30 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Your password will expire at %1
+ info = Changing password
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+ info = Success: Password changed
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-heimdal-old-debug b/tests/data/scripts/expired/basic-heimdal-old-debug
new file mode 100644
index 000000000000..53267f5fac62
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-old-debug
@@ -0,0 +1,43 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login debug
+ account = ignore_k5login debug
+ password = ignore_k5login debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Your password will expire at %1
+ info = Changing password
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+ info = Success: Password changed
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/basic-heimdal-silent b/tests/data/scripts/expired/basic-heimdal-silent
new file mode 100644
index 000000000000..028d5fe382f6
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-silent
@@ -0,0 +1,27 @@
+# Test default handling of expired passwords with silent. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login silent
+ account = ignore_k5login silent
+ password = ignore_k5login silent
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-mit b/tests/data/scripts/expired/basic-mit
new file mode 100644
index 000000000000..9611381b4ce9
--- /dev/null
+++ b/tests/data/scripts/expired/basic-mit
@@ -0,0 +1,28 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Password expired. You must change it now.
+ echo_off = Enter new password: |%n
+ echo_off = Enter it again: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-mit-debug b/tests/data/scripts/expired/basic-mit-debug
new file mode 100644
index 000000000000..5b58b25b8ec2
--- /dev/null
+++ b/tests/data/scripts/expired/basic-mit-debug
@@ -0,0 +1,41 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login debug
+ account = ignore_k5login debug
+ password = ignore_k5login debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Password expired. You must change it now.
+ echo_off = Enter new password: |%n
+ echo_off = Enter it again: |%n
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/basic-mit-flag-silent b/tests/data/scripts/expired/basic-mit-flag-silent
new file mode 100644
index 000000000000..a13bffdeea44
--- /dev/null
+++ b/tests/data/scripts/expired/basic-mit-flag-silent
@@ -0,0 +1,27 @@
+# Test default handling of expired passwords with PAM_SILENT. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate(SILENT) = PAM_SUCCESS
+ acct_mgmt(SILENT) = PAM_SUCCESS
+ open_session(SILENT) = PAM_SUCCESS
+ close_session(SILENT) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ echo_off = Enter new password: |%n
+ echo_off = Enter it again: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-mit-silent b/tests/data/scripts/expired/basic-mit-silent
new file mode 100644
index 000000000000..7dea2b7bdd4e
--- /dev/null
+++ b/tests/data/scripts/expired/basic-mit-silent
@@ -0,0 +1,27 @@
+# Test default handling of expired passwords with silent. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login silent
+ account = ignore_k5login silent
+ password = ignore_k5login silent
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ echo_off = Enter new password: |%n
+ echo_off = Enter it again: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/defer-mit b/tests/data/scripts/expired/defer-mit
new file mode 100644
index 000000000000..7403edbfdbbf
--- /dev/null
+++ b/tests/data/scripts/expired/defer-mit
@@ -0,0 +1,33 @@
+# Test deferring handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = defer_pwchange use_first_pass
+ account = ignore_k5login
+ password = ignore_k5login use_first_pass
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_NEW_AUTHTOK_REQD
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ INFO user %u authenticated as %0 (expired)
+ INFO user %u account password is expired
+ INFO user %u changed Kerberos password
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/defer-mit-debug b/tests/data/scripts/expired/defer-mit-debug
new file mode 100644
index 000000000000..c637f39402f7
--- /dev/null
+++ b/tests/data/scripts/expired/defer-mit-debug
@@ -0,0 +1,57 @@
+# Test deferring handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = defer_pwchange use_first_pass debug
+ account = ignore_k5login debug
+ password = ignore_k5login use_first_pass debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_NEW_AUTHTOK_REQD
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ DEBUG (user %u) krb5_get_init_creds_password: Password has expired
+ DEBUG (user %u) expired account, deferring failure
+ INFO user %u authenticated as %0 (expired)
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ INFO user %u account password is expired
+ DEBUG pam_sm_acct_mgmt: exit (failure)
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG (user %u) attempting authentication as %0 for kadmin/changepw
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_chauthtok: entry (update)
+ INFO user %u changed Kerberos password
+ DEBUG (user %u) obtaining credentials with new password
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/fail b/tests/data/scripts/expired/fail
new file mode 100644
index 000000000000..566b4b9c73dc
--- /dev/null
+++ b/tests/data/scripts/expired/fail
@@ -0,0 +1,20 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login fail_pwchange
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/expired/fail-debug b/tests/data/scripts/expired/fail-debug
new file mode 100644
index 000000000000..7f464b4ed89f
--- /dev/null
+++ b/tests/data/scripts/expired/fail-debug
@@ -0,0 +1,24 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login fail_pwchange debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
diff --git a/tests/data/scripts/fast/anonymous b/tests/data/scripts/fast/anonymous
new file mode 100644
index 000000000000..5f725ae63dcf
--- /dev/null
+++ b/tests/data/scripts/fast/anonymous
@@ -0,0 +1,17 @@
+# Test anonymous FAST. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache anon_fast
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/fast/anonymous-debug b/tests/data/scripts/fast/anonymous-debug
new file mode 100644
index 000000000000..48fd1eadd581
--- /dev/null
+++ b/tests/data/scripts/fast/anonymous-debug
@@ -0,0 +1,22 @@
+# Test FAST with an existing ticket cache, with debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache anon_fast debug
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) anonymous authentication for FAST succeeded
+ DEBUG /^\(user %u\) setting FAST credential cache to MEMORY:/
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/fast/ccache b/tests/data/scripts/fast/ccache
new file mode 100644
index 000000000000..32e5eaa92465
--- /dev/null
+++ b/tests/data/scripts/fast/ccache
@@ -0,0 +1,17 @@
+# Test FAST with an existing ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache fast_ccache=%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/fast/ccache-debug b/tests/data/scripts/fast/ccache-debug
new file mode 100644
index 000000000000..f3788f2fc1c7
--- /dev/null
+++ b/tests/data/scripts/fast/ccache-debug
@@ -0,0 +1,21 @@
+# Test FAST with an existing ticket cache, with debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache fast_ccache=%0 debug
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) setting FAST credential cache to %0
+ DEBUG (user %u) attempting authentication as %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/fast/no-ccache b/tests/data/scripts/fast/no-ccache
new file mode 100644
index 000000000000..71d4e2d494cf
--- /dev/null
+++ b/tests/data/scripts/fast/no-ccache
@@ -0,0 +1,17 @@
+# Test FAST with an existing ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache fast_ccache=%0BAD
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/fast/no-ccache-debug b/tests/data/scripts/fast/no-ccache-debug
new file mode 100644
index 000000000000..743ad5559538
--- /dev/null
+++ b/tests/data/scripts/fast/no-ccache-debug
@@ -0,0 +1,21 @@
+# Test FAST with an existing ticket cache, with debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache fast_ccache=%0BAD debug
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG /^\(user %u\) failed to get principal from FAST ccache %0BAD: /
+ DEBUG (user %u) attempting authentication as %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/long/password b/tests/data/scripts/long/password
new file mode 100644
index 000000000000..e8183976c004
--- /dev/null
+++ b/tests/data/scripts/long/password
@@ -0,0 +1,14 @@
+# Test authentication with an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/long/password-debug b/tests/data/scripts/long/password-debug
new file mode 100644
index 000000000000..832c19340485
--- /dev/null
+++ b/tests/data/scripts/long/password-debug
@@ -0,0 +1,20 @@
+# Test excessively long password handling with debug logging. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
diff --git a/tests/data/scripts/long/use-first b/tests/data/scripts/long/use-first
new file mode 100644
index 000000000000..b68800485d04
--- /dev/null
+++ b/tests/data/scripts/long/use-first
@@ -0,0 +1,14 @@
+# Test use_first_pass with an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/long/use-first-debug b/tests/data/scripts/long/use-first-debug
new file mode 100644
index 000000000000..72747e81f40c
--- /dev/null
+++ b/tests/data/scripts/long/use-first-debug
@@ -0,0 +1,17 @@
+# Test use_first_pass with a long password and debug. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
diff --git a/tests/data/scripts/no-cache/no-prompt b/tests/data/scripts/no-cache/no-prompt
new file mode 100644
index 000000000000..1eef2f26b4ee
--- /dev/null
+++ b/tests/data/scripts/no-cache/no-prompt
@@ -0,0 +1,25 @@
+# Defer prompting to the Kerberos library. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_prompt
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = /^(%u's Password|Password for %u): $/|%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/no-prompt-try b/tests/data/scripts/no-cache/no-prompt-try
new file mode 100644
index 000000000000..1d632a96f9e6
--- /dev/null
+++ b/tests/data/scripts/no-cache/no-prompt-try
@@ -0,0 +1,25 @@
+# Defer prompting to the Kerberos library w/try_first_pass. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_prompt try_first_pass
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = /^(%u's Password|Password for %u): $/|%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/no-prompt-use b/tests/data/scripts/no-cache/no-prompt-use
new file mode 100644
index 000000000000..76ef388465d2
--- /dev/null
+++ b/tests/data/scripts/no-cache/no-prompt-use
@@ -0,0 +1,25 @@
+# Defer prompting to the Kerberos library w/use_first_pass. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_prompt
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = /^(%u's Password|Password for %u): $/|%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/prompt b/tests/data/scripts/no-cache/prompt
new file mode 100644
index 000000000000..b0eb0d9ca57b
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt
@@ -0,0 +1,25 @@
+# Test basic auth w/prompting without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/prompt-expose b/tests/data/scripts/no-cache/prompt-expose
new file mode 100644
index 000000000000..a3365cc69754
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt-expose
@@ -0,0 +1,25 @@
+# Test basic auth w/prompting without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = expose_account no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password for %u: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/prompt-fail b/tests/data/scripts/no-cache/prompt-fail
new file mode 100644
index 000000000000..376b0f911997
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt-fail
@@ -0,0 +1,25 @@
+# Test failed password authentication. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |BAD%p
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/no-cache/prompt-fail-debug b/tests/data/scripts/no-cache/prompt-fail-debug
new file mode 100644
index 000000000000..9c9a7a406b4b
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt-fail-debug
@@ -0,0 +1,36 @@
+# Test failed password authentication with debug logging. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache debug
+ account = no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |BAD%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/no-cache/prompt-principal b/tests/data/scripts/no-cache/prompt-principal
new file mode 100644
index 000000000000..5e7278f1e92d
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt-principal
@@ -0,0 +1,26 @@
+# Test prompting for principal without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = prompt_principal no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_on = Principal: |%u
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/try-first b/tests/data/scripts/no-cache/try-first
new file mode 100644
index 000000000000..366801e9a078
--- /dev/null
+++ b/tests/data/scripts/no-cache/try-first
@@ -0,0 +1,25 @@
+# Test basic auth w/no AUTHTOK and try_first_pass. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = try_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/use-first b/tests/data/scripts/no-cache/use-first
new file mode 100644
index 000000000000..028009fd7ba7
--- /dev/null
+++ b/tests/data/scripts/no-cache/use-first
@@ -0,0 +1,25 @@
+# Test basic auth w/no AUTHTOK and use_first_pass. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pam-user/no-update b/tests/data/scripts/pam-user/no-update
new file mode 100644
index 000000000000..36520bb4f332
--- /dev/null
+++ b/tests/data/scripts/pam-user/no-update
@@ -0,0 +1,20 @@
+# PAM_USER updates disabled. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_update_user
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pam-user/update b/tests/data/scripts/pam-user/update
new file mode 100644
index 000000000000..11d404a02144
--- /dev/null
+++ b/tests/data/scripts/pam-user/update
@@ -0,0 +1,20 @@
+# PAM_USER updates. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %0 authenticated as %1
diff --git a/tests/data/scripts/password/authtok b/tests/data/scripts/password/authtok
new file mode 100644
index 000000000000..9f6a39935b2d
--- /dev/null
+++ b/tests/data/scripts/password/authtok
@@ -0,0 +1,21 @@
+# Test password change with new authtok set but not old. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = use_authtok
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/authtok-force b/tests/data/scripts/password/authtok-force
new file mode 100644
index 000000000000..3bc0b598521b
--- /dev/null
+++ b/tests/data/scripts/password/authtok-force
@@ -0,0 +1,18 @@
+# Test password change with new authtok set but not old. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = use_authtok force_first_pass
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/authtok-too-long b/tests/data/scripts/password/authtok-too-long
new file mode 100644
index 000000000000..df81e24977b3
--- /dev/null
+++ b/tests/data/scripts/password/authtok-too-long
@@ -0,0 +1,17 @@
+# Test use_authtok with an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = use_authtok
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+
+[output]
diff --git a/tests/data/scripts/password/authtok-too-long-debug b/tests/data/scripts/password/authtok-too-long-debug
new file mode 100644
index 000000000000..cb38e8861102
--- /dev/null
+++ b/tests/data/scripts/password/authtok-too-long-debug
@@ -0,0 +1,23 @@
+# Test use_authtok with an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = use_authtok debug
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+
+[output]
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG (user %u) attempting authentication as %0 for kadmin/changepw
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_chauthtok: entry (update)
+ DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/
+ DEBUG pam_sm_chauthtok: exit (failure)
diff --git a/tests/data/scripts/password/banner b/tests/data/scripts/password/banner
new file mode 100644
index 000000000000..98c899c26af5
--- /dev/null
+++ b/tests/data/scripts/password/banner
@@ -0,0 +1,23 @@
+# Test password change with a modified banner. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = banner=realm
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current realm password: |%p
+ echo_off = Enter new realm password: |%n
+ echo_off = Retype new realm password: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/banner-expose b/tests/data/scripts/password/banner-expose
new file mode 100644
index 000000000000..595fa0380b22
--- /dev/null
+++ b/tests/data/scripts/password/banner-expose
@@ -0,0 +1,23 @@
+# Test password change with banner and expose_account. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = expose_account banner=realm
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current realm password for %0: |%p
+ echo_off = Enter new realm password for %0: |%n
+ echo_off = Retype new realm password for %0: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/basic b/tests/data/scripts/password/basic
new file mode 100644
index 000000000000..5cb68267ce26
--- /dev/null
+++ b/tests/data/scripts/password/basic
@@ -0,0 +1,20 @@
+# Test password change with prompting. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/basic-debug b/tests/data/scripts/password/basic-debug
new file mode 100644
index 000000000000..ca1c86b9c2c9
--- /dev/null
+++ b/tests/data/scripts/password/basic-debug
@@ -0,0 +1,28 @@
+# Test password change with prompting and debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = debug
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG (user %u) attempting authentication as %0 for kadmin/changepw
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_chauthtok: entry (update)
+ INFO user %u changed Kerberos password
+ DEBUG pam_sm_chauthtok: exit (success)
diff --git a/tests/data/scripts/password/expose b/tests/data/scripts/password/expose
new file mode 100644
index 000000000000..a82c1bd0b78d
--- /dev/null
+++ b/tests/data/scripts/password/expose
@@ -0,0 +1,23 @@
+# Test password change with prompting and expose_account. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = expose_account
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password for %0: |%p
+ echo_off = Enter new Kerberos password for %0: |%n
+ echo_off = Retype new Kerberos password for %0: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/ignore b/tests/data/scripts/password/ignore
new file mode 100644
index 000000000000..023cf5656f67
--- /dev/null
+++ b/tests/data/scripts/password/ignore
@@ -0,0 +1,18 @@
+# Test password prompt saving for ignored users. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = ignore_root
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
+ chauthtok(UPDATE_AUTHTOK) = PAM_IGNORE
+
+[prompts]
+ echo_off = Enter new password: |%n
+ echo_off = Retype new password: |%n
+
+[output]
diff --git a/tests/data/scripts/password/no-banner b/tests/data/scripts/password/no-banner
new file mode 100644
index 000000000000..9cabbd8ec5f9
--- /dev/null
+++ b/tests/data/scripts/password/no-banner
@@ -0,0 +1,23 @@
+# Test password change with no identifying banner. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = banner=
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current password: |%p
+ echo_off = Enter new password: |%n
+ echo_off = Retype new password: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/no-banner-expose b/tests/data/scripts/password/no-banner-expose
new file mode 100644
index 000000000000..3a5b944887bd
--- /dev/null
+++ b/tests/data/scripts/password/no-banner-expose
@@ -0,0 +1,23 @@
+# Test password change with no banner and expose_account. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = expose_account banner=
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current password for %0: |%p
+ echo_off = Enter new password for %0: |%n
+ echo_off = Retype new password for %0: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/prompt-principal b/tests/data/scripts/password/prompt-principal
new file mode 100644
index 000000000000..1e7274eb058e
--- /dev/null
+++ b/tests/data/scripts/password/prompt-principal
@@ -0,0 +1,24 @@
+# Test password change with prompting and prompt_principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = prompt_principal
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_on = Principal: |%u
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/too-long b/tests/data/scripts/password/too-long
new file mode 100644
index 000000000000..4dbabd5db11e
--- /dev/null
+++ b/tests/data/scripts/password/too-long
@@ -0,0 +1,15 @@
+# Test password change to an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+
+[output]
diff --git a/tests/data/scripts/password/too-long-debug b/tests/data/scripts/password/too-long-debug
new file mode 100644
index 000000000000..18b4ed608612
--- /dev/null
+++ b/tests/data/scripts/password/too-long-debug
@@ -0,0 +1,24 @@
+# Test password change to an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = debug
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+
+[output]
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG (user %u) attempting authentication as %0 for kadmin/changepw
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_chauthtok: entry (update)
+ DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/
+ DEBUG pam_sm_chauthtok: exit (failure)
diff --git a/tests/data/scripts/pkinit/basic b/tests/data/scripts/pkinit/basic
new file mode 100644
index 000000000000..713bf0af1ce1
--- /dev/null
+++ b/tests/data/scripts/pkinit/basic
@@ -0,0 +1,22 @@
+# Test PKINIT auth without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache use_pkinit pkinit_user=FILE:%0
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/basic-debug b/tests/data/scripts/pkinit/basic-debug
new file mode 100644
index 000000000000..92a3fcf934d6
--- /dev/null
+++ b/tests/data/scripts/pkinit/basic-debug
@@ -0,0 +1,30 @@
+# Test PKINIT auth without saving a ticket cache w/debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug no_ccache use_pkinit pkinit_user=FILE:%0
+ account = debug no_ccache
+ session = debug no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/pkinit/no-use-pkinit b/tests/data/scripts/pkinit/no-use-pkinit
new file mode 100644
index 000000000000..ead640bcc4a0
--- /dev/null
+++ b/tests/data/scripts/pkinit/no-use-pkinit
@@ -0,0 +1,18 @@
+# Test for unsupported use_pkinit. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache use_pkinit
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ ERR use_pkinit requested but PKINIT not available or cannot be enforced
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/pkinit/pin-mit b/tests/data/scripts/pkinit/pin-mit
new file mode 100644
index 000000000000..9791ebc2ace6
--- /dev/null
+++ b/tests/data/scripts/pkinit/pin-mit
@@ -0,0 +1,20 @@
+# Test PKINIT auth with a PIN prompt. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache use_pkinit pkinit_user=PKCS12:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Pass phrase for %0: |%1
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/preauth-opt-mit b/tests/data/scripts/pkinit/preauth-opt-mit
new file mode 100644
index 000000000000..4602d18c7556
--- /dev/null
+++ b/tests/data/scripts/pkinit/preauth-opt-mit
@@ -0,0 +1,17 @@
+# Test PKINIT auth with MIT preauth options. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache try_pkinit preauth_opt=X509_user_identity=FILE:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/prompt-try b/tests/data/scripts/pkinit/prompt-try
new file mode 100644
index 000000000000..723a228847e3
--- /dev/null
+++ b/tests/data/scripts/pkinit/prompt-try
@@ -0,0 +1,20 @@
+# Test try_pkinit with an initial prompt. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache try_pkinit pkinit_user=FILE:%0 pkinit_prompt
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Insert smart card if desired, then press Enter: |
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/prompt-use b/tests/data/scripts/pkinit/prompt-use
new file mode 100644
index 000000000000..0b341d5d73ce
--- /dev/null
+++ b/tests/data/scripts/pkinit/prompt-use
@@ -0,0 +1,20 @@
+# Test use_pkinit with an initial prompt. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache use_pkinit pkinit_user=FILE:%0 pkinit_prompt
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Insert smart card and press Enter: |
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/try-pkinit b/tests/data/scripts/pkinit/try-pkinit
new file mode 100644
index 000000000000..13b7bcf76653
--- /dev/null
+++ b/tests/data/scripts/pkinit/try-pkinit
@@ -0,0 +1,17 @@
+# Test optional PKINIT auth without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache try_pkinit pkinit_user=FILE:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/try-pkinit-debug b/tests/data/scripts/pkinit/try-pkinit-debug
new file mode 100644
index 000000000000..c721395abd07
--- /dev/null
+++ b/tests/data/scripts/pkinit/try-pkinit-debug
@@ -0,0 +1,19 @@
+# Test optional PKINIT auth w/debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug no_ccache try_pkinit pkinit_user=FILE:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/pkinit/try-pkinit-debug-mit b/tests/data/scripts/pkinit/try-pkinit-debug-mit
new file mode 100644
index 000000000000..2c8c966bdc03
--- /dev/null
+++ b/tests/data/scripts/pkinit/try-pkinit-debug-mit
@@ -0,0 +1,20 @@
+# Test optional PKINIT auth w/debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug no_ccache try_pkinit pkinit_user=FILE:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/realm/fail-bad-user-realm b/tests/data/scripts/realm/fail-bad-user-realm
new file mode 100644
index 000000000000..d30bec6f1f33
--- /dev/null
+++ b/tests/data/scripts/realm/fail-bad-user-realm
@@ -0,0 +1,17 @@
+# Test authentication failure with different user_realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache user_realm=%0
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/realm/fail-no-realm b/tests/data/scripts/realm/fail-no-realm
new file mode 100644
index 000000000000..87b59aab49f2
--- /dev/null
+++ b/tests/data/scripts/realm/fail-no-realm
@@ -0,0 +1,17 @@
+# Test authentication failure due to wrong realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/realm/fail-no-realm-debug b/tests/data/scripts/realm/fail-no-realm-debug
new file mode 100644
index 000000000000..5ef2ce588177
--- /dev/null
+++ b/tests/data/scripts/realm/fail-no-realm-debug
@@ -0,0 +1,21 @@
+# Test authentication failure due to wrong realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache debug
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u@%0
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
diff --git a/tests/data/scripts/realm/fail-realm b/tests/data/scripts/realm/fail-realm
new file mode 100644
index 000000000000..6dfe6a044354
--- /dev/null
+++ b/tests/data/scripts/realm/fail-realm
@@ -0,0 +1,17 @@
+# Test authentication failure with different realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache realm=%0
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/realm/fail-user-realm b/tests/data/scripts/realm/fail-user-realm
new file mode 100644
index 000000000000..c97324c2d028
--- /dev/null
+++ b/tests/data/scripts/realm/fail-user-realm
@@ -0,0 +1,18 @@
+# Test authentication failure with different user_realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache user_realm=%0
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[output]
+ ERR /^\(user %u\) cannot convert principal to user: /
+ NOTICE failed authorization check; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/realm/pass-realm b/tests/data/scripts/realm/pass-realm
new file mode 100644
index 000000000000..91136c9bfc1c
--- /dev/null
+++ b/tests/data/scripts/realm/pass-realm
@@ -0,0 +1,17 @@
+# Test authentication success with different realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache realm=%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u@%0
diff --git a/tests/data/scripts/realm/pass-user-realm b/tests/data/scripts/realm/pass-user-realm
new file mode 100644
index 000000000000..86007c2d4d26
--- /dev/null
+++ b/tests/data/scripts/realm/pass-user-realm
@@ -0,0 +1,17 @@
+# Test authentication success with different user_realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache user_realm=%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u@%0
diff --git a/tests/data/scripts/stacked/auth-only b/tests/data/scripts/stacked/auth-only
new file mode 100644
index 000000000000..46d3308ac0e4
--- /dev/null
+++ b/tests/data/scripts/stacked/auth-only
@@ -0,0 +1,18 @@
+# Test basic authentication without setcred. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/basic b/tests/data/scripts/stacked/basic
new file mode 100644
index 000000000000..a05640d278bf
--- /dev/null
+++ b/tests/data/scripts/stacked/basic
@@ -0,0 +1,22 @@
+# Test basic authentication without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/prompt b/tests/data/scripts/stacked/prompt
new file mode 100644
index 000000000000..b0eb0d9ca57b
--- /dev/null
+++ b/tests/data/scripts/stacked/prompt
@@ -0,0 +1,25 @@
+# Test basic auth w/prompting without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/prompt-principal b/tests/data/scripts/stacked/prompt-principal
new file mode 100644
index 000000000000..b416671875c7
--- /dev/null
+++ b/tests/data/scripts/stacked/prompt-principal
@@ -0,0 +1,25 @@
+# Test prompting for principal without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = prompt_principal force_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_on = Principal: |%u
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/try-first b/tests/data/scripts/stacked/try-first
new file mode 100644
index 000000000000..3a14b7584bc1
--- /dev/null
+++ b/tests/data/scripts/stacked/try-first
@@ -0,0 +1,22 @@
+# Test try_first_pass without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = try_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/use-first b/tests/data/scripts/stacked/use-first
new file mode 100644
index 000000000000..29c5c5c4188d
--- /dev/null
+++ b/tests/data/scripts/stacked/use-first
@@ -0,0 +1,22 @@
+# Test use_first_pass without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/trace/supported b/tests/data/scripts/trace/supported
new file mode 100644
index 000000000000..f67c389735ff
--- /dev/null
+++ b/tests/data/scripts/trace/supported
@@ -0,0 +1,58 @@
+# Basic test of enabling trace logging. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache trace=%0 debug
+ account = no_ccache trace=%0 debug
+ session = no_ccache trace=%0 debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) no stored password
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_setcred: entry (delete)
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_setcred: entry (establish)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_setcred: entry (refresh)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_setcred: entry (reinit)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/trace/unsupported b/tests/data/scripts/trace/unsupported
new file mode 100644
index 000000000000..2100c34fc2f5
--- /dev/null
+++ b/tests/data/scripts/trace/unsupported
@@ -0,0 +1,52 @@
+# Basic test of attempting trace logging when not supported. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache trace=%0 debug
+ account = no_ccache trace=%0 debug
+ session = no_ccache trace=%0 debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) no stored password
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_setcred: entry (delete)
+ DEBUG pam_sm_setcred: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_setcred: entry (establish)
+ DEBUG pam_sm_setcred: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_setcred: entry (refresh)
+ DEBUG pam_sm_setcred: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_setcred: entry (reinit)
+ DEBUG pam_sm_setcred: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/valgrind.supp b/tests/data/valgrind.supp
new file mode 100644
index 000000000000..6e987803f5e2
--- /dev/null
+++ b/tests/data/valgrind.supp
@@ -0,0 +1,242 @@
+# -*- conf -*-
+#
+# This is a valgrind suppression file for analysis of test suite results.
+#
+# Suppress a variety of apparent memory leaks in various Kerberos
+# implementations due to one-time instantiation of data, and a few other
+# artifacts of the test suite for rra-c-util portability and utility code
+# and related software.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2017-2018, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2011-2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+{
+ dlopen-dlerror
+ Memcheck:Leak
+ fun:calloc
+ fun:_dlerror_run
+}
+{
+ fakeroot-msgsnd
+ Memcheck:Param
+ msgsnd(msgp->mtext)
+ fun:msgsnd
+ fun:send_fakem
+ fun:send_get_fakem
+ obj:*/libfakeroot-sysv.so
+}
+{
+ heimdal-base-once
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:heim_base_once_f
+}
+{
+ heimdal-gss-config
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_config_parse_debug
+}
+{
+ heimdal-gss-config-2
+ Memcheck:Leak
+ fun:*alloc
+ fun:_krb5_config_get_entry
+}
+{
+ heimdal-gss-cred
+ Memcheck:Leak
+ fun:calloc
+ obj:*libgssapi.so.*
+ obj:*libgssapi.so.*
+ fun:gss_acquire_cred
+}
+{
+ heimdal-gss-krb5-init
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:_gsskrb5_init
+}
+{
+ heimdal-gss-load-mech
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:_gss_load_mech
+}
+{
+ heimdal-krb5-init-context-once
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:init_context_once
+}
+{
+ heimdal-krb5-reg-plugins-once
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_plugin_register
+ fun:reg_def_plugins_once
+}
+{
+ heimdal-krb5-openssl-init
+ Memcheck:Leak
+ fun:*alloc
+ obj:*
+ fun:CRYPTO_*alloc
+}
+{
+ mit-gss-ccache
+ Memcheck:Leak
+ fun:*alloc
+ fun:krb5int_setspecific
+ fun:kg_set_ccache_name
+ fun:gss_krb5int_ccache_name
+}
+{
+ mit-gss-ccache-2
+ Memcheck:Leak
+ fun:*alloc
+ fun:strdup
+ fun:kg_set_ccache_name
+ fun:gss_krb5int_ccache_name
+}
+{
+ mit-gss-error
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_gss_save_error_string
+}
+{
+ mit-gss-mechs
+ Memcheck:Leak
+ fun:glob
+ fun:loadConfigFiles
+ fun:updateMechList
+ fun:build_mechSet
+ fun:gss_indicate_mechs
+}
+{
+ mit-kadmin-ovku-error
+ Memcheck:Leak
+ fun:*alloc*
+ fun:initialize_ovku_error_table_r
+}
+{
+ mit-krb5-changepw
+ Memcheck:Leak
+ fun:*alloc
+ fun:change_set_password
+ fun:krb5_change_password
+ fun:krb5_get_init_creds_password
+}
+{
+ mit-krb5-pkinit-openssl-init
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_init_preauth_context
+}
+{
+ mit-krb5-pkinit-openssl-request
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_preauth_request_context_init
+}
+{
+ mit-krb5-pkinit-openssl-request-2
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:k5_preauth_request_context_init
+}
+{
+ mit-krb5-plugin-dirs
+ Memcheck:Leak
+ fun:calloc
+ fun:krb5int_open_plugin_dirs
+}
+{
+ mit-krb5-plugin-dlerror
+ Memcheck:Leak
+ fun:calloc
+ fun:_dlerror_run
+ ...
+ fun:krb5int_open_plugin
+}
+{
+ mit-krb5-plugin-register
+ Memcheck:Leak
+ fun:malloc
+ fun:strdup
+ fun:register_module.isra.1
+}
+{
+ mit-krb5-preauth-init
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:k5_init_preauth_context
+}
+{
+ mit-krb5-preauth-init
+ Memcheck:Leak
+ fun:strdup
+ fun:add_to_list
+ fun:profile_get_values
+ ...
+ fun:clpreauth_prep_questions
+}
+{
+ mit-krb5-preauth-init-2
+ Memcheck:Leak
+ fun:*alloc
+ fun:init_list
+ fun:profile_get_values
+ ...
+ fun:clpreauth_prep_questions
+}
+{
+ mit-krb5-profile
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:profile_open_file
+}
+{
+ portable-setenv
+ Memcheck:Leak
+ fun:malloc
+ fun:test_setenv
+}
diff --git a/tests/docs/pod-spelling-t b/tests/docs/pod-spelling-t
new file mode 100755
index 000000000000..2ea5bf3ba6ee
--- /dev/null
+++ b/tests/docs/pod-spelling-t
@@ -0,0 +1,55 @@
+#!/usr/bin/perl
+#
+# Checks all POD files in the tree for spelling errors using Test::Spelling.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2016, 2019, 2021 Russ Allbery <eagle@eyrie.org>
+# Copyright 2012-2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA qw(skip_unless_author use_prereq);
+use Test::RRA::Automake qw(automake_setup perl_dirs);
+
+use Test::More;
+
+# Only run this test for the module author since the required stopwords are
+# too sensitive to the exact spell-checking program and dictionary.
+skip_unless_author('Spelling tests');
+
+# Load prerequisite modules.
+use_prereq('Test::Spelling');
+
+# Set up Automake testing.
+automake_setup();
+
+# Run the tests.
+all_pod_files_spelling_ok(perl_dirs());
diff --git a/tests/docs/pod-t b/tests/docs/pod-t
new file mode 100755
index 000000000000..be21ddf01cea
--- /dev/null
+++ b/tests/docs/pod-t
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+#
+# Check all POD documents in the tree, except for any embedded Perl module
+# distribution, for POD formatting errors.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2016, 2019, 2021 Russ Allbery <eagle@eyrie.org>
+# Copyright 2012-2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA qw(skip_unless_automated use_prereq);
+use Test::RRA::Automake qw(automake_setup perl_dirs);
+
+use Test::More;
+
+# Skip this test for normal user installs, since we normally pre-generate all
+# of the documentation and the end user doesn't care.
+skip_unless_automated('POD syntax tests');
+
+# Load prerequisite modules.
+use_prereq('Test::Pod');
+
+# Set up Automake testing.
+automake_setup();
+
+# Run the tests.
+all_pod_files_ok(perl_dirs());
diff --git a/tests/docs/spdx-license-t b/tests/docs/spdx-license-t
new file mode 100755
index 000000000000..2841835fb69a
--- /dev/null
+++ b/tests/docs/spdx-license-t
@@ -0,0 +1,149 @@
+#!/usr/bin/perl
+#
+# Check source files for SPDX-License-Identifier fields.
+#
+# Examine all source files in a distribution to check that they contain an
+# SPDX-License-Identifier field. This does not check the syntax or whether
+# the identifiers are valid.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Copyright 2018-2021 Russ Allbery <eagle@eyrie.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA qw(skip_unless_automated);
+use Test::RRA::Automake qw(all_files automake_setup);
+
+use File::Basename qw(basename);
+use Test::More;
+
+# File name (the file without any directory component) and path patterns to
+# skip for this check.
+## no critic (RegularExpressions::ProhibitFixedStringMatches)
+my @IGNORE = (
+ qr{ \A LICENSE \z }xms, # Generated file with no license itself
+ qr{ \A (NEWS|THANKS|TODO) \z }xms, # Package license should be fine
+ qr{ \A README ( [.] .* )? \z }xms, # Package license should be fine
+ qr{ \A (Makefile|libtool) \z }xms, # Generated file
+ qr{ ~ \z }xms, # Backup files
+ qr{ [.] l?a \z }xms, # Created by libtool
+ qr{ [.] o \z }xms, # Compiler objects
+ qr{ [.] output \z }xms, # Test data
+);
+my @IGNORE_PATHS = (
+ qr{ \A debian/ }xms, # Found in debian/* branches
+ qr{ \A docs/metadata/ }xms, # Package license should be fine
+ qr{ \A docs/protocol[.](html|txt) \z }xms, # Generated by xml2rfc
+ qr{ \A m4/ (libtool|lt.*) [.] m4 \z }xms, # Files from Libtool
+ qr{ \A perl/Build \z }xms, # Perl build files
+ qr{ \A perl/MANIFEST \z }xms, # Perl build files
+ qr{ \A perl/MYMETA [.] }xms, # Perl build files
+ qr{ \A perl/blib/ }xms, # Perl build files
+ qr{ \A perl/cover_db/ }xms, # Perl test files
+ qr{ \A perl/_build }xms, # Perl build files
+ qr{ \A php/Makefile [.] global \z }xms, # Created by phpize
+ qr{ \A php/autom4te [.] cache/ }xms, # Created by phpize
+ qr{ \A php/acinclude [.] m4 \z }xms, # Created by phpize
+ qr{ \A php/build/ }xms, # Created by phpize
+ qr{ \A php/config [.] (guess|sub) \z }xms, # Created by phpize
+ qr{ \A php/configure [.] (ac|in) \z }xms, # Created by phpize
+ qr{ \A php/ltmain [.] sh \z }xms, # Created by phpize
+ qr{ \A php/run-tests [.] php \z }xms, # Created by phpize
+ qr{ \A python/ .* [.] egg-info/ }xms, # Python build files
+ qr{ \A tests/config/ (?!README) }xms, # Test configuration
+ qr{ \A tests/tmp/ }xms, # Temporary test files
+);
+## use critic
+
+# Only run this test during automated testing, since failure doesn't indicate
+# any user-noticable flaw in the package itself.
+skip_unless_automated('SPDX identifier tests');
+
+# Set up Automake testing.
+automake_setup();
+
+# Check a single file for an occurrence of the string.
+#
+# $path - Path to the file
+#
+# Returns: undef
+sub check_file {
+ my ($path) = @_;
+ my $filename = basename($path);
+
+ # Ignore files in the whitelist and binary files.
+ for my $pattern (@IGNORE) {
+ return if $filename =~ $pattern;
+ }
+ for my $pattern (@IGNORE_PATHS) {
+ return if $path =~ $pattern;
+ }
+ return if !-T $path;
+
+ # Scan the file.
+ my ($saw_legacy_notice, $saw_spdx, $skip_spdx);
+ open(my $file, '<', $path) or BAIL_OUT("Cannot open $path: $!");
+ while (defined(my $line = <$file>)) {
+ if ($line =~ m{ Generated [ ] by [ ] libtool [ ] }xms) {
+ close($file) or BAIL_OUT("Cannot close $path: $!");
+ return;
+ }
+ if ($line =~ m{ \b See \s+ LICENSE \s+ for \s+ licensing }xms) {
+ $saw_legacy_notice = 1;
+ }
+ if ($line =~ m{ \b SPDX-License-Identifier: \s+ \S+ }xms) {
+ $saw_spdx = 1;
+ last;
+ }
+ if ($line =~ m{ no \s SPDX-License-Identifier \s registered }xms) {
+ $skip_spdx = 1;
+ last;
+ }
+ }
+ close($file) or BAIL_OUT("Cannot close $path: $!");
+
+ # If there is a legacy license notice, report a failure regardless of file
+ # size. Otherwise, skip files under 1KB. They can be rolled up into the
+ # overall project license and the license notice may be a substantial
+ # portion of the file size.
+ if ($saw_legacy_notice) {
+ ok(!$saw_legacy_notice, "$path has legacy license notice");
+ } else {
+ ok($saw_spdx || $skip_spdx || -s $path < 1024, $path);
+ }
+ return;
+}
+
+# Scan every file. We don't declare a plan since we skip a lot of files and
+# don't want to precalculate the file list.
+my @paths = all_files();
+for my $path (@paths) {
+ check_file($path);
+}
+done_testing();
diff --git a/tests/fakepam/README b/tests/fakepam/README
new file mode 100644
index 000000000000..f7522cc1d66e
--- /dev/null
+++ b/tests/fakepam/README
@@ -0,0 +1,276 @@
+ PAM Testing Framework
+
+Overview
+
+ The files in this directory provide a shim PAM library that's used for
+ testing and a test framework used to exercise a PAM module.
+
+ This library and its include files define the minimum amount
+ of the PAM module interface so that PAM modules can be tested without
+ such problems as needing configuration files in /etc/pam.d or needing
+ changes to the system configuration to run a testing PAM module
+ instead of the normal system PAM modules.
+
+ The goal of this library is that all PAM code should be able to be
+ left unchanged and the code just linked with the fakepam library
+ rather than the regular PAM library. The testing code can then call
+ pam_start and pam_end as defined in the fakepam/pam.h header file and
+ inspect internal PAM state as needed.
+
+ The library also provides an interface to exercise a PAM module via an
+ interaction script, so that as much of the testing process as possible
+ is moved into simple text files instead of C code. That test script
+ format supports specifying the PAM configuration, the PAM interfaces
+ to run, the expected prompts and replies, and the expected log
+ messages. That interface is defined in fakepam/script.h.
+
+Fake PAM Library
+
+ Unfortunately, the standard PAM library for most operating systems
+ does not provide a reasonable testing framework. The primary problem
+ is configuration: the PAM library usually hard-codes a configuration
+ location such as /etc/pam.conf or /etc/pam.d/<application>. But there
+ are other problems as well, such as capturing logging rather than
+ having it go to syslog and inspecting PAM internal state to make sure
+ that it's updated properly by the module.
+
+ This library implements some of the same API as the system PAM library
+ and uses the system PAM library headers, but the underlying
+ implementation does not call the system PAM library or dynamically
+ load modules. Instead, it's meant to be linked into a single
+ executable along with the implementation of a PAM module. It does not
+ provide most of the application-level PAM interfaces (so one cannot
+ link a PAM-using application against it), just the interfaces called
+ by a module. The caller of the library can then call the module API
+ (such as pam_sm_authenticate) directly.
+
+ All of the internal state maintained by the PAM library is made
+ available to the test program linked with this library. See
+ fakepam/pam.h for the data structures. This allows verification that
+ the PAM module is setting the internal PAM state properly.
+
+ User Handling
+
+ In order to write good test suites, one often has to be able to
+ authenticate as a variety of users, but PAM modules may expect the
+ authenticating user to exist on the system. The fakepam library
+ provides a pam_modutil_getpwnam (if available) or a getpwnam
+ implementation that returns information for a single user (and user
+ unknown for everyone else). To set the information for the one valid
+ user, call the pam_set_pwd function and provide a struct passwd that
+ will be returned by pam_modutil_getpwnam.
+
+ The fakepam library also provides a replacement krb5_kuserok function
+ for testing PAM modules that use Kerberos. This source file should
+ only be included in packages that are building with Kerberos. It
+ implements the same functionality as the default krb5_kuserok
+ function, but looks for .k5login in the home directory configured by
+ the test framework instead of using getpwnam.
+
+ Only those two functions are intercepted, so if the module looks up
+ users in other ways, it may still bypass the fakepam library and look
+ at system users.
+
+ Output Handling
+
+ The fakepam library intercepts the PAM functions that would normally
+ log to syslog and instead accumulates the output in a static string
+ variable. To retrieve the logging output so far, call pam_output,
+ which returns a struct of all the output strings up to that point and
+ resets the accumulated output.
+
+Scripted PAM Testing
+
+ Also provided as part of the fakepam library is a test framework for
+ testing PAM modules. This test framework allows most of the testing
+ process to be encapsulated in a text configuration file per test,
+ rather than in a tedious set of checks and calls written in C.
+
+ Test API
+
+ The basic test API is to call either run_script (to run a single test
+ script) or run_script_dir (to run all scripts in a particular
+ directory). Both take a configuration struct that controls how the
+ PAM library is set up and called.
+
+ That configuration struct takes the following elements:
+
+ user
+ The user as which to authenticate, passed into pam_start and also
+ substituted for the %u escape. This should match the user whose
+ home directory information is configured using pam_set_pwd if that
+ function is in use.
+
+ password
+ Only used for the %p escape. This is not used to set the
+ authentication token in the PAM library (see authtok below).
+
+ newpass
+ Only used for the %n escape.
+
+ extra
+ An array of up to 10 additional strings used by the %0 through %9
+ escapes when parsing the configuration file, as discussed below.
+
+ authtok
+ Sets the default value of the PAM_AUTHTOK data item. This will be
+ set immediately after initializing the PAM library and before
+ calling any PAM module functions.
+
+ authtok
+ Like authtok, but for the PAM_OLDAUTHTOK data item.
+
+ callback
+ This, and the associated data element, specifies a callback that's
+ called at the end of processing of the script before calling
+ pam_end. This can be used to inspect and verify the internal
+ state of PAM. The data element is an opaque pointer passed into
+ the callback.
+
+ Test Script Basic Format
+
+ Test scripts are composed of one or more sections. Each section
+ begins with:
+
+ [<section>]
+
+ starting in column 1, where <section> is the name of the section. The
+ valid section types and the format of their contents are described
+ below.
+
+ Blank lines and lines starting with # are ignored.
+
+ Several strings undergo %-escape expansion as mentioned below. For
+ any such string, the following escapes are supported:
+
+ %i Current UID (not the UID of the target user)
+ %n New password
+ %p Password
+ %u Username
+ %0 extra[0]
+ ...
+ %9 extra[9]
+
+ All of these are set in the script_config struct.
+
+ Regular expression matching is supported for output lines and for
+ prompts. To mark an expected prompt or output line as a regular
+ expression, it must begin and end with a slash (/). Slashes inside
+ the regular expression do not need to be escaped. If regular
+ expression support is not available in the C library, those matching
+ tests will be skipped.
+
+ The [options] Section
+
+ The [options] section contains the PAM configuration that will be
+ passed to the module. These are the options that are normally listed
+ in the PAM configuration file after the name of the module. The
+ syntax of this section is one or more lines of the form:
+
+ <group> = <options>
+
+ where <group> is one of "account", "auth", "password", or "session".
+ The options are space-delimited and may be either option names or
+ option=value pairs.
+
+ The [run] Section
+
+ The [run] section specifies what PAM interfaces to call. It consists
+ of one or more lines in the format:
+
+ <call> = <status>
+
+ where <call> is the PAM call to make and <status> is the status code
+ that it should return. <call> is one of the PAM module interface
+ functions without the leading "pam_sm_", so one of "acct_mgmt",
+ "authenticate", "setcred", "chauthtok", "open_session", or
+ "close_session". The return status is one of the PAM constants
+ defined for return status, such as PAM_IGNORE or PAM_SUCCESS. The
+ test framework will ensure that the PAM call returns the appropriate
+ status.
+
+ The <call> may be optionally followed by an open parentheses and then
+ a list of flags separated by |, or syntactically:
+
+ <call>(<flag>|<flag>|...) = <status>
+
+ In this form, rather than passing a flags value of 0 to the PAM call,
+ the test framework will pass the combination of the provided flags.
+ The flags are PAM constants without the leading PAM_, so (for example)
+ DELETE_CRED, ESTABLISH_CRED, REFRESH_CRED, or REINITIALIZE_CRED for
+ the "setcred" call.
+
+ As a special case, <call> may be "end" to specify flags to pass to the
+ pam_end call (such as PAM_DATA_SILENT).
+
+ The [end] Section
+
+ The [end] section defines how to call pam_end. It currently takes
+ only one setting, flags, the syntax of which is:
+
+ flags = <flag>|<flag>
+
+ This allows PAM_DATA_SILENT or other flags to be passed to pam_end
+ when running the test script.
+
+ The [output] Section
+
+ The [output] section defines the logging output expected from the
+ module. It consists of zero or more lines in the format:
+
+ <priority> <output>
+ <priority> /<regex>/
+
+ where <priority> is a syslog priority and <output> is the remaining
+ output or a regular expression to match against the output. Valid
+ values for <priority> are DEBUG, INFO, NOTICE, ERR, and CRIT.
+ <output> and <regex> may contain spaces and undergoes %-escape
+ expansion.
+
+ The replacement values are taken from the script_config struct passed
+ as a parameter to run_script or run_script_dir.
+
+ If the [output] section is missing entirely, the test framework will
+ expect there to be no logging output from the PAM module.
+
+ This defines the logging output, not the prompts returned through the
+ conversation function. For that, see the next section.
+
+ The [prompts] Section
+
+ The [prompts] section defines the prompts that the PAM module is
+ expected to send via the conversation function, and the responses that
+ the test harness will send back (if any). This consists of zero or
+ more lines in one of the following formats:
+
+ <type> = <prompt>
+ <type> = /<prompt>/
+ <type> = <prompt>|<response>
+ <type> = /<prompt>/|<response>
+
+ The <type> is the style of prompt, chosen from "echo_off", "echo_on",
+ "error_msg", and "info". The <prompt> is the actual prompt sent and
+ undergoes %-escape expansion. It may be enclosed in slashes (/) to
+ indicate that it's a regular expression instead of literal text. The
+ <response> if present (and its presence is signaled by the |
+ character) contains the response sent back by the test framework and
+ also undergoes %-escape expansion. The response starts with the final
+ | character on the line, so <prompt> regular expressions may freely
+ use | inside the regular expression.
+
+ If the [prompts] section is present and empty, the test harness will
+ check that the PAM module does not send any prompts. If the [prompts]
+ section is absent entirely, the conversation function passed to the
+ PAM module will be NULL.
+
+License
+
+ This file is part of the documentation of rra-c-util, which can be
+ found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+
+ Copyright 2011-2012, 2020-2021 Russ Allbery <eagle@eyrie.org>
+
+ Copying and distribution of this file, with or without modification,
+ are permitted in any medium without royalty provided the copyright
+ notice and this notice are preserved. This file is offered as-is,
+ without any warranty.
diff --git a/tests/fakepam/config.c b/tests/fakepam/config.c
new file mode 100644
index 000000000000..8e0685604d55
--- /dev/null
+++ b/tests/fakepam/config.c
@@ -0,0 +1,766 @@
+/*
+ * Run a PAM interaction script for testing.
+ *
+ * Provides an interface that loads a PAM interaction script from a file and
+ * runs through that script, calling the internal PAM module functions and
+ * checking their results. This allows automation of PAM testing through
+ * external data files instead of coding everything in C.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017-2018, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <syslog.h>
+
+#include <tests/fakepam/internal.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+/* Used for enumerating arrays. */
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
+/* Mapping of strings to PAM function pointers and group numbers. */
+static const struct {
+ const char *name;
+ pam_call call;
+ enum group_type group;
+} CALLS[] = {
+ /* clang-format off */
+ {"acct_mgmt", pam_sm_acct_mgmt, GROUP_ACCOUNT },
+ {"authenticate", pam_sm_authenticate, GROUP_AUTH },
+ {"setcred", pam_sm_setcred, GROUP_AUTH },
+ {"chauthtok", pam_sm_chauthtok, GROUP_PASSWORD},
+ {"open_session", pam_sm_open_session, GROUP_SESSION },
+ {"close_session", pam_sm_close_session, GROUP_SESSION },
+ /* clang-format on */
+};
+
+/* Mapping of PAM flag names without the leading PAM_ to values. */
+static const struct {
+ const char *name;
+ int value;
+} FLAGS[] = {
+ /* clang-format off */
+ {"CHANGE_EXPIRED_AUTHTOK", PAM_CHANGE_EXPIRED_AUTHTOK},
+ {"DELETE_CRED", PAM_DELETE_CRED },
+ {"DISALLOW_NULL_AUTHTOK", PAM_DISALLOW_NULL_AUTHTOK },
+ {"ESTABLISH_CRED", PAM_ESTABLISH_CRED },
+ {"PRELIM_CHECK", PAM_PRELIM_CHECK },
+ {"REFRESH_CRED", PAM_REFRESH_CRED },
+ {"REINITIALIZE_CRED", PAM_REINITIALIZE_CRED },
+ {"SILENT", PAM_SILENT },
+ {"UPDATE_AUTHTOK", PAM_UPDATE_AUTHTOK },
+ /* clang-format on */
+};
+
+/* Mapping of strings to PAM groups. */
+static const struct {
+ const char *name;
+ enum group_type group;
+} GROUPS[] = {
+ /* clang-format off */
+ {"account", GROUP_ACCOUNT },
+ {"auth", GROUP_AUTH },
+ {"password", GROUP_PASSWORD},
+ {"session", GROUP_SESSION },
+ /* clang-format on */
+};
+
+/* Mapping of strings to PAM return values. */
+static const struct {
+ const char *name;
+ int status;
+} RETURNS[] = {
+ /* clang-format off */
+ {"PAM_AUTH_ERR", PAM_AUTH_ERR },
+ {"PAM_AUTHINFO_UNAVAIL", PAM_AUTHINFO_UNAVAIL},
+ {"PAM_AUTHTOK_ERR", PAM_AUTHTOK_ERR },
+ {"PAM_DATA_SILENT", PAM_DATA_SILENT },
+ {"PAM_IGNORE", PAM_IGNORE },
+ {"PAM_NEW_AUTHTOK_REQD", PAM_NEW_AUTHTOK_REQD},
+ {"PAM_SESSION_ERR", PAM_SESSION_ERR },
+ {"PAM_SUCCESS", PAM_SUCCESS },
+ {"PAM_USER_UNKNOWN", PAM_USER_UNKNOWN },
+ /* clang-format on */
+};
+
+/* Mapping of PAM prompt styles to their values. */
+static const struct {
+ const char *name;
+ int style;
+} STYLES[] = {
+ /* clang-format off */
+ {"echo_off", PAM_PROMPT_ECHO_OFF},
+ {"echo_on", PAM_PROMPT_ECHO_ON },
+ {"error_msg", PAM_ERROR_MSG },
+ {"info", PAM_TEXT_INFO },
+ /* clang-format on */
+};
+
+/* Mappings of strings to syslog priorities. */
+static const struct {
+ const char *name;
+ int priority;
+} PRIORITIES[] = {
+ /* clang-format off */
+ {"DEBUG", LOG_DEBUG },
+ {"INFO", LOG_INFO },
+ {"NOTICE", LOG_NOTICE},
+ {"ERR", LOG_ERR },
+ {"CRIT", LOG_CRIT },
+ /* clang-format on */
+};
+
+
+/*
+ * Given a pointer to a string, skip any leading whitespace and return a
+ * pointer to the first non-whitespace character.
+ */
+static char *
+skip_whitespace(char *p)
+{
+ while (isspace((unsigned char) (*p)))
+ p++;
+ return p;
+}
+
+
+/*
+ * Read a line from a file into a BUFSIZ buffer, failing if the line was too
+ * long to fit into the buffer, and returns a copy of that line in newly
+ * allocated memory. Ignores blank lines and comments. Caller is responsible
+ * for freeing. Returns NULL on end of file and fails on read errors.
+ */
+static char *
+readline(FILE *file)
+{
+ char buffer[BUFSIZ];
+ char *line, *first;
+
+ do {
+ line = fgets(buffer, sizeof(buffer), file);
+ if (line == NULL) {
+ if (feof(file))
+ return NULL;
+ sysbail("cannot read line from script");
+ }
+ if (buffer[strlen(buffer) - 1] != '\n')
+ bail("script line too long");
+ buffer[strlen(buffer) - 1] = '\0';
+ first = skip_whitespace(buffer);
+ } while (first[0] == '#' || first[0] == '\0');
+ line = bstrdup(buffer);
+ return line;
+}
+
+
+/*
+ * Given the name of a PAM call, map it to a call enum. This is used later in
+ * switch statements to determine which function to call. Fails on any
+ * unrecognized string. If the optional second argument is not NULL, also
+ * store the group number in that argument.
+ */
+static pam_call
+string_to_call(const char *name, enum group_type *group)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(CALLS); i++)
+ if (strcmp(name, CALLS[i].name) == 0) {
+ if (group != NULL)
+ *group = CALLS[i].group;
+ return CALLS[i].call;
+ }
+ bail("unrecognized PAM call %s", name);
+}
+
+
+/*
+ * Given a PAM flag value without the leading PAM_, map it to the numeric
+ * value of that flag. Fails on any unrecognized string.
+ */
+static int
+string_to_flag(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(FLAGS); i++)
+ if (strcmp(name, FLAGS[i].name) == 0)
+ return FLAGS[i].value;
+ bail("unrecognized PAM flag %s", name);
+}
+
+
+/*
+ * Given a PAM group name, map it to the array index for the options array for
+ * that group. Fails on any unrecognized string.
+ */
+static enum group_type
+string_to_group(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(GROUPS); i++)
+ if (strcmp(name, GROUPS[i].name) == 0)
+ return GROUPS[i].group;
+ bail("unrecognized PAM group %s", name);
+}
+
+
+/*
+ * Given a syslog priority name, map it to the numeric value of that priority.
+ * Fails on any unrecognized string.
+ */
+static int
+string_to_priority(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(PRIORITIES); i++)
+ if (strcmp(name, PRIORITIES[i].name) == 0)
+ return PRIORITIES[i].priority;
+ bail("unrecognized syslog priority %s", name);
+}
+
+
+/*
+ * Given a PAM return status, map it to the actual expected value. Fails on
+ * any unrecognized string.
+ */
+static int
+string_to_status(const char *name)
+{
+ size_t i;
+
+ if (name == NULL)
+ bail("no PAM status on line");
+ for (i = 0; i < ARRAY_SIZE(RETURNS); i++)
+ if (strcmp(name, RETURNS[i].name) == 0)
+ return RETURNS[i].status;
+ bail("unrecognized PAM status %s", name);
+}
+
+
+/*
+ * Given a PAM prompt style value without the leading PAM_PROMPT_, map it to
+ * the numeric value of that flag. Fails on any unrecognized string.
+ */
+static int
+string_to_style(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(STYLES); i++)
+ if (strcmp(name, STYLES[i].name) == 0)
+ return STYLES[i].style;
+ bail("unrecognized PAM prompt style %s", name);
+}
+
+
+/*
+ * We found a section delimiter while parsing another section. Rewind our
+ * input file back before the section delimiter so that we'll read it again.
+ * Takes the length of the line we read, which is used to determine how far to
+ * rewind.
+ */
+static void
+rewind_section(FILE *script, size_t length)
+{
+ if (fseek(script, -length - 1, SEEK_CUR) != 0)
+ sysbail("cannot rewind file");
+}
+
+
+/*
+ * Given a string that may contain %-escapes, expand it into the resulting
+ * value. The following escapes are supported:
+ *
+ * %i current UID (not target user UID)
+ * %n new password
+ * %p password
+ * %u username
+ * %0 user-supplied string
+ * ...
+ * %9 user-supplied string
+ *
+ * The %* escape is preserved as-is, as it has to be interpreted at the time
+ * of checking output. Returns the expanded string in newly-allocated memory.
+ */
+static char *
+expand_string(const char *template, const struct script_config *config)
+{
+ size_t length = 0;
+ const char *p, *extra;
+ char *output, *out;
+ char *uid = NULL;
+
+ length = 0;
+ for (p = template; *p != '\0'; p++) {
+ if (*p != '%')
+ length++;
+ else {
+ p++;
+ switch (*p) {
+ case 'i':
+ if (uid == NULL)
+ basprintf(&uid, "%lu", (unsigned long) getuid());
+ length += strlen(uid);
+ break;
+ case 'n':
+ if (config->newpass == NULL)
+ bail("new password not set");
+ length += strlen(config->newpass);
+ break;
+ case 'p':
+ if (config->password == NULL)
+ bail("password not set");
+ length += strlen(config->password);
+ break;
+ case 'u':
+ length += strlen(config->user);
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ if (config->extra[*p - '0'] == NULL)
+ bail("extra script parameter %%%c not set", *p);
+ length += strlen(config->extra[*p - '0']);
+ break;
+ case '*':
+ length += 2;
+ break;
+ default:
+ length++;
+ break;
+ }
+ }
+ }
+ output = bmalloc(length + 1);
+ for (p = template, out = output; *p != '\0'; p++) {
+ if (*p != '%')
+ *out++ = *p;
+ else {
+ p++;
+ switch (*p) {
+ case 'i':
+ assert(uid != NULL);
+ memcpy(out, uid, strlen(uid));
+ out += strlen(uid);
+ break;
+ case 'n':
+ memcpy(out, config->newpass, strlen(config->newpass));
+ out += strlen(config->newpass);
+ break;
+ case 'p':
+ memcpy(out, config->password, strlen(config->password));
+ out += strlen(config->password);
+ break;
+ case 'u':
+ memcpy(out, config->user, strlen(config->user));
+ out += strlen(config->user);
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ extra = config->extra[*p - '0'];
+ memcpy(out, extra, strlen(extra));
+ out += strlen(extra);
+ break;
+ case '*':
+ *out++ = '%';
+ *out++ = '*';
+ break;
+ default:
+ *out++ = *p;
+ break;
+ }
+ }
+ }
+ *out = '\0';
+ free(uid);
+ return output;
+}
+
+
+/*
+ * Given a whitespace-delimited string of PAM options, split it into an argv
+ * array and argc count and store it in the provided option struct.
+ */
+static void
+split_options(char *string, struct options *options,
+ const struct script_config *config)
+{
+ char *opt;
+ size_t size, count;
+
+ for (opt = strtok(string, " "); opt != NULL; opt = strtok(NULL, " ")) {
+ if (options->argv == NULL) {
+ options->argv = bcalloc(2, sizeof(const char *));
+ options->argv[0] = expand_string(opt, config);
+ options->argc = 1;
+ } else {
+ count = (options->argc + 2);
+ size = sizeof(const char *);
+ options->argv = breallocarray(options->argv, count, size);
+ options->argv[options->argc] = expand_string(opt, config);
+ options->argv[options->argc + 1] = NULL;
+ options->argc++;
+ }
+ }
+}
+
+
+/*
+ * Parse the options section of a PAM script. This consists of one or more
+ * lines in the format:
+ *
+ * <group> = <options>
+ *
+ * where options are either option names or option=value pairs, where the
+ * value may not contain whitespace. Returns an options struct, which stores
+ * argc and argv values for each group.
+ *
+ * Takes the work struct as an argument and puts values into its array.
+ */
+static void
+parse_options(FILE *script, struct work *work,
+ const struct script_config *config)
+{
+ char *line, *group, *token;
+ size_t length = 0;
+ enum group_type type;
+
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ length = strlen(line);
+ group = strtok(line, " ");
+ if (group == NULL)
+ bail("malformed script line");
+ if (group[0] == '[')
+ break;
+ type = string_to_group(group);
+ token = strtok(NULL, " ");
+ if (token == NULL)
+ bail("malformed action line");
+ if (strcmp(token, "=") != 0)
+ bail("malformed action line near %s", token);
+ token = strtok(NULL, "");
+ split_options(token, &work->options[type], config);
+ free(line);
+ }
+ if (line != NULL) {
+ free(line);
+ rewind_section(script, length);
+ }
+}
+
+
+/*
+ * Parse the call portion of a PAM call in the run section of a PAM script.
+ * This handles parsing the PAM flags that optionally may be given as part of
+ * the call. Takes the token representing the call and a pointer to the
+ * action struct to fill in with the call and the option flags.
+ */
+static void
+parse_call(char *token, struct action *action)
+{
+ char *flags, *flag;
+
+ action->flags = 0;
+ flags = strchr(token, '(');
+ if (flags != NULL) {
+ *flags = '\0';
+ flags++;
+ for (flag = strtok(flags, "|,)"); flag != NULL;
+ flag = strtok(NULL, "|,)")) {
+ action->flags |= string_to_flag(flag);
+ }
+ }
+ action->call = string_to_call(token, &action->group);
+}
+
+
+/*
+ * Parse the run section of a PAM script. This consists of one or more lines
+ * in the format:
+ *
+ * <call> = <status>
+ *
+ * where <call> is a PAM call and <status> is what it should return. Returns
+ * a linked list of actions. Fails on any error in parsing.
+ */
+static struct action *
+parse_run(FILE *script)
+{
+ struct action *head = NULL, *current = NULL, *next;
+ char *line, *token, *call;
+ size_t length = 0;
+
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ length = strlen(line);
+ token = strtok(line, " ");
+ if (token[0] == '[')
+ break;
+ next = bmalloc(sizeof(struct action));
+ next->next = NULL;
+ if (head == NULL)
+ head = next;
+ else
+ current->next = next;
+ next->name = bstrdup(token);
+ call = token;
+ token = strtok(NULL, " ");
+ if (token == NULL)
+ bail("malformed action line");
+ if (strcmp(token, "=") != 0)
+ bail("malformed action line near %s", token);
+ token = strtok(NULL, " ");
+ next->status = string_to_status(token);
+ parse_call(call, next);
+ free(line);
+ current = next;
+ }
+ if (head == NULL)
+ bail("empty run section in script");
+ if (line != NULL) {
+ free(line);
+ rewind_section(script, length);
+ }
+ return head;
+}
+
+
+/*
+ * Parse the end section of a PAM script. There is one supported line in the
+ * format:
+ *
+ * flags = <flag>|<flag>
+ *
+ * where <flag> is a flag to pass to pam_end. Returns the flags.
+ */
+static int
+parse_end(FILE *script)
+{
+ char *line, *token, *flag;
+ size_t length = 0;
+ int flags = PAM_SUCCESS;
+
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ length = strlen(line);
+ token = strtok(line, " ");
+ if (token[0] == '[')
+ break;
+ if (strcmp(token, "flags") != 0)
+ bail("unknown end setting %s", token);
+ token = strtok(NULL, " ");
+ if (token == NULL)
+ bail("malformed end line");
+ if (strcmp(token, "=") != 0)
+ bail("malformed end line near %s", token);
+ token = strtok(NULL, " ");
+ flag = strtok(token, "|");
+ while (flag != NULL) {
+ flags |= string_to_status(flag);
+ flag = strtok(NULL, "|");
+ }
+ free(line);
+ }
+ if (line != NULL) {
+ free(line);
+ rewind_section(script, length);
+ }
+ return flags;
+}
+
+
+/*
+ * Parse the output section of a PAM script. This consists of zero or more
+ * lines in the format:
+ *
+ * PRIORITY some output information
+ * PRIORITY /output regex/
+ *
+ * where PRIORITY is replaced by the numeric syslog priority corresponding to
+ * that priority and the rest of the output undergoes %-esacape expansion.
+ * Returns the accumulated output as a vector.
+ */
+static struct output *
+parse_output(FILE *script, const struct script_config *config)
+{
+ char *line, *token, *message;
+ struct output *output;
+ int priority;
+
+ output = output_new();
+ if (output == NULL)
+ sysbail("cannot allocate vector");
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ token = strtok(line, " ");
+ priority = string_to_priority(token);
+ token = strtok(NULL, "");
+ if (token == NULL)
+ bail("malformed line %s", line);
+ message = expand_string(token, config);
+ output_add(output, priority, message);
+ free(message);
+ free(line);
+ }
+ return output;
+}
+
+
+/*
+ * Parse the prompts section of a PAM script. This consists of zero or more
+ * lines in one of the formats:
+ *
+ * type = prompt
+ * type = /prompt/
+ * type = prompt|response
+ * type = /prompt/|response
+ *
+ * If the type is error_msg or info, there is no response. Otherwise,
+ * everything after the last | is taken to be the response that should be
+ * provided to that prompt. The response undergoes %-escape expansion.
+ */
+static struct prompts *
+parse_prompts(FILE *script, const struct script_config *config)
+{
+ struct prompts *prompts = NULL;
+ struct prompt *prompt;
+ char *line, *token, *style, *end;
+ size_t size, count, i;
+ size_t length = 0;
+
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ length = strlen(line);
+ token = strtok(line, " ");
+ if (token[0] == '[')
+ break;
+ if (prompts == NULL) {
+ prompts = bcalloc(1, sizeof(struct prompts));
+ prompts->prompts = bcalloc(1, sizeof(struct prompt));
+ prompts->allocated = 1;
+ } else if (prompts->allocated == prompts->size) {
+ count = prompts->allocated * 2;
+ size = sizeof(struct prompt);
+ prompts->prompts = breallocarray(prompts->prompts, count, size);
+ prompts->allocated = count;
+ for (i = prompts->size; i < prompts->allocated; i++) {
+ prompts->prompts[i].prompt = NULL;
+ prompts->prompts[i].response = NULL;
+ }
+ }
+ prompt = &prompts->prompts[prompts->size];
+ style = token;
+ token = strtok(NULL, " ");
+ if (token == NULL)
+ bail("malformed prompt line");
+ if (strcmp(token, "=") != 0)
+ bail("malformed prompt line near %s", token);
+ prompt->style = string_to_style(style);
+ token = strtok(NULL, "");
+ if (prompt->style == PAM_ERROR_MSG || prompt->style == PAM_TEXT_INFO)
+ prompt->prompt = expand_string(token, config);
+ else {
+ end = strrchr(token, '|');
+ if (end == NULL)
+ bail("malformed prompt line near %s", token);
+ *end = '\0';
+ prompt->prompt = expand_string(token, config);
+ token = end + 1;
+ prompt->response = expand_string(token, config);
+ }
+ prompts->size++;
+ free(line);
+ }
+ if (line != NULL) {
+ free(line);
+ rewind_section(script, length);
+ }
+ return prompts;
+}
+
+
+/*
+ * Parse a PAM interaction script. This handles parsing of the top-level
+ * section markers and dispatches the parsing to other functions. Returns the
+ * total work to do as a work struct.
+ */
+struct work *
+parse_script(FILE *script, const struct script_config *config)
+{
+ struct work *work;
+ char *line, *token;
+
+ work = bmalloc(sizeof(struct work));
+ memset(work, 0, sizeof(struct work));
+ work->end_flags = PAM_SUCCESS;
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ token = strtok(line, " ");
+ if (token[0] != '[')
+ bail("line outside of section: %s", line);
+ if (strcmp(token, "[options]") == 0)
+ parse_options(script, work, config);
+ else if (strcmp(token, "[run]") == 0)
+ work->actions = parse_run(script);
+ else if (strcmp(token, "[end]") == 0)
+ work->end_flags = parse_end(script);
+ else if (strcmp(token, "[output]") == 0)
+ work->output = parse_output(script, config);
+ else if (strcmp(token, "[prompts]") == 0)
+ work->prompts = parse_prompts(script, config);
+ else
+ bail("unknown section: %s", token);
+ free(line);
+ }
+ if (work->actions == NULL)
+ bail("no run section defined");
+ return work;
+}
diff --git a/tests/fakepam/data.c b/tests/fakepam/data.c
new file mode 100644
index 000000000000..0650d59e9b75
--- /dev/null
+++ b/tests/fakepam/data.c
@@ -0,0 +1,356 @@
+/*
+ * Data manipulation functions for the fake PAM library, used for testing.
+ *
+ * This file contains the implementation of pam_get_* and pam_set_* for the
+ * various data items supported by the PAM library, plus the PAM environment
+ * manipulation functions.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/pam.h>
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+
+/*
+ * Return a stored PAM data element in the provided data variable. As a
+ * special case, if the data is NULL, pretend it doesn't exist.
+ */
+int
+pam_get_data(const pam_handle_t *pamh, const char *name, const void **data)
+{
+ struct fakepam_data *item;
+
+ for (item = pamh->data; item != NULL; item = item->next)
+ if (strcmp(item->name, name) == 0) {
+ if (item->data == NULL)
+ return PAM_NO_MODULE_DATA;
+ *data = item->data;
+ return PAM_SUCCESS;
+ }
+ return PAM_NO_MODULE_DATA;
+}
+
+
+/*
+ * Store a data item. Replaces the existing data item (calling its cleanup)
+ * if it is already set; otherwise, add a new data item.
+ */
+int
+pam_set_data(pam_handle_t *pamh, const char *item, void *data,
+ void (*cleanup)(pam_handle_t *, void *, int))
+{
+ struct fakepam_data *p;
+
+ for (p = pamh->data; p != NULL; p = p->next)
+ if (strcmp(p->name, item) == 0) {
+ if (p->cleanup != NULL)
+ p->cleanup(pamh, p->data, PAM_DATA_REPLACE);
+ p->data = data;
+ p->cleanup = cleanup;
+ return PAM_SUCCESS;
+ }
+ p = malloc(sizeof(struct fakepam_data));
+ if (p == NULL)
+ return PAM_BUF_ERR;
+ p->name = strdup(item);
+ if (p->name == NULL) {
+ free(p);
+ return PAM_BUF_ERR;
+ }
+ p->data = data;
+ p->cleanup = cleanup;
+ p->next = pamh->data;
+ pamh->data = p;
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Retrieve a PAM item. Currently, this only supports a limited subset of the
+ * possible items.
+ */
+int
+pam_get_item(const pam_handle_t *pamh, int item, PAM_CONST void **data)
+{
+ switch (item) {
+ case PAM_AUTHTOK:
+ *data = pamh->authtok;
+ return PAM_SUCCESS;
+ case PAM_CONV:
+ if (pamh->conversation) {
+ *data = pamh->conversation;
+ return PAM_SUCCESS;
+ } else {
+ return PAM_BAD_ITEM;
+ }
+ case PAM_OLDAUTHTOK:
+ *data = pamh->oldauthtok;
+ return PAM_SUCCESS;
+ case PAM_RHOST:
+ *data = (PAM_CONST char *) pamh->rhost;
+ return PAM_SUCCESS;
+ case PAM_RUSER:
+ *data = (PAM_CONST char *) pamh->ruser;
+ return PAM_SUCCESS;
+ case PAM_SERVICE:
+ *data = (PAM_CONST char *) pamh->service;
+ return PAM_SUCCESS;
+ case PAM_TTY:
+ *data = (PAM_CONST char *) pamh->tty;
+ return PAM_SUCCESS;
+ case PAM_USER:
+ *data = (PAM_CONST char *) pamh->user;
+ return PAM_SUCCESS;
+ case PAM_USER_PROMPT:
+ *data = "login: ";
+ return PAM_SUCCESS;
+ default:
+ return PAM_BAD_ITEM;
+ }
+}
+
+
+/*
+ * Set a PAM item. Currently only PAM_USER is supported.
+ */
+int
+pam_set_item(pam_handle_t *pamh, int item, PAM_CONST void *data)
+{
+ switch (item) {
+ case PAM_AUTHTOK:
+ free(pamh->authtok);
+ pamh->authtok = strdup(data);
+ if (pamh->authtok == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_OLDAUTHTOK:
+ free(pamh->oldauthtok);
+ pamh->oldauthtok = strdup(data);
+ if (pamh->oldauthtok == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_RHOST:
+ free(pamh->rhost);
+ pamh->rhost = strdup(data);
+ if (pamh->rhost == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_RUSER:
+ free(pamh->ruser);
+ pamh->ruser = strdup(data);
+ if (pamh->ruser == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_TTY:
+ free(pamh->tty);
+ pamh->tty = strdup(data);
+ if (pamh->tty == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_USER:
+ pamh->user = (const char *) data;
+ return PAM_SUCCESS;
+ default:
+ return PAM_BAD_ITEM;
+ }
+}
+
+
+/*
+ * Return the user for the PAM context.
+ */
+int
+pam_get_user(pam_handle_t *pamh, PAM_CONST char **user,
+ const char *prompt UNUSED)
+{
+ if (pamh->user == NULL)
+ return PAM_CONV_ERR;
+ else {
+ *user = (PAM_CONST char *) pamh->user;
+ return PAM_SUCCESS;
+ }
+}
+
+
+/*
+ * Return a setting in the PAM environment.
+ */
+PAM_CONST char *
+pam_getenv(pam_handle_t *pamh, const char *name)
+{
+ size_t i;
+
+ if (pamh->environ == NULL)
+ return NULL;
+ for (i = 0; pamh->environ[i] != NULL; i++)
+ if (strncmp(name, pamh->environ[i], strlen(name)) == 0
+ && pamh->environ[i][strlen(name)] == '=')
+ return pamh->environ[i] + strlen(name) + 1;
+ return NULL;
+}
+
+
+/*
+ * Return a newly malloc'd copy of the complete PAM environment. This must be
+ * freed by the caller.
+ */
+char **
+pam_getenvlist(pam_handle_t *pamh)
+{
+ char **env;
+ size_t i;
+
+ if (pamh->environ == NULL) {
+ pamh->environ = malloc(sizeof(char *));
+ if (pamh->environ == NULL)
+ return NULL;
+ pamh->environ[0] = NULL;
+ }
+ for (i = 0; pamh->environ[i] != NULL; i++)
+ ;
+ env = calloc(i + 1, sizeof(char *));
+ if (env == NULL)
+ return NULL;
+ for (i = 0; pamh->environ[i] != NULL; i++) {
+ env[i] = strdup(pamh->environ[i]);
+ if (env[i] == NULL)
+ goto fail;
+ }
+ env[i] = NULL;
+ return env;
+
+fail:
+ for (i = 0; env[i] != NULL; i++)
+ free(env[i]);
+ free(env);
+ return NULL;
+}
+
+
+/*
+ * Add a setting to the PAM environment. If there is another existing
+ * variable with the same value, the value is replaced, unless the setting
+ * doesn't end in an equal sign. If it doesn't end in an equal sign, any
+ * existing environment variable of that name is removed. This follows the
+ * Linux PAM semantics.
+ *
+ * On HP-UX, there is no separate PAM environment, so the module just uses the
+ * main environment. For our tests to work on that platform, we therefore
+ * have to do the same thing.
+ */
+#ifdef HAVE_PAM_GETENV
+int
+pam_putenv(pam_handle_t *pamh, const char *setting)
+{
+ char *copy = NULL;
+ const char *equals;
+ size_t namelen;
+ bool delete = false;
+ bool found = false;
+ size_t i, j;
+ char **env;
+
+ equals = strchr(setting, '=');
+ if (equals != NULL)
+ namelen = equals - setting;
+ else {
+ delete = true;
+ namelen = strlen(setting);
+ }
+ if (!delete) {
+ copy = strdup(setting);
+ if (copy == NULL)
+ return PAM_BUF_ERR;
+ }
+
+ /* Handle the first call to pam_putenv. */
+ if (pamh->environ == NULL) {
+ if (delete)
+ return PAM_BAD_ITEM;
+ pamh->environ = calloc(2, sizeof(char *));
+ if (pamh->environ == NULL) {
+ free(copy);
+ return PAM_BUF_ERR;
+ }
+ pamh->environ[0] = copy;
+ pamh->environ[1] = NULL;
+ return PAM_SUCCESS;
+ }
+
+ /*
+ * We have an existing array. See if we're replacing a value, deleting a
+ * value, or adding a new one. When deleting, waste a bit of memory but
+ * save some time by not bothering to reduce the size of the array.
+ */
+ for (i = 0; pamh->environ[i] != NULL; i++)
+ if (strncmp(setting, pamh->environ[i], namelen) == 0
+ && pamh->environ[i][namelen] == '=') {
+ if (delete) {
+ free(pamh->environ[i]);
+ for (j = i + 1; pamh->environ[j] != NULL; i++, j++)
+ pamh->environ[i] = pamh->environ[j];
+ pamh->environ[i] = NULL;
+ } else {
+ free(pamh->environ[i]);
+ pamh->environ[i] = copy;
+ }
+ found = true;
+ break;
+ }
+ if (!found) {
+ if (delete)
+ return PAM_BAD_ITEM;
+ env = reallocarray(pamh->environ, (i + 2), sizeof(char *));
+ if (env == NULL) {
+ free(copy);
+ return PAM_BUF_ERR;
+ }
+ pamh->environ = env;
+ pamh->environ[i] = copy;
+ pamh->environ[i + 1] = NULL;
+ }
+ return PAM_SUCCESS;
+}
+
+#else /* !HAVE_PAM_GETENV */
+
+int
+pam_putenv(pam_handle_t *pamh UNUSED, const char *setting)
+{
+ return putenv((char *) setting);
+}
+
+#endif /* !HAVE_PAM_GETENV */
diff --git a/tests/fakepam/general.c b/tests/fakepam/general.c
new file mode 100644
index 000000000000..0f11bb2f7995
--- /dev/null
+++ b/tests/fakepam/general.c
@@ -0,0 +1,151 @@
+/*
+ * Interface for fake PAM library, used for testing.
+ *
+ * This contains the basic public interfaces for the fake PAM library, used
+ * for testing, and some general utility functions.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+#include <pwd.h>
+
+#include <tests/fakepam/pam.h>
+
+/* Stores the static struct passwd returned by getpwnam if the name matches. */
+static struct passwd *pwd_info = NULL;
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+
+/*
+ * Initializes the pam_handle_t data structure. This function is only called
+ * from test programs, not from any of the module code. We can put anything
+ * we want in this structure, since it's opaque to the regular code.
+ */
+int
+pam_start(const char *service_name, const char *user,
+ const struct pam_conv *pam_conversation, pam_handle_t **pamh)
+{
+ struct pam_handle *handle;
+
+ handle = calloc(1, sizeof(struct pam_handle));
+ if (handle == NULL)
+ return PAM_BUF_ERR;
+ handle->service = service_name;
+ handle->user = user;
+ handle->conversation = pam_conversation;
+ *pamh = handle;
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Free the pam_handle_t data structure and related resources. This is
+ * important to test the data cleanups. Freeing the memory is not strictly
+ * required since it's only used for testing, but it helps keep our memory
+ * usage clean so that we can run the test suite under valgrind.
+ */
+int
+pam_end(pam_handle_t *pamh, int status)
+{
+ struct fakepam_data *item, *next;
+ size_t i;
+
+ if (pamh->environ != NULL) {
+ for (i = 0; pamh->environ[i] != NULL; i++)
+ free(pamh->environ[i]);
+ free(pamh->environ);
+ }
+ free(pamh->authtok);
+ free(pamh->oldauthtok);
+ free(pamh->rhost);
+ free(pamh->ruser);
+ free(pamh->tty);
+ for (item = pamh->data; item != NULL;) {
+ if (item->cleanup != NULL)
+ item->cleanup(pamh, item->data, status);
+ free(item->name);
+ next = item->next;
+ free(item);
+ item = next;
+ }
+ free(pamh);
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Interface specific to this fake PAM library to set the struct passwd that's
+ * returned by getpwnam queries if the name matches.
+ */
+void
+pam_set_pwd(struct passwd *pwd)
+{
+ pwd_info = pwd;
+}
+
+
+/*
+ * For testing purposes, we want to be able to intercept getpwnam. This is
+ * fairly easy on platforms that have pam_modutil_getpwnam, since then our
+ * code will always call that function and we can provide an implementation
+ * that does whatever we want. For platforms that don't have that function,
+ * we'll try to intercept the C library getpwnam function.
+ *
+ * We store only one struct passwd data structure statically. If the user
+ * we're looking up matches that, we return it; otherwise, we return NULL.
+ */
+#ifdef HAVE_PAM_MODUTIL_GETPWNAM
+struct passwd *
+pam_modutil_getpwnam(pam_handle_t *pamh UNUSED, const char *name)
+{
+ if (pwd_info != NULL && strcmp(pwd_info->pw_name, name) == 0)
+ return pwd_info;
+ else {
+ errno = 0;
+ return NULL;
+ }
+}
+#else
+struct passwd *
+getpwnam(const char *name)
+{
+ if (pwd_info != NULL && strcmp(pwd_info->pw_name, name) == 0)
+ return pwd_info;
+ else {
+ errno = 0;
+ return NULL;
+ }
+}
+#endif
diff --git a/tests/fakepam/internal.h b/tests/fakepam/internal.h
new file mode 100644
index 000000000000..3c6fedacd45e
--- /dev/null
+++ b/tests/fakepam/internal.h
@@ -0,0 +1,119 @@
+/*
+ * Internal data types and prototypes for the fake PAM test framework.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef FAKEPAM_INTERNAL_H
+#define FAKEPAM_INTERNAL_H 1
+
+#include <portable/pam.h>
+#include <sys/types.h>
+
+/* Forward declarations to avoid unnecessary includes. */
+struct output;
+struct script_config;
+
+/* The type of a PAM module call. */
+typedef int (*pam_call)(pam_handle_t *, int, int, const char **);
+
+/* The possible PAM groups as element numbers in an array of options. */
+enum group_type
+{
+ GROUP_ACCOUNT = 0,
+ GROUP_AUTH = 1,
+ GROUP_PASSWORD = 2,
+ GROUP_SESSION = 3,
+};
+
+/* Holds a PAM argc and argv. */
+struct options {
+ char **argv;
+ int argc;
+};
+
+/*
+ * Holds a linked list of actions: a PAM call that should return some
+ * status.
+ */
+struct action {
+ char *name;
+ pam_call call;
+ int flags;
+ enum group_type group;
+ int status;
+ struct action *next;
+};
+
+/* Holds an expected PAM prompt style, the prompt, and the response. */
+struct prompt {
+ int style;
+ char *prompt;
+ char *response;
+};
+
+/* Holds an array of PAM prompts and the current index into that array. */
+struct prompts {
+ struct prompt *prompts;
+ size_t size;
+ size_t allocated;
+ size_t current;
+};
+
+/*
+ * Holds the complete set of things that we should do, configuration for them,
+ * and expected output and return values.
+ */
+struct work {
+ struct options options[4];
+ struct action *actions;
+ struct prompts *prompts;
+ struct output *output;
+ int end_flags;
+};
+
+BEGIN_DECLS
+
+
+/* Create a new output struct. */
+struct output *output_new(void);
+
+/* Add a new output line (with numeric priority) to an output struct. */
+void output_add(struct output *, int, const char *);
+
+
+/*
+ * Parse a PAM interaction script. Returns the total work to do as a work
+ * struct.
+ */
+struct work *parse_script(FILE *, const struct script_config *);
+
+END_DECLS
+
+#endif /* !FAKEPAM_API_H */
diff --git a/tests/fakepam/kuserok.c b/tests/fakepam/kuserok.c
new file mode 100644
index 000000000000..d66bc1d03acc
--- /dev/null
+++ b/tests/fakepam/kuserok.c
@@ -0,0 +1,119 @@
+/*
+ * Replacement for krb5_kuserok for testing.
+ *
+ * This is a reimplementation of krb5_kuserok that uses the replacement
+ * getpwnam function and the special passwd struct internal to the fake PAM
+ * module to locate .k5login. The default Kerberos krb5_kuserok always calls
+ * the system getpwnam, which we may not be able to intercept, and will
+ * therefore fail because it can't locate the .k5login file for the test user
+ * (or succeed oddly because it finds some random file on the testing system).
+ *
+ * This implementation is drastically simplified from the Kerberos library
+ * version, and much less secure (which shouldn't matter since it's only
+ * acting on test data).
+ *
+ * This is an optional part of the fake PAM library and can be omitted when
+ * testing modules that don't use Kerberos.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/tap/string.h>
+
+
+/*
+ * Given a Kerberos principal representing the authenticated identity and the
+ * username of the local account, return true if that principal is authorized
+ * to log on to that account. The principal is authorized if the .k5login
+ * file does not exist and the user matches the localname form of the
+ * principal, or if the file does exist and the principal is listed in it.
+ *
+ * This version retrieves the home directory from the internal fake PAM
+ * library path.
+ */
+krb5_boolean
+krb5_kuserok(krb5_context ctx, krb5_principal princ, const char *user)
+{
+ char *principal, *path;
+ struct passwd *pwd;
+ FILE *file;
+ krb5_error_code code;
+ char buffer[BUFSIZ];
+ bool found = false;
+#ifdef HAVE_PAM_MODUTIL_GETPWNAM
+ struct pam_handle pamh;
+#endif
+
+ /*
+ * Find .k5login and confirm if it exists. If it doesn't, fall back on
+ * krb5_aname_to_localname.
+ */
+#ifdef HAVE_PAM_MODUTIL_GETPWNAM
+ memset(&pamh, 0, sizeof(pamh));
+ pwd = pam_modutil_getpwnam(&pamh, user);
+#else
+ pwd = getpwnam(user);
+#endif
+ if (pwd == NULL)
+ return false;
+ basprintf(&path, "%s/.k5login", pwd->pw_dir);
+ if (access(path, R_OK) < 0) {
+ free(path);
+ code = krb5_aname_to_localname(ctx, princ, sizeof(buffer), buffer);
+ return (code == 0 && strcmp(buffer, user) == 0);
+ }
+ file = fopen(path, "r");
+ if (file == NULL) {
+ free(path);
+ return false;
+ }
+ free(path);
+
+ /* .k5login exists. Scan it for the principal. */
+ if (krb5_unparse_name(ctx, princ, &principal) != 0) {
+ fclose(file);
+ return false;
+ }
+ while (!found && (fgets(buffer, sizeof(buffer), file) != NULL)) {
+ if (buffer[strlen(buffer) - 1] == '\n')
+ buffer[strlen(buffer) - 1] = '\0';
+ if (strcmp(buffer, principal) == 0)
+ found = true;
+ }
+ fclose(file);
+ krb5_free_unparsed_name(ctx, principal);
+ return found;
+}
diff --git a/tests/fakepam/logging.c b/tests/fakepam/logging.c
new file mode 100644
index 000000000000..c3a3fa044576
--- /dev/null
+++ b/tests/fakepam/logging.c
@@ -0,0 +1,183 @@
+/*
+ * Logging functions for the fake PAM library, used for testing.
+ *
+ * This file contains the implementation of pam_syslog and pam_vsyslog, which
+ * log to an internal buffer rather than to syslog, and the testing function
+ * used to recover that buffer. It also includes the pam_strerror
+ * implementation.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/internal.h>
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+/* The struct used to accumulate log messages. */
+static struct output *messages = NULL;
+
+
+/*
+ * Allocate a new, empty output struct and call bail if memory allocation
+ * fails.
+ */
+struct output *
+output_new(void)
+{
+ struct output *output;
+
+ output = bmalloc(sizeof(struct output));
+ output->count = 0;
+ output->allocated = 1;
+ output->lines = bmalloc(sizeof(output->lines[0]));
+ output->lines[0].line = NULL;
+ return output;
+}
+
+
+/*
+ * Add a new output line to the output struct, resizing the array as
+ * necessary. Calls bail if memory allocation fails.
+ */
+void
+output_add(struct output *output, int priority, const char *string)
+{
+ size_t next = output->count;
+ size_t size, n;
+
+ if (output->count == output->allocated) {
+ n = output->allocated + 1;
+ size = sizeof(output->lines[0]);
+ output->lines = breallocarray(output->lines, n, size);
+ output->allocated = n;
+ }
+ output->lines[next].priority = priority;
+ output->lines[next].line = bstrdup(string);
+ output->count++;
+}
+
+
+/*
+ * Return the error string associated with the PAM error code. We do this as
+ * a giant case statement so that we don't assume anything about the error
+ * codes used by the system PAM library.
+ */
+const char *
+pam_strerror(PAM_STRERROR_CONST pam_handle_t *pamh UNUSED, int code)
+{
+ /* clang-format off */
+ switch (code) {
+ case PAM_SUCCESS: return "No error";
+ case PAM_OPEN_ERR: return "Failure loading service module";
+ case PAM_SYMBOL_ERR: return "Symbol not found";
+ case PAM_SERVICE_ERR: return "Error in service module";
+ case PAM_SYSTEM_ERR: return "System error";
+ case PAM_BUF_ERR: return "Memory buffer error";
+ default: return "Unknown error";
+ }
+ /* clang-format on */
+}
+
+
+/*
+ * Log a message using variadic arguments. Just a wrapper around
+ * pam_vsyslog.
+ */
+void
+pam_syslog(const pam_handle_t *pamh, int priority, const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ pam_vsyslog(pamh, priority, format, args);
+ va_end(args);
+}
+
+
+/*
+ * Log a PAM error message with a given priority. Just appends the priority,
+ * a space, and the error message, followed by a newline, to the internal
+ * buffer, allocating new space if needed. Ignore memory allocation failures;
+ * we have no way of reporting them, but the tests will fail due to missing
+ * output.
+ */
+void
+pam_vsyslog(const pam_handle_t *pamh UNUSED, int priority, const char *format,
+ va_list args)
+{
+ char *message = NULL;
+
+ bvasprintf(&message, format, args);
+ if (messages == NULL)
+ messages = output_new();
+ output_add(messages, priority, message);
+ free(message);
+}
+
+
+/*
+ * Used by test code. Returns the accumulated messages in an output struct
+ * and starts a new one. Caller is responsible for freeing with
+ * pam_output_free.
+ */
+struct output *
+pam_output(void)
+{
+ struct output *output;
+
+ output = messages;
+ messages = NULL;
+ return output;
+}
+
+
+/*
+ * Free an output struct.
+ */
+void
+pam_output_free(struct output *output)
+{
+ size_t i;
+
+ if (output == NULL)
+ return;
+ for (i = 0; i < output->count; i++)
+ if (output->lines[i].line != NULL)
+ free(output->lines[i].line);
+ free(output->lines);
+ free(output);
+}
diff --git a/tests/fakepam/pam.h b/tests/fakepam/pam.h
new file mode 100644
index 000000000000..41f508e0f31f
--- /dev/null
+++ b/tests/fakepam/pam.h
@@ -0,0 +1,101 @@
+/*
+ * Testing interfaces to the fake PAM library.
+ *
+ * This header defines the interfaces to the fake PAM library that are used by
+ * test code to initialize the library and recover test data from it. We
+ * don't define any interface that we're going to duplicate from the main PAM
+ * API.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef FAKEPAM_PAM_H
+#define FAKEPAM_PAM_H 1
+
+#include <config.h>
+#include <portable/macros.h>
+#include <portable/pam.h>
+
+/* Used inside the fake PAM library to hold data items. */
+struct fakepam_data {
+ char *name;
+ void *data;
+ void (*cleanup)(pam_handle_t *, void *, int);
+ struct fakepam_data *next;
+};
+
+/* This is an opaque data structure, so we can put whatever we want in it. */
+struct pam_handle {
+ const char *service;
+ const char *user;
+ char *authtok;
+ char *oldauthtok;
+ char *rhost;
+ char *ruser;
+ char *tty;
+ const struct pam_conv *conversation;
+ char **environ;
+ struct fakepam_data *data;
+ struct passwd *pwd;
+};
+
+/*
+ * Used to accumulate output from the PAM module. Each call to a logging
+ * function will result in an additional line added to the array, and count
+ * will hold the total.
+ */
+struct output {
+ size_t count;
+ size_t allocated;
+ struct {
+ int priority;
+ char *line;
+ } * lines;
+};
+
+BEGIN_DECLS
+
+/*
+ * Sets the struct passwd returned by getpwnam calls. The last struct passed
+ * to this function will be returned provided the pw_name matches.
+ */
+void pam_set_pwd(struct passwd *pwd);
+
+/*
+ * Returns the accumulated messages logged with pam_syslog or pam_vsyslog
+ * since the last call to pam_output and then clears the output. Returns
+ * newly allocated memory that the caller is responsible for freeing with
+ * pam_output_free, or NULL if no output has been logged since the last call
+ * or since startup.
+ */
+struct output *pam_output(void);
+void pam_output_free(struct output *);
+
+END_DECLS
+
+#endif /* !FAKEPAM_API_H */
diff --git a/tests/fakepam/script.c b/tests/fakepam/script.c
new file mode 100644
index 000000000000..6f3812577960
--- /dev/null
+++ b/tests/fakepam/script.c
@@ -0,0 +1,411 @@
+/*
+ * Run a PAM interaction script for testing.
+ *
+ * Provides an interface that loads a PAM interaction script from a file and
+ * runs through that script, calling the internal PAM module functions and
+ * checking their results. This allows automation of PAM testing through
+ * external data files instead of coding everything in C.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2016, 2018, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#ifdef HAVE_REGCOMP
+# include <regex.h>
+#endif
+#include <syslog.h>
+
+#include <tests/fakepam/internal.h>
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/macros.h>
+#include <tests/tap/string.h>
+
+
+/*
+ * Compare a regex to a string. If regular expression support isn't
+ * available, we skip this test.
+ */
+#ifdef HAVE_REGCOMP
+static void __attribute__((__format__(printf, 3, 4)))
+like(const char *wanted, const char *seen, const char *format, ...)
+{
+ va_list args;
+ regex_t regex;
+ char err[BUFSIZ];
+ int status;
+
+ if (seen == NULL) {
+ fflush(stderr);
+ printf("# wanted: /%s/\n# seen: (null)\n", wanted);
+ va_start(args, format);
+ okv(0, format, args);
+ va_end(args);
+ return;
+ }
+ memset(&regex, 0, sizeof(regex));
+ status = regcomp(&regex, wanted, REG_EXTENDED | REG_NOSUB);
+ if (status != 0) {
+ regerror(status, &regex, err, sizeof(err));
+ bail("invalid regex /%s/: %s", wanted, err);
+ }
+ status = regexec(&regex, seen, 0, NULL, 0);
+ switch (status) {
+ case 0:
+ va_start(args, format);
+ okv(1, format, args);
+ va_end(args);
+ break;
+ case REG_NOMATCH:
+ printf("# wanted: /%s/\n# seen: %s\n", wanted, seen);
+ va_start(args, format);
+ okv(0, format, args);
+ va_end(args);
+ break;
+ default:
+ regerror(status, &regex, err, sizeof(err));
+ bail("regexec failed for regex /%s/: %s", wanted, err);
+ }
+ regfree(&regex);
+}
+#else /* !HAVE_REGCOMP */
+static void
+like(const char *wanted, const char *seen, const char *format UNUSED, ...)
+{
+ diag("wanted /%s/", wanted);
+ diag(" seen %s", seen);
+ skip("regex support not available");
+}
+#endif /* !HAVE_REGCOMP */
+
+
+/*
+ * Compare an expected string with a seen string, used by both output checking
+ * and prompt checking. This is a separate function because the expected
+ * string may be a regex, determined by seeing if it starts and ends with a
+ * slash (/), which may require a regex comparison.
+ *
+ * Eventually calls either is_string or ok to report results via TAP.
+ */
+static void __attribute__((__format__(printf, 3, 4)))
+compare_string(char *wanted, char *seen, const char *format, ...)
+{
+ va_list args;
+ char *comment, *regex;
+ size_t length;
+
+ /* Format the comment since we need it regardless. */
+ va_start(args, format);
+ bvasprintf(&comment, format, args);
+ va_end(args);
+
+ /* Check whether the wanted string is a regex. */
+ length = strlen(wanted);
+ if (wanted[0] == '/' && wanted[length - 1] == '/') {
+ regex = bstrndup(wanted + 1, length - 2);
+ like(regex, seen, "%s", comment);
+ free(regex);
+ } else {
+ is_string(wanted, seen, "%s", comment);
+ }
+ free(comment);
+}
+
+
+/*
+ * The PAM conversation function. Takes the prompts struct from the
+ * configuration and interacts appropriately. If a prompt is of the expected
+ * type but not the expected string, it still responds; if it's not of the
+ * expected type, it returns PAM_CONV_ERR.
+ *
+ * Currently only handles a single prompt at a time.
+ */
+static int
+converse(int num_msg, const struct pam_message **msg,
+ struct pam_response **resp, void *appdata_ptr)
+{
+ struct prompts *prompts = appdata_ptr;
+ struct prompt *prompt;
+ char *message;
+ size_t length;
+ int i;
+
+ *resp = bcalloc(num_msg, sizeof(struct pam_response));
+ for (i = 0; i < num_msg; i++) {
+ message = bstrdup(msg[i]->msg);
+
+ /* Remove newlines for comparison purposes. */
+ length = strlen(message);
+ while (length > 0 && message[length - 1] == '\n')
+ message[length-- - 1] = '\0';
+
+ /* Check if we've gotten too many prompts but quietly ignore them. */
+ if (prompts->current >= prompts->size) {
+ diag("unexpected prompt: %s", message);
+ free(message);
+ ok(0, "more prompts than expected");
+ continue;
+ }
+
+ /* Be sure everything matches and return the response, if any. */
+ prompt = &prompts->prompts[prompts->current];
+ is_int(prompt->style, msg[i]->msg_style, "style of prompt %lu",
+ (unsigned long) prompts->current + 1);
+ compare_string(prompt->prompt, message, "value of prompt %lu",
+ (unsigned long) prompts->current + 1);
+ free(message);
+ prompts->current++;
+ if (prompt->style == msg[i]->msg_style && prompt->response != NULL) {
+ (*resp)[i].resp = bstrdup(prompt->response);
+ (*resp)[i].resp_retcode = 0;
+ }
+ }
+
+ /*
+ * Always return success even if the prompts don't match. Otherwise,
+ * we're likely to abort the conversation in the middle and possibly
+ * leave passwords set incorrectly.
+ */
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Check the actual PAM output against the expected output. We divide the
+ * expected and seen output into separate lines and compare each one so that
+ * we can handle regular expressions and the output priority.
+ */
+static void
+check_output(const struct output *wanted, const struct output *seen)
+{
+ size_t i;
+
+ if (wanted == NULL && seen == NULL)
+ ok(1, "no output");
+ else if (wanted == NULL) {
+ for (i = 0; i < seen->count; i++)
+ diag("unexpected: (%d) %s", seen->lines[i].priority,
+ seen->lines[i].line);
+ ok(0, "no output");
+ } else if (seen == NULL) {
+ for (i = 0; i < wanted->count; i++) {
+ is_int(wanted->lines[i].priority, 0, "output priority %lu",
+ (unsigned long) i + 1);
+ is_string(wanted->lines[i].line, NULL, "output line %lu",
+ (unsigned long) i + 1);
+ }
+ } else {
+ for (i = 0; i < wanted->count && i < seen->count; i++) {
+ is_int(wanted->lines[i].priority, seen->lines[i].priority,
+ "output priority %lu", (unsigned long) i + 1);
+ compare_string(wanted->lines[i].line, seen->lines[i].line,
+ "output line %lu", (unsigned long) i + 1);
+ }
+ if (wanted->count > seen->count)
+ for (i = seen->count; i < wanted->count; i++) {
+ is_int(wanted->lines[i].priority, 0, "output priority %lu",
+ (unsigned long) i + 1);
+ is_string(wanted->lines[i].line, NULL, "output line %lu",
+ (unsigned long) i + 1);
+ }
+ if (seen->count > wanted->count) {
+ for (i = wanted->count; i < seen->count; i++)
+ diag("unexpected: (%d) %s", seen->lines[i].priority,
+ seen->lines[i].line);
+ ok(0, "unexpected output lines");
+ } else {
+ ok(1, "no excess output");
+ }
+ }
+}
+
+
+/*
+ * The core of the work. Given the path to a PAM interaction script, which
+ * may be relative to C_TAP_SOURCE or C_TAP_BUILD, the user (may be NULL), and
+ * the stored password (may be NULL), run that script, outputting the results
+ * in TAP format.
+ */
+void
+run_script(const char *file, const struct script_config *config)
+{
+ char *path;
+ struct output *output;
+ FILE *script;
+ struct work *work;
+ struct options *opts;
+ struct action *action, *oaction;
+ struct pam_conv conv = {NULL, NULL};
+ pam_handle_t *pamh;
+ int status;
+ size_t i, j;
+ const char *argv_empty[] = {NULL};
+
+ /* Open and parse the script. */
+ if (access(file, R_OK) == 0)
+ path = bstrdup(file);
+ else {
+ path = test_file_path(file);
+ if (path == NULL)
+ bail("cannot find PAM script %s", file);
+ }
+ script = fopen(path, "r");
+ if (script == NULL)
+ sysbail("cannot open %s", path);
+ work = parse_script(script, config);
+ fclose(script);
+ diag("Starting %s", file);
+ if (work->prompts != NULL) {
+ conv.conv = converse;
+ conv.appdata_ptr = work->prompts;
+ }
+
+ /* Initialize PAM. */
+ status = pam_start("test", config->user, &conv, &pamh);
+ if (status != PAM_SUCCESS)
+ sysbail("cannot create PAM handle");
+ if (config->authtok != NULL)
+ pamh->authtok = bstrdup(config->authtok);
+ if (config->oldauthtok != NULL)
+ pamh->oldauthtok = bstrdup(config->oldauthtok);
+
+ /* Run the actions and check their return status. */
+ for (action = work->actions; action != NULL; action = action->next) {
+ if (work->options[action->group].argv == NULL)
+ status = (*action->call)(pamh, action->flags, 0, argv_empty);
+ else {
+ opts = &work->options[action->group];
+ status = (*action->call)(pamh, action->flags, opts->argc,
+ (const char **) opts->argv);
+ }
+ is_int(action->status, status, "status for %s", action->name);
+ }
+ output = pam_output();
+ check_output(work->output, output);
+ pam_output_free(output);
+
+ /* If we have a test callback, call it now. */
+ if (config->callback != NULL)
+ config->callback(pamh, config, config->data);
+
+ /* Free memory and return. */
+ pam_end(pamh, work->end_flags);
+ action = work->actions;
+ while (action != NULL) {
+ free(action->name);
+ oaction = action;
+ action = action->next;
+ free(oaction);
+ }
+ for (i = 0; i < ARRAY_SIZE(work->options); i++)
+ if (work->options[i].argv != NULL) {
+ for (j = 0; work->options[i].argv[j] != NULL; j++)
+ free(work->options[i].argv[j]);
+ free(work->options[i].argv);
+ }
+ if (work->output)
+ pam_output_free(work->output);
+ if (work->prompts != NULL) {
+ for (i = 0; i < work->prompts->size; i++) {
+ free(work->prompts->prompts[i].prompt);
+ free(work->prompts->prompts[i].response);
+ }
+ free(work->prompts->prompts);
+ free(work->prompts);
+ }
+ free(work);
+ free(path);
+}
+
+
+/*
+ * Check a filename for acceptable characters. Returns true if the file
+ * consists solely of [a-zA-Z0-9-] and false otherwise.
+ */
+static bool
+valid_filename(const char *filename)
+{
+ const char *p;
+
+ for (p = filename; *p != '\0'; p++) {
+ if (*p >= 'A' && *p <= 'Z')
+ continue;
+ if (*p >= 'a' && *p <= 'z')
+ continue;
+ if (*p >= '0' && *p <= '9')
+ continue;
+ if (*p == '-')
+ continue;
+ return false;
+ }
+ return true;
+}
+
+
+/*
+ * The same as run_script, but run every script found in the given directory,
+ * skipping file names that contain characters other than alphanumerics and -.
+ */
+void
+run_script_dir(const char *dir, const struct script_config *config)
+{
+ DIR *handle;
+ struct dirent *entry;
+ const char *path;
+ char *file;
+
+ if (access(dir, R_OK) == 0)
+ path = dir;
+ else
+ path = test_file_path(dir);
+ handle = opendir(path);
+ if (handle == NULL)
+ sysbail("cannot open directory %s", dir);
+ errno = 0;
+ while ((entry = readdir(handle)) != NULL) {
+ if (!valid_filename(entry->d_name))
+ continue;
+ basprintf(&file, "%s/%s", path, entry->d_name);
+ run_script(file, config);
+ free(file);
+ errno = 0;
+ }
+ if (errno != 0)
+ sysbail("cannot read directory %s", dir);
+ closedir(handle);
+ if (path != dir)
+ test_file_path_free((char *) path);
+}
diff --git a/tests/fakepam/script.h b/tests/fakepam/script.h
new file mode 100644
index 000000000000..c99fc12f55d2
--- /dev/null
+++ b/tests/fakepam/script.h
@@ -0,0 +1,82 @@
+/*
+ * PAM interaction script API.
+ *
+ * Provides an interface that loads a PAM interaction script from a file and
+ * runs through that script, calling the internal PAM module functions and
+ * checking their results. This allows automation of PAM testing through
+ * external data files instead of coding everything in C.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2016 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TESTS_MODULE_SCRIPT_H
+#define TESTS_MODULE_SCRIPT_H 1
+
+#include <portable/pam.h>
+
+#include <tests/tap/basic.h>
+
+/* A test callback called after PAM functions are run but before pam_end. */
+struct script_config;
+typedef void (*script_callback)(pam_handle_t *, const struct script_config *,
+ void *);
+
+/* Configuration for the PAM interaction script API. */
+struct script_config {
+ const char *user; /* Username to pass into pam_start (%u). */
+ const char *password; /* Substituted for %p in prompts. */
+ const char *newpass; /* Substituted for %n in prompts. */
+ const char *extra[10]; /* Substituted for %0-%9 in logging. */
+ const char *authtok; /* Stored as AUTHTOK before PAM. */
+ const char *oldauthtok; /* Stored as OLDAUTHTOK before PAM. */
+ script_callback callback; /* Called after PAM, before pam_end. */
+ void *data; /* Passed to the callback function. */
+};
+
+BEGIN_DECLS
+
+/*
+ * Given the file name of an interaction script (which may be a full path or
+ * relative to C_TAP_SOURCE or C_TAP_BUILD) and configuration containing other
+ * parameters such as the user, run that script, reporting the results via the
+ * TAP format.
+ */
+void run_script(const char *file, const struct script_config *)
+ __attribute__((__nonnull__));
+
+/*
+ * The same as run_script, but run every script found in the given directory,
+ * skipping file names that contain characters other than alphanumerics and -.
+ */
+void run_script_dir(const char *dir, const struct script_config *)
+ __attribute__((__nonnull__));
+
+END_DECLS
+
+#endif /* !TESTS_MODULE_SCRIPT_H */
diff --git a/tests/module/alt-auth-t.c b/tests/module/alt-auth-t.c
new file mode 100644
index 000000000000..df32ff941001
--- /dev/null
+++ b/tests/module/alt-auth-t.c
@@ -0,0 +1,117 @@
+/*
+ * Tests for the alt_auth_map functionality in libpam-krb5.
+ *
+ * This test case tests the variations of the alt_auth_map functionality for
+ * both authentication and account management. It requires a Kerberos
+ * configuration, but does not attempt to save a session ticket cache (to
+ * avoid requiring user configuration).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ char *user;
+
+ /*
+ * Load the Kerberos principal and password from a file, but set the
+ * principal as extra[0] and use something else bogus as the user. We
+ * want to test that alt_auth_map works when there's no relationship
+ * between the mapped principal and the user.
+ */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = "bogus-nonexistent-account";
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->username;
+ config.extra[1] = krbconf->userprinc;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+ config.extra[2] = "bogus.example.com";
+
+ /* Test without password prompting. */
+ plan_lazy();
+ run_script("data/scripts/alt-auth/basic", &config);
+ run_script("data/scripts/alt-auth/basic-debug", &config);
+ run_script("data/scripts/alt-auth/fail", &config);
+ run_script("data/scripts/alt-auth/fail-debug", &config);
+ run_script("data/scripts/alt-auth/force", &config);
+ run_script("data/scripts/alt-auth/only", &config);
+
+ /*
+ * If the alternate account exists but the password is incorrect, we
+ * should not fall back to the regular account. Test with debug so that
+ * we don't need two principals configured.
+ */
+ config.authtok = "bogus incorrect password";
+ run_script("data/scripts/alt-auth/force-fail-debug", &config);
+
+ /*
+ * Switch to our correct user (but wrong realm) realm to test username
+ * mapping to a different realm.
+ */
+ config.authtok = krbconf->password;
+ config.user = krbconf->username;
+ config.extra[2] = krbconf->realm;
+ run_script("data/scripts/alt-auth/username-map", &config);
+
+ /*
+ * Split the username into two parts, one in the PAM configuration and one
+ * in the real username, so that we can test interpolation of the username
+ * when %s isn't the first token.
+ */
+ config.user = &krbconf->username[1];
+ user = bstrndup(krbconf->username, 1);
+ config.extra[3] = user;
+ run_script("data/scripts/alt-auth/username-map-prefix", &config);
+ free(user);
+ config.extra[3] = NULL;
+
+ /*
+ * Ensure that we don't add the realm of the authentication username when
+ * the alt_auth_map already includes a realm.
+ */
+ basprintf(&user, "%s@foo.example.com", krbconf->username);
+ config.user = user;
+ diag("re-running username-map with fully-qualified PAM user");
+ run_script("data/scripts/alt-auth/username-map", &config);
+ free(user);
+
+ /*
+ * Add the password and make the user match our authentication principal,
+ * and then test fallback to normal authentication when alternative
+ * authentication fails.
+ */
+ config.user = krbconf->userprinc;
+ config.password = krbconf->password;
+ config.extra[2] = krbconf->realm;
+ run_script("data/scripts/alt-auth/fallback", &config);
+ run_script("data/scripts/alt-auth/fallback-debug", &config);
+ run_script("data/scripts/alt-auth/fallback-realm", &config);
+ run_script("data/scripts/alt-auth/force-fallback", &config);
+ run_script("data/scripts/alt-auth/only-fail", &config);
+
+ return 0;
+}
diff --git a/tests/module/bad-authtok-t.c b/tests/module/bad-authtok-t.c
new file mode 100644
index 000000000000..385dd5946849
--- /dev/null
+++ b/tests/module/bad-authtok-t.c
@@ -0,0 +1,53 @@
+/*
+ * Authentication tests for the pam-krb5 module with an incorrect AUTHTOK.
+ *
+ * This test case includes tests that require Kerberos to be configured and a
+ * username and password available and that run with an incorrect AUTHTOK
+ * already set. They test various prompting fallback cases. They don't write
+ * a ticket cache (which requires additional work to test the cache
+ * ownership).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->userprinc;
+ config.password = krbconf->password;
+
+ /* Set the authtok to something bogus. */
+ config.authtok = "BAD PASSWORD THAT WILL NOT WORK";
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ plan_lazy();
+ run_script_dir("data/scripts/bad-authtok", &config);
+
+ return 0;
+}
diff --git a/tests/module/basic-t.c b/tests/module/basic-t.c
new file mode 100644
index 000000000000..cacad5906ffb
--- /dev/null
+++ b/tests/module/basic-t.c
@@ -0,0 +1,67 @@
+/*
+ * Basic tests for the pam-krb5 module.
+ *
+ * This test case includes all tests that can be done without having Kerberos
+ * configured and a username and password available, and without any special
+ * configuration.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct passwd pwd;
+ char *uid;
+ char *uidplus;
+
+ plan_lazy();
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that this test will run on any system.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ /* Create a fake passwd struct for our user. */
+ memset(&pwd, 0, sizeof(pwd));
+ pwd.pw_name = (char *) "root";
+ pwd.pw_uid = getuid();
+ pwd.pw_gid = getgid();
+ pam_set_pwd(&pwd);
+
+ /*
+ * Attempt login as the root user to test ignore_root. Set our current
+ * UID and a UID one larger for testing minimum_uid.
+ */
+ basprintf(&uid, "%lu", (unsigned long) pwd.pw_uid);
+ basprintf(&uidplus, "%lu", (unsigned long) pwd.pw_uid + 1);
+ memset(&config, 0, sizeof(config));
+ config.user = "root";
+ config.extra[0] = uid;
+ config.extra[1] = uidplus;
+
+ run_script_dir("data/scripts/basic", &config);
+
+ free(uid);
+ free(uidplus);
+ return 0;
+}
diff --git a/tests/module/cache-cleanup-t.c b/tests/module/cache-cleanup-t.c
new file mode 100644
index 000000000000..8b5012fc3507
--- /dev/null
+++ b/tests/module/cache-cleanup-t.c
@@ -0,0 +1,104 @@
+/*
+ * Test for properly cleaning up ticket caches.
+ *
+ * Verify that the temporary Kerberos ticket cache generated during
+ * authentication is cleaned up on pam_end, even if no session was opened.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <dirent.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ DIR *tmpdir;
+ struct dirent *file;
+ char *tmppath, *path;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+
+ /* Generate a testing krb5.conf file. */
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Get the temporary directory and store that as the %1 substitution. */
+ tmppath = test_tmpdir();
+ config.extra[1] = tmppath;
+
+ plan_lazy();
+
+ /*
+ * We need to ensure that the only thing in the test temporary directory
+ * is the krb5.conf file that we generated and any valgrind logs, since
+ * we're going to check for cleanup by looking for any out-of-place files.
+ */
+ tmpdir = opendir(tmppath);
+ if (tmpdir == NULL)
+ sysbail("cannot open directory %s", tmppath);
+ while ((file = readdir(tmpdir)) != NULL) {
+ if (strcmp(file->d_name, ".") == 0 || strcmp(file->d_name, "..") == 0)
+ continue;
+ if (strcmp(file->d_name, "krb5.conf") == 0)
+ continue;
+ if (strcmp(file->d_name, "valgrind") == 0)
+ continue;
+ basprintf(&path, "%s/%s", tmppath, file->d_name);
+ if (unlink(path) < 0)
+ sysbail("cannot delete temporary file %s", path);
+ free(path);
+ }
+ closedir(tmpdir);
+
+ /*
+ * Authenticate only, call pam_end, and be sure the ticket cache is
+ * gone. The auth-only script sets ccache_dir to the temporary directory,
+ * so the module will create a temporary ticket cache there and then
+ * should clean it up.
+ */
+ run_script("data/scripts/cache-cleanup/auth-only", &config);
+ path = NULL;
+ tmpdir = opendir(tmppath);
+ if (tmpdir == NULL)
+ sysbail("cannot open directory %s", tmppath);
+ while ((file = readdir(tmpdir)) != NULL) {
+ if (strcmp(file->d_name, ".") == 0 || strcmp(file->d_name, "..") == 0)
+ continue;
+ if (strcmp(file->d_name, "krb5.conf") == 0)
+ continue;
+ if (strcmp(file->d_name, "valgrind") == 0)
+ continue;
+ if (path == NULL)
+ basprintf(&path, "%s/%s", tmppath, file->d_name);
+ }
+ closedir(tmpdir);
+ if (path != NULL)
+ diag("found stray temporary file %s", path);
+ ok(path == NULL, "ticket cache cleaned up");
+ if (path != NULL)
+ free(path);
+
+ test_tmpdir_free(tmppath);
+ return 0;
+}
diff --git a/tests/module/cache-t.c b/tests/module/cache-t.c
new file mode 100644
index 000000000000..8ec82df7c460
--- /dev/null
+++ b/tests/module/cache-t.c
@@ -0,0 +1,210 @@
+/*
+ * Authentication tests for the pam-krb5 module with ticket cache.
+ *
+ * This test case includes all tests that require Kerberos to be configured, a
+ * username and password available, and a ticket cache created, but with the
+ * PAM module running as the same user for which the ticket cache will be
+ * created (so without setuid and with chown doing nothing).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011, 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+/* Additional data used by the cache check callback. */
+struct extra {
+ char *realm;
+ char *cache_path;
+};
+
+
+/*
+ * PAM test callback to check whether we created a ticket cache and the ticket
+ * cache is for the correct user.
+ */
+static void
+check_cache(const char *file, const struct script_config *config,
+ const struct extra *extra)
+{
+ struct stat st;
+ krb5_error_code code;
+ krb5_context ctx = NULL;
+ krb5_ccache ccache = NULL;
+ krb5_principal princ = NULL;
+ krb5_principal tgtprinc = NULL;
+ krb5_creds in, out;
+ char *principal = NULL;
+
+ /* Check ownership and permissions. */
+ is_int(0, stat(file, &st), "cache exists");
+ is_int(getuid(), st.st_uid, "...with correct UID");
+ is_int(getgid(), st.st_gid, "...with correct GID");
+ is_int(0600, (st.st_mode & 0777), "...with correct permissions");
+
+ /* Check the existence of the ticket cache and its principal. */
+ code = krb5_init_context(&ctx);
+ if (code != 0)
+ bail("cannot create Kerberos context");
+ code = krb5_cc_resolve(ctx, file, &ccache);
+ is_int(0, code, "able to resolve Kerberos ticket cache");
+ code = krb5_cc_get_principal(ctx, ccache, &princ);
+ is_int(0, code, "able to get principal");
+ code = krb5_unparse_name(ctx, princ, &principal);
+ is_int(0, code, "...and principal is valid");
+ is_string(config->extra[0], principal, "...and matches our principal");
+
+ /* Retrieve the krbtgt for the realm and check properties. */
+ code = krb5_build_principal_ext(
+ ctx, &tgtprinc, (unsigned int) strlen(extra->realm), extra->realm,
+ KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME, strlen(extra->realm), extra->realm,
+ NULL);
+ if (code != 0)
+ bail("cannot create krbtgt principal name");
+ memset(&in, 0, sizeof(in));
+ memset(&out, 0, sizeof(out));
+ in.server = tgtprinc;
+ in.client = princ;
+ code = krb5_cc_retrieve_cred(ctx, ccache, KRB5_TC_MATCH_SRV_NAMEONLY, &in,
+ &out);
+ is_int(0, code, "able to get krbtgt credentials");
+ ok(out.times.endtime > time(NULL) + 30 * 60, "...good for 30 minutes");
+ krb5_free_cred_contents(ctx, &out);
+
+ /* Close things and release memory. */
+ krb5_free_principal(ctx, tgtprinc);
+ krb5_free_unparsed_name(ctx, principal);
+ krb5_free_principal(ctx, princ);
+ krb5_cc_close(ctx, ccache);
+ krb5_free_context(ctx);
+}
+
+
+/*
+ * Same as check_cache except unlink the ticket cache afterwards. Used to
+ * check the ticket cache in cases where the PAM module will not clean it up
+ * afterwards, such as calling pam_end with PAM_DATA_SILENT.
+ */
+static void
+check_cache_callback(pam_handle_t *pamh, const struct script_config *config,
+ void *data)
+{
+ struct extra *extra = data;
+ const char *cache, *file;
+ char *prefix;
+
+ cache = pam_getenv(pamh, "KRB5CCNAME");
+ ok(cache != NULL, "KRB5CCNAME is set in PAM environment");
+ if (cache == NULL)
+ return;
+ basprintf(&prefix, "FILE:/tmp/krb5cc_%lu_", (unsigned long) getuid());
+ diag("KRB5CCNAME = %s", cache);
+ ok(strncmp(prefix, cache, strlen(prefix)) == 0,
+ "cache file name prefix is correct");
+ free(prefix);
+ file = cache + strlen("FILE:");
+ extra->cache_path = bstrdup(file);
+ check_cache(file, config, extra);
+}
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ char *k5login;
+ struct extra extra;
+ struct passwd pwd;
+ FILE *file;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ extra.realm = krbconf->realm;
+ extra.cache_path = NULL;
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+
+ /* Generate a testing krb5.conf file. */
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Create a fake passwd struct for our user. */
+ memset(&pwd, 0, sizeof(pwd));
+ pwd.pw_name = krbconf->username;
+ pwd.pw_uid = getuid();
+ pwd.pw_gid = getgid();
+ basprintf(&pwd.pw_dir, "%s/tmp", getenv("BUILD"));
+ pam_set_pwd(&pwd);
+
+ plan_lazy();
+
+ /* Basic test. */
+ run_script("data/scripts/cache/basic", &config);
+
+ /* Check the cache status before the session is closed. */
+ config.callback = check_cache_callback;
+ config.data = &extra;
+ run_script("data/scripts/cache/open-session", &config);
+ free(extra.cache_path);
+ extra.cache_path = NULL;
+
+ /*
+ * Try again but passing PAM_DATA_SILENT to pam_end. This should leave
+ * the ticket cache intact.
+ */
+ run_script("data/scripts/cache/end-data-silent", &config);
+ check_cache(extra.cache_path, &config, &extra);
+ if (unlink(extra.cache_path) < 0)
+ sysdiag("unable to unlink temporary cache %s", extra.cache_path);
+ free(extra.cache_path);
+ extra.cache_path = NULL;
+
+ /* Change the authenticating user and test search_k5login. */
+ pwd.pw_name = (char *) "testuser";
+ pam_set_pwd(&pwd);
+ config.user = "testuser";
+ basprintf(&k5login, "%s/.k5login", pwd.pw_dir);
+ file = fopen(k5login, "w");
+ if (file == NULL)
+ sysbail("cannot create %s", k5login);
+ if (fprintf(file, "%s\n", krbconf->userprinc) < 0)
+ sysbail("cannot write to %s", k5login);
+ if (fclose(file) < 0)
+ sysbail("cannot flush %s", k5login);
+ run_script("data/scripts/cache/search-k5login", &config);
+ free(extra.cache_path);
+ extra.cache_path = NULL;
+ config.callback = NULL;
+ run_script("data/scripts/cache/search-k5login-debug", &config);
+ unlink(k5login);
+ free(k5login);
+
+ /* Test search_k5login when no .k5login file exists. */
+ pwd.pw_name = krbconf->username;
+ pam_set_pwd(&pwd);
+ config.user = krbconf->username;
+ diag("testing search_k5login with no .k5login file");
+ run_script("data/scripts/cache/search-k5login", &config);
+
+ free(pwd.pw_dir);
+ return 0;
+}
diff --git a/tests/module/expired-t.c b/tests/module/expired-t.c
new file mode 100644
index 000000000000..01a1892a0d04
--- /dev/null
+++ b/tests/module/expired-t.c
@@ -0,0 +1,175 @@
+/*
+ * Tests for the pam-krb5 module with an expired password.
+ *
+ * This test case checks correct handling of an account whose password has
+ * expired and the multiple different paths the module can take for handling
+ * that case.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+#include <time.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kadmin.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ char *newpass, *date;
+ struct passwd pwd;
+ time_t now;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.password = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+
+ /*
+ * Ensure we can expire the password. Heimdal has a prompt for the
+ * expiration time, so save that to use as a substitution in the script.
+ */
+ now = time(NULL) - 1;
+ if (!kerberos_expire_password(krbconf->userprinc, now))
+ skip_all("kadmin not configured or kadmin mismatch");
+ date = bstrdup(ctime(&now));
+ date[strlen(date) - 1] = '\0';
+ config.extra[1] = date;
+
+ /* Generate a testing krb5.conf file. */
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Create a fake passwd struct for our user. */
+ memset(&pwd, 0, sizeof(pwd));
+ pwd.pw_name = krbconf->username;
+ pwd.pw_uid = getuid();
+ pwd.pw_gid = getgid();
+ basprintf(&pwd.pw_dir, "%s/tmp", getenv("BUILD"));
+ pam_set_pwd(&pwd);
+
+ /*
+ * We'll be changing the password to something new. This needs to be
+ * sufficiently random that it's unlikely to fall afoul of password
+ * strength checking.
+ */
+ basprintf(&newpass, "ngh1,a%lu nn9af6", (unsigned long) getpid());
+ config.newpass = newpass;
+
+ plan_lazy();
+
+ /*
+ * Default behavior. We have to distinguish between two versions of
+ * Heimdal for testing because the prompts changed substantially. Use the
+ * existence of krb5_principal_set_comp_string to distinguish because it
+ * was introduced at the same time.
+ */
+#ifdef HAVE_KRB5_HEIMDAL
+# ifdef HAVE_KRB5_PRINCIPAL_SET_COMP_STRING
+ run_script("data/scripts/expired/basic-heimdal", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-heimdal-debug", &config);
+# else
+ run_script("data/scripts/expired/basic-heimdal-old", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-heimdal-old-debug", &config);
+# endif
+#else
+ run_script("data/scripts/expired/basic-mit", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-mit-debug", &config);
+#endif
+
+ /* Test again with PAM_SILENT, specified two ways. */
+#ifdef HAVE_KRB5_HEIMDAL
+ config.newpass = newpass;
+ config.password = krbconf->password;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-heimdal-silent", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-heimdal-flag-silent", &config);
+#else
+ config.newpass = newpass;
+ config.password = krbconf->password;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-mit-silent", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-mit-flag-silent", &config);
+#endif
+
+ /*
+ * We can only run the remaining checks if we can suppress the Kerberos
+ * library behavior of prompting for a new password when the password has
+ * expired.
+ */
+#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT
+
+ /* Check the forced failure behavior. */
+ run_script("data/scripts/expired/fail", &config);
+ run_script("data/scripts/expired/fail-debug", &config);
+
+ /*
+ * Defer the error to the account management check.
+ *
+ * Skip this check on Heimdal currently (Heimdal 7.4.0) because its
+ * implementation of krb5_get_init_creds_opt_set_change_password_prompt is
+ * incomplete. See <https://github.com/heimdal/heimdal/issues/322>.
+ */
+# ifdef HAVE_KRB5_HEIMDAL
+ skip_block(2, "deferring password changes broken in Heimdal");
+# else
+ config.newpass = newpass;
+ config.password = krbconf->password;
+ config.authtok = krbconf->password;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/defer-mit", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ config.authtok = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/defer-mit-debug", &config);
+# endif
+
+#else /* !HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT */
+
+ /* Mention that we skipped something for the record. */
+ skip_block(4, "cannot disable library password prompting");
+
+#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT */
+
+ /* In case we ran into some error, try to unexpire the password. */
+ kerberos_expire_password(krbconf->userprinc, 0);
+
+ free(date);
+ free(newpass);
+ free(pwd.pw_dir);
+ return 0;
+}
diff --git a/tests/module/fast-anon-t.c b/tests/module/fast-anon-t.c
new file mode 100644
index 000000000000..6355a5154f69
--- /dev/null
+++ b/tests/module/fast-anon-t.c
@@ -0,0 +1,108 @@
+/*
+ * Tests for anonymous FAST support in pam-krb5.
+ *
+ * Tests for anonymous Flexible Authentication Secure Tunneling, a mechanism
+ * for improving the preauthentication part of the Kerberos protocol and
+ * protecting it against various attacks.
+ *
+ * This is broken out from the other FAST tests because it uses PKINIT, and
+ * PKINIT code cannot be tested under valgrind with MIT Kerberos due to some
+ * bug in valgrind.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+
+
+/*
+ * Test whether anonymous authentication works. If this doesn't, we need to
+ * skip the tests of anonymous FAST.
+ */
+static bool
+anon_fast_works(void)
+{
+ krb5_context ctx;
+ krb5_error_code retval;
+ krb5_principal princ = NULL;
+ char *realm;
+ krb5_creds creds;
+ krb5_get_init_creds_opt *opts = NULL;
+
+ /* Construct the anonymous principal name. */
+ retval = krb5_init_context(&ctx);
+ if (retval != 0)
+ bail("cannot initialize Kerberos");
+ retval = krb5_get_default_realm(ctx, &realm);
+ if (retval != 0)
+ bail("cannot get default realm");
+ retval = krb5_build_principal_ext(
+ ctx, &princ, (unsigned int) strlen(realm), realm,
+ strlen(KRB5_WELLKNOWN_NAME), KRB5_WELLKNOWN_NAME,
+ strlen(KRB5_ANON_NAME), KRB5_ANON_NAME, NULL);
+ if (retval != 0)
+ bail("cannot construct anonymous principal");
+ krb5_free_default_realm(ctx, realm);
+
+ /* Obtain the credentials. */
+ memset(&creds, 0, sizeof(creds));
+ retval = krb5_get_init_creds_opt_alloc(ctx, &opts);
+ if (retval != 0)
+ bail("cannot create credential options");
+ krb5_get_init_creds_opt_set_anonymous(opts, 1);
+ krb5_get_init_creds_opt_set_tkt_life(opts, 60);
+ retval = krb5_get_init_creds_password(ctx, &creds, princ, NULL, NULL, NULL,
+ 0, NULL, opts);
+
+ /* Clean up. */
+ if (princ != NULL)
+ krb5_free_principal(ctx, princ);
+ if (opts != NULL)
+ krb5_get_init_creds_opt_free(ctx, opts);
+ krb5_free_cred_contents(ctx, &creds);
+
+ /* Return whether authentication succeeded. */
+ return (retval == 0);
+}
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Skip the test if FAST is not available. */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME
+ skip_all("FAST support not available");
+#endif
+
+ /* Initialize Kerberos configuration. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Skip the test if anonymous PKINIT doesn't work. */
+ if (!anon_fast_works())
+ skip_all("anonymous PKINIT failed");
+
+ /* Test anonymous FAST. */
+ plan_lazy();
+ run_script("data/scripts/fast/anonymous", &config);
+ run_script("data/scripts/fast/anonymous-debug", &config);
+
+ return 0;
+}
diff --git a/tests/module/fast-t.c b/tests/module/fast-t.c
new file mode 100644
index 000000000000..51fee27098c8
--- /dev/null
+++ b/tests/module/fast-t.c
@@ -0,0 +1,57 @@
+/*
+ * Tests for authenticated FAST support in pam-krb5.
+ *
+ * Tests for Flexible Authentication Secure Tunneling, a mechanism for
+ * improving the preauthentication part of the Kerberos protocol and
+ * protecting it against various attacks. This tests authenticated FAST;
+ * anonymous FAST is tested separately.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Skip the test if FAST is not available. */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME
+ skip_all("FAST support not available");
+#endif
+
+ /* Initialize Kerberos configuration. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_BOTH);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->userprinc;
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->cache;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ /* Test fast_ccache */
+ plan_lazy();
+ run_script("data/scripts/fast/ccache", &config);
+ run_script("data/scripts/fast/ccache-debug", &config);
+ run_script("data/scripts/fast/no-ccache", &config);
+ run_script("data/scripts/fast/no-ccache-debug", &config);
+
+ return 0;
+}
diff --git a/tests/module/long-t.c b/tests/module/long-t.c
new file mode 100644
index 000000000000..73614b0f6ec9
--- /dev/null
+++ b/tests/module/long-t.c
@@ -0,0 +1,46 @@
+/*
+ * Excessively long password tests for the pam-krb5 module.
+ *
+ * This test case includes all tests for excessively long passwords that can
+ * be done without having Kerberos configured and a username and password
+ * available.
+ *
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ char *password;
+
+ plan_lazy();
+
+ memset(&config, 0, sizeof(config));
+ config.user = "test";
+
+ /* Test a password that is too long. */
+ password = bcalloc_type(PAM_MAX_RESP_SIZE + 1, char);
+ memset(password, 'a', PAM_MAX_RESP_SIZE);
+ config.password = password;
+ run_script("data/scripts/long/password", &config);
+ run_script("data/scripts/long/password-debug", &config);
+
+ /* Test a stored authtok that's too long. */
+ config.authtok = password;
+ config.password = "testing";
+ run_script("data/scripts/long/use-first", &config);
+ run_script("data/scripts/long/use-first-debug", &config);
+
+ free(password);
+ return 0;
+}
diff --git a/tests/module/no-cache-t.c b/tests/module/no-cache-t.c
new file mode 100644
index 000000000000..8b282d1de397
--- /dev/null
+++ b/tests/module/no-cache-t.c
@@ -0,0 +1,47 @@
+/*
+ * Authentication tests for the pam-krb5 module without a ticket cache.
+ *
+ * This test case includes tests that require Kerberos to be configured and a
+ * username and password available, but which don't write a ticket cache
+ * (which requires additional work to test the cache ownership). This test
+ * does not set AUTHTOK.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011, 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->userprinc;
+ config.password = krbconf->password;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ plan_lazy();
+ run_script_dir("data/scripts/no-cache", &config);
+
+ return 0;
+}
diff --git a/tests/module/pam-user-t.c b/tests/module/pam-user-t.c
new file mode 100644
index 000000000000..72cc21eebae3
--- /dev/null
+++ b/tests/module/pam-user-t.c
@@ -0,0 +1,80 @@
+/*
+ * Tests for PAM_USER handling.
+ *
+ * This test case includes tests that require Kerberos to be configured and a
+ * username and password available, but which don't write a ticket cache
+ * (which requires additional work to test the cache ownership).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/macros.h>
+
+
+/*
+ * Callback to check that PAM_USER matches the desired value, passed in as the
+ * data parameter.
+ */
+static void
+check_pam_user(pam_handle_t *pamh, const struct script_config *config UNUSED,
+ void *data)
+{
+ int retval;
+ const char *name = NULL;
+ const char *expected = data;
+
+ retval = pam_get_item(pamh, PAM_USER, (PAM_CONST void **) &name);
+ is_int(PAM_SUCCESS, retval, "Found PAM_USER");
+ is_string(expected, name, "...matching %s", expected);
+}
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.password = krbconf->password;
+ config.callback = check_pam_user;
+ config.extra[0] = krbconf->username;
+ config.extra[1] = krbconf->userprinc;
+
+ /*
+ * Generate a testing krb5.conf file matching the realm of the Kerberos
+ * configuration so that canonicalization will work.
+ */
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Declare our plan. */
+ plan_lazy();
+
+ /* Authentication without a realm. No canonicalization. */
+ config.user = krbconf->username;
+ config.data = krbconf->username;
+ run_script("data/scripts/pam-user/update", &config);
+
+ /* Authentication with the local realm. Should be canonicalized. */
+ config.user = krbconf->userprinc;
+ run_script("data/scripts/pam-user/update", &config);
+
+ /*
+ * Now, test again with user updates disabled. The PAM_USER value should
+ * now not be canonicalized.
+ */
+ config.data = krbconf->userprinc;
+ run_script("data/scripts/pam-user/no-update", &config);
+
+ return 0;
+}
diff --git a/tests/module/password-t.c b/tests/module/password-t.c
new file mode 100644
index 000000000000..bdf9762bc6cb
--- /dev/null
+++ b/tests/module/password-t.c
@@ -0,0 +1,152 @@
+/*
+ * Authentication tests for the pam-krb5 module with ticket cache.
+ *
+ * This test case includes all tests that require Kerberos to be configured, a
+ * username and password available, and a ticket cache created, but with the
+ * PAM module running as the same user for which the ticket cache will be
+ * created (so without setuid and with chown doing nothing).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/macros.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+static void
+check_authtok(pam_handle_t *pamh, const struct script_config *config,
+ void *data UNUSED)
+{
+ int retval;
+ const char *authtok;
+
+ retval = pam_get_item(pamh, PAM_AUTHTOK, (PAM_CONST void **) &authtok);
+ is_int(PAM_SUCCESS, retval, "Found PAM_AUTHTOK");
+ is_string(config->newpass, authtok, "...and it is correct");
+}
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ char *newpass;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.password = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+
+ /* Generate a testing krb5.conf file. */
+ kerberos_generate_conf(krbconf->realm);
+
+ plan_lazy();
+
+ /*
+ * First test trying to change the password to something that's
+ * excessively long.
+ */
+ newpass = bcalloc_type(PAM_MAX_RESP_SIZE + 1, char);
+ memset(newpass, 'a', PAM_MAX_RESP_SIZE);
+ config.newpass = newpass;
+ run_script("data/scripts/password/too-long", &config);
+ run_script("data/scripts/password/too-long-debug", &config);
+
+ /* Test use_authtok with an excessively long password. */
+ config.newpass = NULL;
+ config.authtok = newpass;
+ run_script("data/scripts/password/authtok-too-long", &config);
+ run_script("data/scripts/password/authtok-too-long-debug", &config);
+
+ /*
+ * Change the password to something new. This needs to be sufficiently
+ * random that it's unlikely to fall afoul of password strength checking.
+ */
+ free(newpass);
+ config.authtok = NULL;
+ basprintf(&newpass, "ngh1,a%lu nn9af6%lu", (unsigned long) getpid(),
+ (unsigned long) time(NULL));
+ config.newpass = newpass;
+ run_script("data/scripts/password/basic", &config);
+ config.password = newpass;
+ config.newpass = krbconf->password;
+ run_script("data/scripts/password/basic-debug", &config);
+
+ /* Test prompt_principal with password change. */
+ config.password = krbconf->password;
+ config.newpass = newpass;
+ run_script("data/scripts/password/prompt-principal", &config);
+
+ /* Change the password back and test expose-account. */
+ config.password = newpass;
+ config.newpass = krbconf->password;
+ run_script("data/scripts/password/expose", &config);
+
+ /*
+ * Test two banner settings by changing the password and then changing it
+ * back again.
+ */
+ config.password = krbconf->password;
+ config.newpass = newpass;
+ run_script("data/scripts/password/banner", &config);
+ config.password = newpass;
+ config.newpass = krbconf->password;
+ run_script("data/scripts/password/no-banner", &config);
+
+ /* Do the same, but with expose_account set as well. */
+ config.password = krbconf->password;
+ config.newpass = newpass;
+ run_script("data/scripts/password/banner-expose", &config);
+ config.password = newpass;
+ config.newpass = krbconf->password;
+ run_script("data/scripts/password/no-banner-expose", &config);
+
+ /* Test use_authtok. */
+ config.password = krbconf->password;
+ config.newpass = NULL;
+ config.authtok = newpass;
+ run_script("data/scripts/password/authtok", &config);
+
+ /* Test use_authtok with force_first_pass. */
+ config.password = NULL;
+ config.authtok = krbconf->password;
+ config.oldauthtok = newpass;
+ run_script("data/scripts/password/authtok-force", &config);
+
+ /*
+ * Ensure PAM_AUTHTOK and PAM_OLDAUTHTOK are set even if the user is
+ * ignored.
+ */
+ config.user = "root";
+ config.authtok = NULL;
+ config.oldauthtok = NULL;
+ config.password = "old-password";
+ config.newpass = "new-password";
+ config.callback = check_authtok;
+ run_script("data/scripts/password/ignore", &config);
+
+ free(newpass);
+ return 0;
+}
diff --git a/tests/module/pkinit-t.c b/tests/module/pkinit-t.c
new file mode 100644
index 000000000000..6bbb6993b2af
--- /dev/null
+++ b/tests/module/pkinit-t.c
@@ -0,0 +1,98 @@
+/*
+ * PKINIT authentication tests for the pam-krb5 module.
+ *
+ * This test case includes tests that require a PKINIT certificate, but which
+ * don't write a Kerberos ticket cache.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+#if defined(HAVE_KRB5_MIT) && defined(PATH_OPENSSL)
+ const char **generate_pkcs12;
+ char *tmpdir, *pkcs12_path;
+#endif
+
+ /* Load the Kerberos principal and certificate path. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PKINIT);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->pkinit_principal;
+ config.extra[0] = krbconf->pkinit_cert;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ /* Check things that are the same with both Kerberos implementations. */
+ plan_lazy();
+ run_script("data/scripts/pkinit/basic", &config);
+ run_script("data/scripts/pkinit/basic-debug", &config);
+ run_script("data/scripts/pkinit/prompt-use", &config);
+ run_script("data/scripts/pkinit/prompt-try", &config);
+ run_script("data/scripts/pkinit/try-pkinit", &config);
+
+ /* Debugging output is a little different between the implementations. */
+#ifdef HAVE_KRB5_HEIMDAL
+ run_script("data/scripts/pkinit/try-pkinit-debug", &config);
+#else
+ run_script("data/scripts/pkinit/try-pkinit-debug-mit", &config);
+#endif
+
+ /* Only MIT Kerberos supports setting preauth options. */
+#ifdef HAVE_KRB5_MIT
+ run_script("data/scripts/pkinit/preauth-opt-mit", &config);
+#endif
+
+ /*
+ * If OpenSSL is available, test prompting with MIT Kerberos since we have
+ * to implement the prompting for the use_pkinit case ourselves. To do
+ * this, convert the input PKINIT certificate to a PKCS12 file with a
+ * password.
+ */
+#if defined(HAVE_KRB5_MIT) && defined(PATH_OPENSSL)
+ tmpdir = test_tmpdir();
+ basprintf(&pkcs12_path, "%s/%s", tmpdir, "pkinit-pkcs12");
+ generate_pkcs12 = bcalloc_type(10, const char *);
+ generate_pkcs12[0] = PATH_OPENSSL;
+ generate_pkcs12[1] = "pkcs12";
+ generate_pkcs12[2] = "-export";
+ generate_pkcs12[3] = "-in";
+ generate_pkcs12[4] = krbconf->pkinit_cert;
+ generate_pkcs12[5] = "-password";
+ generate_pkcs12[6] = "pass:some-password";
+ generate_pkcs12[7] = "-out";
+ generate_pkcs12[8] = pkcs12_path;
+ generate_pkcs12[9] = NULL;
+ run_setup(generate_pkcs12);
+ free(generate_pkcs12);
+ config.extra[0] = pkcs12_path;
+ config.extra[1] = "some-password";
+ run_script("data/scripts/pkinit/pin-mit", &config);
+ unlink(pkcs12_path);
+ free(pkcs12_path);
+ test_tmpdir_free(tmpdir);
+#endif /* HAVE_KRB5_MIT && PATH_OPENSSL */
+
+ return 0;
+}
diff --git a/tests/module/realm-t.c b/tests/module/realm-t.c
new file mode 100644
index 000000000000..d5643ca1f3e5
--- /dev/null
+++ b/tests/module/realm-t.c
@@ -0,0 +1,87 @@
+/*
+ * Authentication tests for realm support in pam-krb5.
+ *
+ * Test the realm and user_realm option in the PAM configuration, which is
+ * special in several ways since it influences krb5.conf parsing and is read
+ * out of order in the initial configuration.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ struct passwd pwd;
+ FILE *file;
+ char *k5login;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.authtok = krbconf->password;
+
+ /* Don't keep track of the tests in each script. */
+ plan_lazy();
+
+ /* Start with a nonexistent default realm for authentication failure. */
+ kerberos_generate_conf("bogus.example.com");
+ config.extra[0] = "bogus.example.com";
+ run_script("data/scripts/realm/fail-no-realm", &config);
+ run_script("data/scripts/realm/fail-no-realm-debug", &config);
+
+ /* Running a script that sets realm properly should pass. */
+ config.extra[0] = krbconf->realm;
+ run_script("data/scripts/realm/pass-realm", &config);
+
+ /* Setting user_realm should continue to fail due to no .k5login file. */
+ run_script("data/scripts/realm/fail-user-realm", &config);
+
+ /* If we add a .k5login file for the user, user_realm should work. */
+ pwd.pw_name = krbconf->username;
+ pwd.pw_uid = getuid();
+ pwd.pw_gid = getgid();
+ pwd.pw_dir = test_tmpdir();
+ pam_set_pwd(&pwd);
+ basprintf(&k5login, "%s/.k5login", pwd.pw_dir);
+ file = fopen(k5login, "w");
+ if (file == NULL)
+ sysbail("cannot create %s", k5login);
+ if (fprintf(file, "%s\n", krbconf->userprinc) < 0)
+ sysbail("cannot write to %s", k5login);
+ if (fclose(file) < 0)
+ sysbail("cannot flush %s", k5login);
+ run_script("data/scripts/realm/pass-user-realm", &config);
+ pam_set_pwd(NULL);
+ unlink(k5login);
+ free(k5login);
+ test_tmpdir_free(pwd.pw_dir);
+
+ /* Switch to the correct realm, but set the wrong realm in PAM. */
+ kerberos_generate_conf(krbconf->realm);
+ config.extra[0] = "bogus.example.com";
+ run_script("data/scripts/realm/fail-realm", &config);
+ run_script("data/scripts/realm/fail-bad-user-realm", &config);
+
+ return 0;
+}
diff --git a/tests/module/stacked-t.c b/tests/module/stacked-t.c
new file mode 100644
index 000000000000..ef8e70885ecb
--- /dev/null
+++ b/tests/module/stacked-t.c
@@ -0,0 +1,50 @@
+/*
+ * Authentication tests for the pam-krb5 module with an existing AUTHTOK.
+ *
+ * This test case includes tests that require Kerberos to be configured and a
+ * username and password available and that run with AUTHTOK already set, but
+ * which don't write a ticket cache (which requires additional work to test
+ * the cache ownership).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->userprinc;
+ config.password = krbconf->password;
+ config.authtok = krbconf->password;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ plan_lazy();
+ run_script_dir("data/scripts/stacked", &config);
+
+ return 0;
+}
diff --git a/tests/module/trace-t.c b/tests/module/trace-t.c
new file mode 100644
index 000000000000..db3aa67f9e24
--- /dev/null
+++ b/tests/module/trace-t.c
@@ -0,0 +1,48 @@
+/*
+ * Tests for trace logging in the pam-krb5 module.
+ *
+ * Checks that trace logging is handled properly. This is currently very
+ * simple and just checks that the file is created.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ char *tmpdir, *trace;
+
+ plan_lazy();
+
+ memset(&config, 0, sizeof(config));
+ config.user = "testuser";
+ tmpdir = test_tmpdir();
+ basprintf(&trace, "%s/trace", tmpdir);
+ config.extra[0] = trace;
+#ifdef HAVE_KRB5_SET_TRACE_FILENAME
+ run_script("data/scripts/trace/supported", &config);
+ is_int(0, access(trace, F_OK), "Trace file was created");
+ unlink(trace);
+#else
+ run_script("data/scripts/trace/unsupported", &config);
+ is_int(-1, access(trace, F_OK), "Trace file does not exist");
+#endif
+
+ free(trace);
+ test_tmpdir_free(tmpdir);
+ return 0;
+}
diff --git a/tests/pam-util/args-t.c b/tests/pam-util/args-t.c
new file mode 100644
index 000000000000..4ec102e511ed
--- /dev/null
+++ b/tests/pam-util/args-t.c
@@ -0,0 +1,86 @@
+/*
+ * PAM utility argument initialization test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010, 2012-2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <pam-util/args.h>
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+
+
+int
+main(void)
+{
+ pam_handle_t *pamh;
+ struct pam_conv conv = {NULL, NULL};
+ struct pam_args *args;
+
+ plan(12);
+
+ if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS)
+ sysbail("Fake PAM initialization failed");
+ args = putil_args_new(pamh, 0);
+ ok(args != NULL, "New args struct is not NULL");
+ if (args == NULL)
+ ok_block(7, 0, "...args struct is NULL");
+ else {
+ ok(args->pamh == pamh, "...and pamh is correct");
+ ok(args->config == NULL, "...and config is NULL");
+ ok(args->user == NULL, "...and user is NULL");
+ is_int(args->debug, false, "...and debug is false");
+ is_int(args->silent, false, "...and silent is false");
+#ifdef HAVE_KRB5
+ ok(args->ctx != NULL, "...and the Kerberos context is initialized");
+ ok(args->realm == NULL, "...and realm is NULL");
+#else
+ skip_block(2, "Kerberos support not configured");
+#endif
+ }
+ putil_args_free(args);
+ ok(1, "Freeing the args struct works");
+
+ args = putil_args_new(pamh, PAM_SILENT);
+ ok(args != NULL, "New args struct with PAM_SILENT is not NULL");
+ if (args == NULL)
+ ok(0, "...args is NULL");
+ else
+ is_int(args->silent, true, "...and silent is true");
+ putil_args_free(args);
+
+ putil_args_free(NULL);
+ ok(1, "Freeing a NULL args struct works");
+
+ pam_end(pamh, 0);
+
+ return 0;
+}
diff --git a/tests/pam-util/fakepam-t.c b/tests/pam-util/fakepam-t.c
new file mode 100644
index 000000000000..1e09c5fdde75
--- /dev/null
+++ b/tests/pam-util/fakepam-t.c
@@ -0,0 +1,121 @@
+/*
+ * Fake PAM library test suite.
+ *
+ * This is not actually a test for the pam-util layer, but rather is a test
+ * for the trickier components of the fake PAM library that in turn is used to
+ * test the pam-util layer and PAM modules.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+
+
+int
+main(void)
+{
+ pam_handle_t *pamh;
+ struct pam_conv conv = {NULL, NULL};
+ char **env;
+ size_t i;
+
+ /*
+ * Skip this test if the native PAM library doesn't support a PAM
+ * environment, since we "break" pam_putenv to mirror the native behavior
+ * in that case.
+ */
+#ifndef HAVE_PAM_GETENV
+ skip_all("system doesn't support PAM environment");
+#endif
+
+ plan(33);
+
+ /* Basic environment manipulation. */
+ if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS)
+ sysbail("Fake PAM initialization failed");
+ is_int(PAM_BAD_ITEM, pam_putenv(pamh, "TEST"), "delete when NULL");
+ ok(pam_getenv(pamh, "TEST") == NULL, "getenv when NULL");
+ env = pam_getenvlist(pamh);
+ ok(env != NULL, "getenvlist when NULL returns non-NULL");
+ if (env == NULL)
+ bail("pam_getenvlist returned NULL");
+ is_string(NULL, env[0], "...but first element is NULL");
+ for (i = 0; env[i] != NULL; i++)
+ free(env[i]);
+ free(env);
+
+ /* putenv and getenv. */
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "TEST=foo"), "putenv TEST");
+ is_string("foo", pam_getenv(pamh, "TEST"), "getenv TEST");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO=bar"), "putenv FOO");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "BAR=baz"), "putenv BAR");
+ is_string("foo", pam_getenv(pamh, "TEST"), "getenv TEST");
+ is_string("bar", pam_getenv(pamh, "FOO"), "getenv FOO");
+ is_string("baz", pam_getenv(pamh, "BAR"), "getenv BAR");
+ ok(pam_getenv(pamh, "BAZ") == NULL, "getenv BAZ is NULL");
+
+ /* Replacing and deleting environment variables. */
+ is_int(PAM_BAD_ITEM, pam_putenv(pamh, "BAZ"), "putenv nonexistent delete");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO=foo"), "putenv replace");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOON=bar=n"), "putenv prefix");
+ is_string("foo", pam_getenv(pamh, "FOO"), "getenv FOO");
+ is_string("bar=n", pam_getenv(pamh, "FOON"), "getenv FOON");
+ is_int(PAM_BAD_ITEM, pam_putenv(pamh, "FO"), "putenv delete FO");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO"), "putenv delete FOO");
+ ok(pam_getenv(pamh, "FOO") == NULL, "getenv FOO is NULL");
+ is_string("bar=n", pam_getenv(pamh, "FOON"), "getenv FOON");
+ is_string("baz", pam_getenv(pamh, "BAR"), "getenv BAR");
+
+ /* pam_getenvlist. */
+ env = pam_getenvlist(pamh);
+ ok(env != NULL, "getenvlist not NULL");
+ if (env == NULL)
+ bail("pam_getenvlist returned NULL");
+ is_string("TEST=foo", env[0], "getenvlist TEST");
+ is_string("BAR=baz", env[1], "getenvlist BAR");
+ is_string("FOON=bar=n", env[2], "getenvlist FOON");
+ ok(env[3] == NULL, "getenvlist length");
+ for (i = 0; env[i] != NULL; i++)
+ free(env[i]);
+ free(env);
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO=foo"), "putenv FOO");
+ is_string("TEST=foo", pamh->environ[0], "pamh environ TEST");
+ is_string("BAR=baz", pamh->environ[1], "pamh environ BAR");
+ is_string("FOON=bar=n", pamh->environ[2], "pamh environ FOON");
+ is_string("FOO=foo", pamh->environ[3], "pamh environ FOO");
+ ok(pamh->environ[4] == NULL, "pamh environ length");
+
+ pam_end(pamh, 0);
+
+ return 0;
+}
diff --git a/tests/pam-util/logging-t.c b/tests/pam-util/logging-t.c
new file mode 100644
index 000000000000..84072bd6b91a
--- /dev/null
+++ b/tests/pam-util/logging-t.c
@@ -0,0 +1,146 @@
+/*
+ * PAM logging test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <syslog.h>
+
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+/* Test a normal PAM logging function. */
+#define TEST(func, p, n) \
+ do { \
+ (func)(args, "%s", "foo"); \
+ seen = pam_output(); \
+ is_int((p), seen->lines[0].priority, "priority %d", (p)); \
+ is_string("foo", seen->lines[0].line, "line %s", (n)); \
+ pam_output_free(seen); \
+ } while (0)
+
+/* Test a PAM error logging function. */
+#define TEST_PAM(func, c, p, n) \
+ do { \
+ (func)(args, (c), "%s", "bar"); \
+ if ((c) == PAM_SUCCESS) \
+ expected = strdup("bar"); \
+ else \
+ basprintf(&expected, "%s: %s", "bar", \
+ pam_strerror(args->pamh, c)); \
+ seen = pam_output(); \
+ is_int((p), seen->lines[0].priority, "priority %s", (n)); \
+ is_string(expected, seen->lines[0].line, "line %s", (n)); \
+ pam_output_free(seen); \
+ free(expected); \
+ } while (0)
+
+/* Test a PAM Kerberos error logging function .*/
+#define TEST_KRB5(func, p, n) \
+ do { \
+ const char *msg; \
+ \
+ code = krb5_parse_name(args->ctx, "foo@bar@EXAMPLE.COM", &princ); \
+ (func)(args, code, "%s", "krb"); \
+ code = krb5_parse_name(args->ctx, "foo@bar@EXAMPLE.COM", &princ); \
+ msg = krb5_get_error_message(args->ctx, code); \
+ basprintf(&expected, "%s: %s", "krb", msg); \
+ seen = pam_output(); \
+ is_int((p), seen->lines[0].priority, "priority %s", (n)); \
+ is_string(expected, seen->lines[0].line, "line %s", (n)); \
+ pam_output_free(seen); \
+ free(expected); \
+ krb5_free_error_message(args->ctx, msg); \
+ } while (0)
+
+
+int
+main(void)
+{
+ pam_handle_t *pamh;
+ struct pam_args *args;
+ struct pam_conv conv = {NULL, NULL};
+ char *expected;
+ struct output *seen;
+#ifdef HAVE_KRB5
+ krb5_error_code code;
+ krb5_principal princ;
+#endif
+
+ plan(27);
+
+ if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS)
+ sysbail("Fake PAM initialization failed");
+ args = putil_args_new(pamh, 0);
+ if (args == NULL)
+ bail("cannot create PAM argument struct");
+ TEST(putil_crit, LOG_CRIT, "putil_crit");
+ TEST(putil_err, LOG_ERR, "putil_err");
+ putil_debug(args, "%s", "foo");
+ ok(pam_output() == NULL, "putil_debug without debug on");
+ args->debug = true;
+ TEST(putil_debug, LOG_DEBUG, "putil_debug");
+ args->debug = false;
+
+ TEST_PAM(putil_crit_pam, PAM_SYSTEM_ERR, LOG_CRIT, "putil_crit_pam S");
+ TEST_PAM(putil_crit_pam, PAM_BUF_ERR, LOG_CRIT, "putil_crit_pam B");
+ TEST_PAM(putil_crit_pam, PAM_SUCCESS, LOG_CRIT, "putil_crit_pam ok");
+ TEST_PAM(putil_err_pam, PAM_SYSTEM_ERR, LOG_ERR, "putil_err_pam");
+ putil_debug_pam(args, PAM_SYSTEM_ERR, "%s", "bar");
+ ok(pam_output() == NULL, "putil_debug_pam without debug on");
+ args->debug = true;
+ TEST_PAM(putil_debug_pam, PAM_SYSTEM_ERR, LOG_DEBUG, "putil_debug_pam");
+ TEST_PAM(putil_debug_pam, PAM_SUCCESS, LOG_DEBUG, "putil_debug_pam ok");
+ args->debug = false;
+
+#ifdef HAVE_KRB5
+ TEST_KRB5(putil_crit_krb5, LOG_CRIT, "putil_crit_krb5");
+ TEST_KRB5(putil_err_krb5, LOG_ERR, "putil_err_krb5");
+ code = krb5_parse_name(args->ctx, "foo@bar@EXAMPLE.COM", &princ);
+ putil_debug_krb5(args, code, "%s", "krb");
+ ok(pam_output() == NULL, "putil_debug_krb5 without debug on");
+ args->debug = true;
+ TEST_KRB5(putil_debug_krb5, LOG_DEBUG, "putil_debug_krb5");
+ args->debug = false;
+#else
+ skip_block(4, "not built with Kerberos support");
+#endif
+
+ putil_args_free(args);
+ pam_end(pamh, 0);
+
+ return 0;
+}
diff --git a/tests/pam-util/options-t.c b/tests/pam-util/options-t.c
new file mode 100644
index 000000000000..f8b76730fb2d
--- /dev/null
+++ b/tests/pam-util/options-t.c
@@ -0,0 +1,458 @@
+/*
+ * PAM option parsing test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <syslog.h>
+
+#include <pam-util/args.h>
+#include <pam-util/options.h>
+#include <pam-util/vector.h>
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+/* The configuration struct we will use for testing. */
+struct pam_config {
+ struct vector *cells;
+ bool debug;
+#ifdef HAVE_KRB5
+ krb5_deltat expires;
+#else
+ long expires;
+#endif
+ bool ignore_root;
+ long minimum_uid;
+ char *program;
+};
+
+#define K(name) (#name), offsetof(struct pam_config, name)
+
+/* The rules specifying the configuration options. */
+static struct option options[] = {
+ /* clang-format off */
+ { K(cells), true, LIST (NULL) },
+ { K(debug), true, BOOL (false) },
+ { K(expires), true, TIME (10) },
+ { K(ignore_root), false, BOOL (true) },
+ { K(minimum_uid), true, NUMBER (0) },
+ { K(program), true, STRING (NULL) },
+ /* clang-format on */
+};
+static const size_t optlen = sizeof(options) / sizeof(options[0]);
+
+/*
+ * A macro used to parse the various ways of spelling booleans. This reuses
+ * the argv_bool variable, setting it to the first value provided and then
+ * calling putil_args_parse() on it. It then checks whether the provided
+ * config option is set to the expected value.
+ */
+#define TEST_BOOL(a, c, v) \
+ do { \
+ argv_bool[0] = (a); \
+ status = putil_args_parse(args, 1, argv_bool, options, optlen); \
+ ok(status, "Parse of %s", (a)); \
+ is_int((v), (c), "...and value is correct"); \
+ ok(pam_output() == NULL, "...and no output"); \
+ } while (0)
+
+/*
+ * A macro used to test error reporting from putil_args_parse(). This reuses
+ * the argv_err variable, setting it to the first value provided and then
+ * calling putil_args_parse() on it. It then recovers the error message and
+ * expects it to match the severity and error message given.
+ */
+#define TEST_ERROR(a, p, e) \
+ do { \
+ argv_err[0] = (a); \
+ status = putil_args_parse(args, 1, argv_err, options, optlen); \
+ ok(status, "Parse of %s", (a)); \
+ seen = pam_output(); \
+ if (seen == NULL) \
+ ok_block(2, false, "...no error output"); \
+ else { \
+ is_int((p), seen->lines[0].priority, "...priority for %s", (a)); \
+ is_string((e), seen->lines[0].line, "...error for %s", (a)); \
+ } \
+ pam_output_free(seen); \
+ } while (0)
+
+
+/*
+ * Allocate and initialize a new struct config.
+ */
+static struct pam_config *
+config_new(void)
+{
+ return bcalloc(1, sizeof(struct pam_config));
+}
+
+
+/*
+ * Free a struct config and all of its members.
+ */
+static void
+config_free(struct pam_config *config)
+{
+ if (config == NULL)
+ return;
+ vector_free(config->cells);
+ free(config->program);
+ free(config);
+}
+
+
+int
+main(void)
+{
+ pam_handle_t *pamh;
+ struct pam_args *args;
+ struct pam_conv conv = {NULL, NULL};
+ bool status;
+ struct vector *cells;
+ char *program;
+ struct output *seen;
+ const char *argv_bool[2] = {NULL, NULL};
+ const char *argv_err[2] = {NULL, NULL};
+ const char *argv_empty[] = {NULL};
+#ifdef HAVE_KRB5
+ const char *argv_all[] = {"cells=stanford.edu,ir.stanford.edu",
+ "debug",
+ "expires=1d",
+ "ignore_root",
+ "minimum_uid=1000",
+ "program=/bin/true"};
+ char *krb5conf;
+#else
+ const char *argv_all[] = {"cells=stanford.edu,ir.stanford.edu",
+ "debug",
+ "expires=86400",
+ "ignore_root",
+ "minimum_uid=1000",
+ "program=/bin/true"};
+#endif
+
+ if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS)
+ sysbail("cannot create pam_handle_t");
+ args = putil_args_new(pamh, 0);
+ if (args == NULL)
+ bail("cannot create PAM argument struct");
+
+ plan(161);
+
+ /* First, check just the defaults. */
+ args->config = config_new();
+ status = putil_args_defaults(args, options, optlen);
+ ok(status, "Setting the defaults");
+ ok(args->config->cells == NULL, "...cells default");
+ is_int(false, args->config->debug, "...debug default");
+ is_int(10, args->config->expires, "...expires default");
+ is_int(true, args->config->ignore_root, "...ignore_root default");
+ is_int(0, args->config->minimum_uid, "...minimum_uid default");
+ ok(args->config->program == NULL, "...program default");
+
+ /* Now parse an empty set of PAM arguments. Nothing should change. */
+ status = putil_args_parse(args, 0, argv_empty, options, optlen);
+ ok(status, "Parse of empty argv");
+ ok(args->config->cells == NULL, "...cells still default");
+ is_int(false, args->config->debug, "...debug still default");
+ is_int(10, args->config->expires, "...expires default");
+ is_int(true, args->config->ignore_root, "...ignore_root still default");
+ is_int(0, args->config->minimum_uid, "...minimum_uid still default");
+ ok(args->config->program == NULL, "...program still default");
+
+ /* Now, check setting everything. */
+ status = putil_args_parse(args, 6, argv_all, options, optlen);
+ ok(status, "Parse of full argv");
+ if (args->config->cells == NULL)
+ ok_block(4, false, "...cells is set");
+ else {
+ ok(args->config->cells != NULL, "...cells is set");
+ is_int(2, args->config->cells->count, "...with two cells");
+ is_string("stanford.edu", args->config->cells->strings[0],
+ "...first is stanford.edu");
+ is_string("ir.stanford.edu", args->config->cells->strings[1],
+ "...second is ir.stanford.edu");
+ }
+ is_int(true, args->config->debug, "...debug is set");
+ is_int(86400, args->config->expires, "...expires is set");
+ is_int(true, args->config->ignore_root, "...ignore_root is set");
+ is_int(1000, args->config->minimum_uid, "...minimum_uid is set");
+ is_string("/bin/true", args->config->program, "...program is set");
+ config_free(args->config);
+ args->config = NULL;
+
+ /* Test deep copying of defaults. */
+ cells = vector_new();
+ if (cells == NULL)
+ sysbail("cannot allocate memory");
+ vector_add(cells, "foo.com");
+ vector_add(cells, "bar.com");
+ options[0].defaults.list = cells;
+ program = strdup("/bin/false");
+ if (program == NULL)
+ sysbail("cannot allocate memory");
+ options[5].defaults.string = program;
+ args->config = config_new();
+ status = putil_args_defaults(args, options, optlen);
+ ok(status, "Setting defaults with new defaults");
+ if (args->config->cells == NULL)
+ ok_block(4, false, "...cells is set");
+ else {
+ ok(args->config->cells != NULL, "...cells is set");
+ is_int(2, args->config->cells->count, "...with two cells");
+ is_string("foo.com", args->config->cells->strings[0],
+ "...first is foo.com");
+ is_string("bar.com", args->config->cells->strings[1],
+ "...second is bar.com");
+ }
+ is_string("/bin/false", args->config->program, "...program is /bin/false");
+ status = putil_args_parse(args, 6, argv_all, options, optlen);
+ ok(status, "Parse of full argv after defaults");
+ if (args->config->cells == NULL)
+ ok_block(4, false, "...cells is set");
+ else {
+ ok(args->config->cells != NULL, "...cells is set");
+ is_int(2, args->config->cells->count, "...with two cells");
+ is_string("stanford.edu", args->config->cells->strings[0],
+ "...first is stanford.edu");
+ is_string("ir.stanford.edu", args->config->cells->strings[1],
+ "...second is ir.stanford.edu");
+ }
+ is_int(true, args->config->debug, "...debug is set");
+ is_int(86400, args->config->expires, "...expires is set");
+ is_int(true, args->config->ignore_root, "...ignore_root is set");
+ is_int(1000, args->config->minimum_uid, "...minimum_uid is set");
+ is_string("/bin/true", args->config->program, "...program is set");
+ is_string("foo.com", cells->strings[0], "...first cell after parse");
+ is_string("bar.com", cells->strings[1], "...second cell after parse");
+ is_string("/bin/false", program, "...string after parse");
+ config_free(args->config);
+ args->config = NULL;
+ is_string("foo.com", cells->strings[0], "...first cell after free");
+ is_string("bar.com", cells->strings[1], "...second cell after free");
+ is_string("/bin/false", program, "...string after free");
+ options[0].defaults.list = NULL;
+ options[5].defaults.string = NULL;
+ vector_free(cells);
+ free(program);
+
+ /* Test specifying the default for a vector parameter as a string. */
+ options[0].type = TYPE_STRLIST;
+ options[0].defaults.string = "foo.com,bar.com";
+ args->config = config_new();
+ status = putil_args_defaults(args, options, optlen);
+ ok(status, "Setting defaults with string default for vector");
+ if (args->config->cells == NULL)
+ ok_block(4, false, "...cells is set");
+ else {
+ ok(args->config->cells != NULL, "...cells is set");
+ is_int(2, args->config->cells->count, "...with two cells");
+ is_string("foo.com", args->config->cells->strings[0],
+ "...first is foo.com");
+ is_string("bar.com", args->config->cells->strings[1],
+ "...second is bar.com");
+ }
+ config_free(args->config);
+ args->config = NULL;
+ options[0].type = TYPE_LIST;
+ options[0].defaults.string = NULL;
+
+ /* Should be no errors so far. */
+ ok(pam_output() == NULL, "No errors so far");
+
+ /* Test various ways of spelling booleans. */
+ args->config = config_new();
+ TEST_BOOL("debug", args->config->debug, true);
+ TEST_BOOL("debug=false", args->config->debug, false);
+ TEST_BOOL("debug=true", args->config->debug, true);
+ TEST_BOOL("debug=no", args->config->debug, false);
+ TEST_BOOL("debug=yes", args->config->debug, true);
+ TEST_BOOL("debug=off", args->config->debug, false);
+ TEST_BOOL("debug=on", args->config->debug, true);
+ TEST_BOOL("debug=0", args->config->debug, false);
+ TEST_BOOL("debug=1", args->config->debug, true);
+ TEST_BOOL("debug=False", args->config->debug, false);
+ TEST_BOOL("debug=trUe", args->config->debug, true);
+ TEST_BOOL("debug=No", args->config->debug, false);
+ TEST_BOOL("debug=Yes", args->config->debug, true);
+ TEST_BOOL("debug=OFF", args->config->debug, false);
+ TEST_BOOL("debug=ON", args->config->debug, true);
+ config_free(args->config);
+ args->config = NULL;
+
+ /* Test for various parsing errors. */
+ args->config = config_new();
+ TEST_ERROR("debug=", LOG_ERR, "invalid boolean in setting: debug=");
+ TEST_ERROR("debug=truth", LOG_ERR,
+ "invalid boolean in setting: debug=truth");
+ TEST_ERROR("minimum_uid", LOG_ERR, "value missing for option minimum_uid");
+ TEST_ERROR("minimum_uid=", LOG_ERR,
+ "value missing for option minimum_uid=");
+ TEST_ERROR("minimum_uid=foo", LOG_ERR,
+ "invalid number in setting: minimum_uid=foo");
+ TEST_ERROR("minimum_uid=1000foo", LOG_ERR,
+ "invalid number in setting: minimum_uid=1000foo");
+ TEST_ERROR("program", LOG_ERR, "value missing for option program");
+ TEST_ERROR("cells", LOG_ERR, "value missing for option cells");
+ config_free(args->config);
+ args->config = NULL;
+
+#ifdef HAVE_KRB5
+
+ /* Test for Kerberos krb5.conf option parsing. */
+ krb5conf = test_file_path("data/krb5-pam.conf");
+ if (krb5conf == NULL)
+ bail("cannot find data/krb5-pam.conf");
+ if (setenv("KRB5_CONFIG", krb5conf, 1) < 0)
+ sysbail("cannot set KRB5_CONFIG");
+ krb5_free_context(args->ctx);
+ status = krb5_init_context(&args->ctx);
+ if (status != 0)
+ bail("cannot parse test krb5.conf file");
+ args->config = config_new();
+ status = putil_args_defaults(args, options, optlen);
+ ok(status, "Setting the defaults");
+ status = putil_args_krb5(args, "testing", options, optlen);
+ ok(status, "Options from krb5.conf");
+ ok(args->config->cells == NULL, "...cells default");
+ is_int(true, args->config->debug, "...debug set from krb5.conf");
+ is_int(1800, args->config->expires, "...expires set from krb5.conf");
+ is_int(true, args->config->ignore_root, "...ignore_root default");
+ is_int(1000, args->config->minimum_uid,
+ "...minimum_uid set from krb5.conf");
+ ok(args->config->program == NULL, "...program default");
+ status = putil_args_krb5(args, "other-test", options, optlen);
+ ok(status, "Options from krb5.conf (other-test)");
+ is_int(-1000, args->config->minimum_uid,
+ "...minimum_uid set from krb5.conf other-test");
+
+ /* Test with a realm set, which should expose more settings. */
+ krb5_free_context(args->ctx);
+ status = krb5_init_context(&args->ctx);
+ if (status != 0)
+ bail("cannot parse test krb5.conf file");
+ args->realm = strdup("FOO.COM");
+ if (args->realm == NULL)
+ sysbail("cannot allocate memory");
+ status = putil_args_krb5(args, "testing", options, optlen);
+ ok(status, "Options from krb5.conf with FOO.COM");
+ is_int(2, args->config->cells->count, "...cells count from krb5.conf");
+ is_string("foo.com", args->config->cells->strings[0],
+ "...first cell from krb5.conf");
+ is_string("bar.com", args->config->cells->strings[1],
+ "...second cell from krb5.conf");
+ is_int(true, args->config->debug, "...debug set from krb5.conf");
+ is_int(1800, args->config->expires, "...expires set from krb5.conf");
+ is_int(true, args->config->ignore_root, "...ignore_root default");
+ is_int(1000, args->config->minimum_uid,
+ "...minimum_uid set from krb5.conf");
+ is_string("/bin/false", args->config->program,
+ "...program from krb5.conf");
+
+ /* Test with a different realm. */
+ free(args->realm);
+ args->realm = strdup("BAR.COM");
+ if (args->realm == NULL)
+ sysbail("cannot allocate memory");
+ status = putil_args_krb5(args, "testing", options, optlen);
+ ok(status, "Options from krb5.conf with BAR.COM");
+ is_int(2, args->config->cells->count, "...cells count from krb5.conf");
+ is_string("bar.com", args->config->cells->strings[0],
+ "...first cell from krb5.conf");
+ is_string("foo.com", args->config->cells->strings[1],
+ "...second cell from krb5.conf");
+ is_int(true, args->config->debug, "...debug set from krb5.conf");
+ is_int(1800, args->config->expires, "...expires set from krb5.conf");
+ is_int(true, args->config->ignore_root, "...ignore_root default");
+ is_int(1000, args->config->minimum_uid,
+ "...minimum_uid set from krb5.conf");
+ is_string("echo /bin/true", args->config->program,
+ "...program from krb5.conf");
+ config_free(args->config);
+ args->config = config_new();
+ status = putil_args_krb5(args, "other-test", options, optlen);
+ ok(status, "Options from krb5.conf (other-test with realm)");
+ ok(args->config->cells == NULL, "...cells is NULL");
+ is_string("echo /bin/true", args->config->program,
+ "...program from krb5.conf");
+ config_free(args->config);
+ args->config = NULL;
+
+ /* Test for time parsing errors. */
+ args->config = config_new();
+ TEST_ERROR("expires=ft87", LOG_ERR,
+ "bad time value in setting: expires=ft87");
+ config_free(args->config);
+
+ /* Test error reporting from the krb5.conf parser. */
+ args->config = config_new();
+ status = putil_args_krb5(args, "bad-number", options, optlen);
+ ok(status, "Options from krb5.conf (bad-number)");
+ seen = pam_output();
+ is_string("invalid number in krb5.conf setting for minimum_uid: 1000foo",
+ seen->lines[0].line, "...and correct error reported");
+ is_int(LOG_ERR, seen->lines[0].priority, "...with correct priority");
+ pam_output_free(seen);
+ config_free(args->config);
+ args->config = NULL;
+
+ /* Test error reporting on times from the krb5.conf parser. */
+ args->config = config_new();
+ status = putil_args_krb5(args, "bad-time", options, optlen);
+ ok(status, "Options from krb5.conf (bad-time)");
+ seen = pam_output();
+ if (seen == NULL)
+ ok_block(2, false, "...no error output");
+ else {
+ is_string("invalid time in krb5.conf setting for expires: ft87",
+ seen->lines[0].line, "...and correct error reported");
+ is_int(LOG_ERR, seen->lines[0].priority, "...with correct priority");
+ }
+ pam_output_free(seen);
+ config_free(args->config);
+ args->config = NULL;
+
+ test_file_path_free(krb5conf);
+
+#else /* !HAVE_KRB5 */
+
+ skip_block(37, "Kerberos support not configured");
+
+#endif
+
+ putil_args_free(args);
+ pam_end(pamh, 0);
+ return 0;
+}
diff --git a/tests/pam-util/vector-t.c b/tests/pam-util/vector-t.c
new file mode 100644
index 000000000000..d7b87e36d8f4
--- /dev/null
+++ b/tests/pam-util/vector-t.c
@@ -0,0 +1,149 @@
+/*
+ * PAM utility vector library test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2014, 2016, 2018-2019 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <sys/wait.h>
+
+#include <pam-util/vector.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct vector *vector, *ovector, *copy;
+ char *command, *string;
+ const char *env[2];
+ pid_t child;
+ size_t i;
+ const char cstring[] = "This is a\ttest. ";
+
+ plan(60);
+
+ vector = vector_new();
+ ok(vector != NULL, "vector_new returns non-NULL");
+ if (vector == NULL)
+ bail("vector_new returned NULL");
+ ok(vector_add(vector, cstring), "vector_add succeeds");
+ is_int(1, vector->count, "vector_add increases count");
+ ok(vector->strings[0] != cstring, "...and allocated new memory");
+ ok(vector_resize(vector, 4), "vector_resize succeeds");
+ is_int(4, vector->allocated, "vector_resize works");
+ ok(vector_add(vector, cstring), "vector_add #2");
+ ok(vector_add(vector, cstring), "vector_add #3");
+ ok(vector_add(vector, cstring), "vector_add #4");
+ is_int(4, vector->allocated, "...and no reallocation when adding strings");
+ is_int(4, vector->count, "...and the count matches");
+ is_string(cstring, vector->strings[0], "added the right string");
+ is_string(cstring, vector->strings[1], "added the right string");
+ is_string(cstring, vector->strings[2], "added the right string");
+ is_string(cstring, vector->strings[3], "added the right string");
+ ok(vector->strings[1] != vector->strings[2], "each pointer is different");
+ ok(vector->strings[2] != vector->strings[3], "each pointer is different");
+ ok(vector->strings[3] != vector->strings[0], "each pointer is different");
+ ok(vector->strings[0] != cstring, "each pointer is different");
+ copy = vector_copy(vector);
+ ok(copy != NULL, "vector_copy returns non-NULL");
+ if (copy == NULL)
+ bail("vector_copy returned NULL");
+ is_int(4, copy->count, "...and has right count");
+ is_int(4, copy->allocated, "...and has right allocated count");
+ for (i = 0; i < 4; i++) {
+ is_string(cstring, copy->strings[i], "...and string %lu is right",
+ (unsigned long) i);
+ ok(copy->strings[i] != vector->strings[i],
+ "...and pointer %lu is different", (unsigned long) i);
+ }
+ vector_free(copy);
+ vector_clear(vector);
+ is_int(0, vector->count, "vector_clear works");
+ is_int(4, vector->allocated, "...but doesn't free the allocation");
+ string = strdup(cstring);
+ if (string == NULL)
+ sysbail("cannot allocate memory");
+ ok(vector_add(vector, cstring), "vector_add succeeds");
+ ok(vector_add(vector, string), "vector_add succeeds");
+ is_int(2, vector->count, "added two strings to the vector");
+ ok(vector->strings[1] != string, "...and the pointers are different");
+ ok(vector_resize(vector, 1), "vector_resize succeeds");
+ is_int(1, vector->count, "vector_resize shrinks the vector");
+ ok(vector->strings[0] != cstring, "...and the pointer is different");
+ vector_free(vector);
+ free(string);
+
+ vector = vector_split_multi("foo, bar, baz", ", ", NULL);
+ ok(vector != NULL, "vector_split_multi returns non-NULL");
+ if (vector == NULL)
+ bail("vector_split_multi returned NULL");
+ is_int(3, vector->count, "vector_split_multi returns right count");
+ is_string("foo", vector->strings[0], "...first string");
+ is_string("bar", vector->strings[1], "...second string");
+ is_string("baz", vector->strings[2], "...third string");
+ ovector = vector;
+ vector = vector_split_multi("", ", ", vector);
+ ok(vector != NULL, "reuse of vector doesn't return NULL");
+ ok(vector == ovector, "...and reuses the same vector pointer");
+ is_int(0, vector->count, "vector_split_multi reuse with empty string");
+ is_int(3, vector->allocated, "...and doesn't free allocation");
+ vector = vector_split_multi(",,, foo, ", ", ", vector);
+ ok(vector != NULL, "reuse of vector doesn't return NULL");
+ is_int(1, vector->count, "vector_split_multi with extra separators");
+ is_string("foo", vector->strings[0], "...first string");
+ vector = vector_split_multi(", , ", ", ", vector);
+ is_int(0, vector->count, "vector_split_multi with only separators");
+ vector_free(vector);
+
+ vector = vector_new();
+ ok(vector_add(vector, "/bin/sh"), "vector_add succeeds");
+ ok(vector_add(vector, "-c"), "vector_add succeeds");
+ basprintf(&command, "echo ok %lu - vector_exec", testnum++);
+ ok(vector_add(vector, command), "vector_add succeeds");
+ child = fork();
+ if (child < 0)
+ sysbail("unable to fork");
+ else if (child == 0)
+ if (vector_exec("/bin/sh", vector) < 0)
+ sysdiag("unable to exec /bin/sh");
+ waitpid(child, NULL, 0);
+ vector_free(vector);
+ free(command);
+
+ vector = vector_new();
+ ok(vector_add(vector, "/bin/sh"), "vector_add succeeds");
+ ok(vector_add(vector, "-c"), "vector_add succeeds");
+ ok(vector_add(vector, "echo ok $NUMBER - vector_exec_env"),
+ "vector_add succeeds");
+ basprintf(&string, "NUMBER=%lu", testnum++);
+ env[0] = string;
+ env[1] = NULL;
+ child = fork();
+ if (child < 0)
+ sysbail("unable to fork");
+ else if (child == 0)
+ if (vector_exec_env("/bin/sh", vector, env) < 0)
+ sysdiag("unable to exec /bin/sh");
+ waitpid(child, NULL, 0);
+ vector_free(vector);
+ free(string);
+
+ return 0;
+}
diff --git a/tests/portable/asprintf-t.c b/tests/portable/asprintf-t.c
new file mode 100644
index 000000000000..3b10a6622c31
--- /dev/null
+++ b/tests/portable/asprintf-t.c
@@ -0,0 +1,69 @@
+/*
+ * asprintf and vasprintf test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2014, 2018 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006-2009, 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/macros.h>
+#include <portable/system.h>
+
+#include <tests/tap/basic.h>
+
+int test_asprintf(char **, const char *, ...)
+ __attribute__((__format__(printf, 2, 3)));
+int test_vasprintf(char **, const char *, va_list)
+ __attribute__((__format__(printf, 2, 0)));
+
+static int __attribute__((__format__(printf, 2, 3)))
+vatest(char **result, const char *format, ...)
+{
+ va_list args;
+ int status;
+
+ va_start(args, format);
+ status = test_vasprintf(result, format, args);
+ va_end(args);
+ return status;
+}
+
+int
+main(void)
+{
+ char *result = NULL;
+
+ plan(12);
+
+ is_int(7, test_asprintf(&result, "%s", "testing"), "asprintf length");
+ is_string("testing", result, "asprintf result");
+ free(result);
+ ok(3, "free asprintf");
+ is_int(0, test_asprintf(&result, "%s", ""), "asprintf empty length");
+ is_string("", result, "asprintf empty string");
+ free(result);
+ ok(6, "free asprintf of empty string");
+
+ is_int(6, vatest(&result, "%d %s", 2, "test"), "vasprintf length");
+ is_string("2 test", result, "vasprintf result");
+ free(result);
+ ok(9, "free vasprintf");
+ is_int(0, vatest(&result, "%s", ""), "vasprintf empty length");
+ is_string("", result, "vasprintf empty string");
+ free(result);
+ ok(12, "free vasprintf of empty string");
+
+ return 0;
+}
diff --git a/tests/portable/asprintf.c b/tests/portable/asprintf.c
new file mode 100644
index 000000000000..221c9932c5cd
--- /dev/null
+++ b/tests/portable/asprintf.c
@@ -0,0 +1,2 @@
+#define TESTING 1
+#include <portable/asprintf.c>
diff --git a/tests/portable/mkstemp-t.c b/tests/portable/mkstemp-t.c
new file mode 100644
index 000000000000..dc268210f063
--- /dev/null
+++ b/tests/portable/mkstemp-t.c
@@ -0,0 +1,81 @@
+/*
+ * mkstemp test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009, 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <errno.h>
+#include <sys/stat.h>
+
+#include <tests/tap/basic.h>
+
+int test_mkstemp(char *template);
+
+int
+main(void)
+{
+ int fd;
+ char template[] = "tsXXXXXXX";
+ char tooshort[] = "XXXXX";
+ char bad1[] = "/foo/barXXXXX";
+ char bad2[] = "/foo/barXXXXXX.out";
+ char buffer[256];
+ struct stat st1, st2;
+ ssize_t length;
+
+ plan(20);
+
+ /* First, test a few error messages. */
+ errno = 0;
+ is_int(-1, test_mkstemp(tooshort), "too short of template");
+ is_int(EINVAL, errno, "...with correct errno");
+ is_string("XXXXX", tooshort, "...and template didn't change");
+ errno = 0;
+ is_int(-1, test_mkstemp(bad1), "bad template");
+ is_int(EINVAL, errno, "...with correct errno");
+ is_string("/foo/barXXXXX", bad1, "...and template didn't change");
+ errno = 0;
+ is_int(-1, test_mkstemp(bad2), "template doesn't end in XXXXXX");
+ is_int(EINVAL, errno, "...with correct errno");
+ is_string("/foo/barXXXXXX.out", bad2, "...and template didn't change");
+ errno = 0;
+
+ /* Now try creating a real file. */
+ fd = test_mkstemp(template);
+ ok(fd >= 0, "mkstemp works with valid template");
+ ok(strcmp(template, "tsXXXXXXX") != 0, "...and template changed");
+ ok(strncmp(template, "tsX", 3) == 0, "...and didn't touch first X");
+ ok(access(template, F_OK) == 0, "...and the file exists");
+
+ /* Make sure that it's the same file as template refers to now. */
+ ok(stat(template, &st1) == 0, "...and stat of template works");
+ ok(fstat(fd, &st2) == 0, "...and stat of open file descriptor works");
+ ok(st1.st_ino == st2.st_ino, "...and they're the same file");
+ unlink(template);
+
+ /* Make sure the open mode is correct. */
+ length = strlen(template);
+ is_int(length, write(fd, template, length), "write to open file works");
+ ok(lseek(fd, 0, SEEK_SET) == 0, "...and rewind works");
+ is_int(length, read(fd, buffer, length), "...and the data is there");
+ buffer[length] = '\0';
+ is_string(template, buffer, "...and matches what we wrote");
+ close(fd);
+
+ return 0;
+}
diff --git a/tests/portable/mkstemp.c b/tests/portable/mkstemp.c
new file mode 100644
index 000000000000..4632d3de86ed
--- /dev/null
+++ b/tests/portable/mkstemp.c
@@ -0,0 +1,2 @@
+#define TESTING 1
+#include <portable/mkstemp.c>
diff --git a/tests/portable/strndup-t.c b/tests/portable/strndup-t.c
new file mode 100644
index 000000000000..9bf28a31beec
--- /dev/null
+++ b/tests/portable/strndup-t.c
@@ -0,0 +1,60 @@
+/*
+ * strndup test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2018 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <tests/tap/basic.h>
+
+char *test_strndup(const char *, size_t);
+
+
+int
+main(void)
+{
+ char buffer[3];
+ char *result;
+
+ plan(7);
+
+ result = test_strndup("foo", 8);
+ is_string("foo", result, "strndup longer than string");
+ free(result);
+ result = test_strndup("foo", 2);
+ is_string("fo", result, "strndup shorter than string");
+ free(result);
+ result = test_strndup("foo", 3);
+ is_string("foo", result, "strndup same size as string");
+ free(result);
+ result = test_strndup("foo", 0);
+ is_string("", result, "strndup of size 0");
+ free(result);
+ memcpy(buffer, "foo", 3);
+ result = test_strndup(buffer, 3);
+ is_string("foo", result, "strndup of non-nul-terminated string");
+ free(result);
+ errno = 0;
+ result = test_strndup(NULL, 0);
+ is_string(NULL, result, "strndup of NULL");
+ is_int(errno, EINVAL, "...and returns EINVAL");
+
+ return 0;
+}
diff --git a/tests/portable/strndup.c b/tests/portable/strndup.c
new file mode 100644
index 000000000000..99c3bc13a744
--- /dev/null
+++ b/tests/portable/strndup.c
@@ -0,0 +1,2 @@
+#define TESTING 1
+#include <portable/strndup.c>
diff --git a/tests/runtests.c b/tests/runtests.c
new file mode 100644
index 000000000000..54ec1c93d08b
--- /dev/null
+++ b/tests/runtests.c
@@ -0,0 +1,1782 @@
+/*
+ * Run a set of tests, reporting results.
+ *
+ * Test suite driver that runs a set of tests implementing a subset of the
+ * Test Anything Protocol (TAP) and reports the results.
+ *
+ * Any bug reports, bug fixes, and improvements are very much welcome and
+ * should be sent to the e-mail address below. This program is part of C TAP
+ * Harness <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+ *
+ * Copyright 2000-2001, 2004, 2006-2019 Russ Allbery <eagle@eyrie.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/*
+ * Usage:
+ *
+ * runtests [-hv] [-b <build-dir>] [-s <source-dir>] -l <test-list>
+ * runtests [-hv] [-b <build-dir>] [-s <source-dir>] <test> [<test> ...]
+ * runtests -o [-h] [-b <build-dir>] [-s <source-dir>] <test>
+ *
+ * In the first case, expects a list of executables located in the given file,
+ * one line per executable, possibly followed by a space-separated list of
+ * options. For each one, runs it as part of a test suite, reporting results.
+ * In the second case, use the same infrastructure, but run only the tests
+ * listed on the command line.
+ *
+ * Test output should start with a line containing the number of tests
+ * (numbered from 1 to this number), optionally preceded by "1..", although
+ * that line may be given anywhere in the output. Each additional line should
+ * be in the following format:
+ *
+ * ok <number>
+ * not ok <number>
+ * ok <number> # skip
+ * not ok <number> # todo
+ *
+ * where <number> is the number of the test. An optional comment is permitted
+ * after the number if preceded by whitespace. ok indicates success, not ok
+ * indicates failure. "# skip" and "# todo" are a special cases of a comment,
+ * and must start with exactly that formatting. They indicate the test was
+ * skipped for some reason (maybe because it doesn't apply to this platform)
+ * or is testing something known to currently fail. The text following either
+ * "# skip" or "# todo" and whitespace is the reason.
+ *
+ * As a special case, the first line of the output may be in the form:
+ *
+ * 1..0 # skip some reason
+ *
+ * which indicates that this entire test case should be skipped and gives a
+ * reason.
+ *
+ * Any other lines are ignored, although for compliance with the TAP protocol
+ * all lines other than the ones in the above format should be sent to
+ * standard error rather than standard output and start with #.
+ *
+ * This is a subset of TAP as documented in Test::Harness::TAP or
+ * TAP::Parser::Grammar, which comes with Perl.
+ *
+ * If the -o option is given, instead run a single test and display all of its
+ * output. This is intended for use with failing tests so that the person
+ * running the test suite can get more details about what failed.
+ *
+ * If built with the C preprocessor symbols C_TAP_SOURCE and C_TAP_BUILD
+ * defined, C TAP Harness will export those values in the environment so that
+ * tests can find the source and build directory and will look for tests under
+ * both directories. These paths can also be set with the -b and -s
+ * command-line options, which will override anything set at build time.
+ *
+ * If the -v option is given, or the C_TAP_VERBOSE environment variable is set,
+ * display the full output of each test as it runs rather than showing a
+ * summary of the results of each test.
+ */
+
+/* Required for fdopen(), getopt(), and putenv(). */
+#if defined(__STRICT_ANSI__) || defined(PEDANTIC)
+# ifndef _XOPEN_SOURCE
+# define _XOPEN_SOURCE 500
+# endif
+#endif
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+/* sys/time.h must be included before sys/resource.h on some platforms. */
+#include <sys/resource.h>
+
+/* AIX 6.1 (and possibly later) doesn't have WCOREDUMP. */
+#ifndef WCOREDUMP
+# define WCOREDUMP(status) ((unsigned) (status) &0x80)
+#endif
+
+/*
+ * POSIX requires that these be defined in <unistd.h>, but they're not always
+ * available. If one of them has been defined, all the rest almost certainly
+ * have.
+ */
+#ifndef STDIN_FILENO
+# define STDIN_FILENO 0
+# define STDOUT_FILENO 1
+# define STDERR_FILENO 2
+#endif
+
+/*
+ * Used for iterating through arrays. Returns the number of elements in the
+ * array (useful for a < upper bound in a for loop).
+ */
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
+/*
+ * The source and build versions of the tests directory. This is used to set
+ * the C_TAP_SOURCE and C_TAP_BUILD environment variables (and the SOURCE and
+ * BUILD environment variables set for backward compatibility) and find test
+ * programs, if set. Normally, this should be set as part of the build
+ * process to the test subdirectories of $(abs_top_srcdir) and
+ * $(abs_top_builddir) respectively.
+ */
+#ifndef C_TAP_SOURCE
+# define C_TAP_SOURCE NULL
+#endif
+#ifndef C_TAP_BUILD
+# define C_TAP_BUILD NULL
+#endif
+
+/* Test status codes. */
+enum test_status
+{
+ TEST_FAIL,
+ TEST_PASS,
+ TEST_SKIP,
+ TEST_INVALID
+};
+
+/* Really, just a boolean, but this is more self-documenting. */
+enum test_verbose
+{
+ CONCISE = 0,
+ VERBOSE = 1
+};
+
+/* Indicates the state of our plan. */
+enum plan_status
+{
+ PLAN_INIT, /* Nothing seen yet. */
+ PLAN_FIRST, /* Plan seen before any tests. */
+ PLAN_PENDING, /* Test seen and no plan yet. */
+ PLAN_FINAL /* Plan seen after some tests. */
+};
+
+/* Error exit statuses for test processes. */
+#define CHILDERR_DUP 100 /* Couldn't redirect stderr or stdout. */
+#define CHILDERR_EXEC 101 /* Couldn't exec child process. */
+#define CHILDERR_STDIN 102 /* Couldn't open stdin file. */
+#define CHILDERR_STDERR 103 /* Couldn't open stderr file. */
+
+/* Structure to hold data for a set of tests. */
+struct testset {
+ char *file; /* The file name of the test. */
+ char **command; /* The argv vector to run the command. */
+ enum plan_status plan; /* The status of our plan. */
+ unsigned long count; /* Expected count of tests. */
+ unsigned long current; /* The last seen test number. */
+ unsigned int length; /* The length of the last status message. */
+ unsigned long passed; /* Count of passing tests. */
+ unsigned long failed; /* Count of failing lists. */
+ unsigned long skipped; /* Count of skipped tests (passed). */
+ unsigned long allocated; /* The size of the results table. */
+ enum test_status *results; /* Table of results by test number. */
+ unsigned int aborted; /* Whether the set was aborted. */
+ unsigned int reported; /* Whether the results were reported. */
+ int status; /* The exit status of the test. */
+ unsigned int all_skipped; /* Whether all tests were skipped. */
+ char *reason; /* Why all tests were skipped. */
+};
+
+/* Structure to hold a linked list of test sets. */
+struct testlist {
+ struct testset *ts;
+ struct testlist *next;
+};
+
+/*
+ * Usage message. Should be used as a printf format with four arguments: the
+ * path to runtests, given three times, and the usage_description. This is
+ * split into variables to satisfy the pedantic ISO C90 limit on strings.
+ */
+static const char usage_message[] = "\
+Usage: %s [-hv] [-b <build-dir>] [-s <source-dir>] <test> ...\n\
+ %s [-hv] [-b <build-dir>] [-s <source-dir>] -l <test-list>\n\
+ %s -o [-h] [-b <build-dir>] [-s <source-dir>] <test>\n\
+\n\
+Options:\n\
+ -b <build-dir> Set the build directory to <build-dir>\n\
+%s";
+static const char usage_extra[] = "\
+ -l <list> Take the list of tests to run from <test-list>\n\
+ -o Run a single test rather than a list of tests\n\
+ -s <source-dir> Set the source directory to <source-dir>\n\
+ -v Show the full output of each test\n\
+\n\
+runtests normally runs each test listed on the command line. With the -l\n\
+option, it instead runs every test listed in a file. With the -o option,\n\
+it instead runs a single test and shows its complete output.\n";
+
+/*
+ * Header used for test output. %s is replaced by the file name of the list
+ * of tests.
+ */
+static const char banner[] = "\n\
+Running all tests listed in %s. If any tests fail, run the failing\n\
+test program with runtests -o to see more details.\n\n";
+
+/* Header for reports of failed tests. */
+static const char header[] = "\n\
+Failed Set Fail/Total (%) Skip Stat Failing Tests\n\
+-------------------------- -------------- ---- ---- ------------------------";
+
+/* Include the file name and line number in malloc failures. */
+#define xcalloc(n, type) \
+ ((type *) x_calloc((n), sizeof(type), __FILE__, __LINE__))
+#define xmalloc(size) ((char *) x_malloc((size), __FILE__, __LINE__))
+#define xstrdup(p) x_strdup((p), __FILE__, __LINE__)
+#define xstrndup(p, size) x_strndup((p), (size), __FILE__, __LINE__)
+#define xreallocarray(p, n, type) \
+ ((type *) x_reallocarray((p), (n), sizeof(type), __FILE__, __LINE__))
+
+/*
+ * __attribute__ is available in gcc 2.5 and later, but only with gcc 2.7
+ * could you use the __format__ form of the attributes, which is what we use
+ * (to avoid confusion with other macros).
+ */
+#ifndef __attribute__
+# if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 7)
+# define __attribute__(spec) /* empty */
+# endif
+#endif
+
+/*
+ * We use __alloc_size__, but it was only available in fairly recent versions
+ * of GCC. Suppress warnings about the unknown attribute if GCC is too old.
+ * We know that we're GCC at this point, so we can use the GCC variadic macro
+ * extension, which will still work with versions of GCC too old to have C99
+ * variadic macro support.
+ */
+#if !defined(__attribute__) && !defined(__alloc_size__)
+# if defined(__GNUC__) && !defined(__clang__)
+# if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 3)
+# define __alloc_size__(spec, args...) /* empty */
+# endif
+# endif
+#endif
+
+/*
+ * LLVM and Clang pretend to be GCC but don't support all of the __attribute__
+ * settings that GCC does. For them, suppress warnings about unknown
+ * attributes on declarations. This unfortunately will affect the entire
+ * compilation context, but there's no push and pop available.
+ */
+#if !defined(__attribute__) && (defined(__llvm__) || defined(__clang__))
+# pragma GCC diagnostic ignored "-Wattributes"
+#endif
+
+/* Declare internal functions that benefit from compiler attributes. */
+static void die(const char *, ...)
+ __attribute__((__nonnull__, __noreturn__, __format__(printf, 1, 2)));
+static void sysdie(const char *, ...)
+ __attribute__((__nonnull__, __noreturn__, __format__(printf, 1, 2)));
+static void *x_calloc(size_t, size_t, const char *, int)
+ __attribute__((__alloc_size__(1, 2), __malloc__, __nonnull__));
+static void *x_malloc(size_t, const char *, int)
+ __attribute__((__alloc_size__(1), __malloc__, __nonnull__));
+static void *x_reallocarray(void *, size_t, size_t, const char *, int)
+ __attribute__((__alloc_size__(2, 3), __malloc__, __nonnull__(4)));
+static char *x_strdup(const char *, const char *, int)
+ __attribute__((__malloc__, __nonnull__));
+static char *x_strndup(const char *, size_t, const char *, int)
+ __attribute__((__malloc__, __nonnull__));
+
+
+/*
+ * Report a fatal error and exit.
+ */
+static void
+die(const char *format, ...)
+{
+ va_list args;
+
+ fflush(stdout);
+ fprintf(stderr, "runtests: ");
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+ fprintf(stderr, "\n");
+ exit(1);
+}
+
+
+/*
+ * Report a fatal error, including the results of strerror, and exit.
+ */
+static void
+sysdie(const char *format, ...)
+{
+ int oerrno;
+ va_list args;
+
+ oerrno = errno;
+ fflush(stdout);
+ fprintf(stderr, "runtests: ");
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+ fprintf(stderr, ": %s\n", strerror(oerrno));
+ exit(1);
+}
+
+
+/*
+ * Allocate zeroed memory, reporting a fatal error and exiting on failure.
+ */
+static void *
+x_calloc(size_t n, size_t size, const char *file, int line)
+{
+ void *p;
+
+ n = (n > 0) ? n : 1;
+ size = (size > 0) ? size : 1;
+ p = calloc(n, size);
+ if (p == NULL)
+ sysdie("failed to calloc %lu bytes at %s line %d",
+ (unsigned long) size, file, line);
+ return p;
+}
+
+
+/*
+ * Allocate memory, reporting a fatal error and exiting on failure.
+ */
+static void *
+x_malloc(size_t size, const char *file, int line)
+{
+ void *p;
+
+ p = malloc(size);
+ if (p == NULL)
+ sysdie("failed to malloc %lu bytes at %s line %d",
+ (unsigned long) size, file, line);
+ return p;
+}
+
+
+/*
+ * Reallocate memory, reporting a fatal error and exiting on failure.
+ *
+ * We should technically use SIZE_MAX here for the overflow check, but
+ * SIZE_MAX is C99 and we're only assuming C89 + SUSv3, which does not
+ * guarantee that it exists. They do guarantee that UINT_MAX exists, and we
+ * can assume that UINT_MAX <= SIZE_MAX. And we should not be allocating
+ * anything anywhere near that large.
+ *
+ * (In theory, C89 and C99 permit size_t to be smaller than unsigned int, but
+ * I disbelieve in the existence of such systems and they will have to cope
+ * without overflow checks.)
+ */
+static void *
+x_reallocarray(void *p, size_t n, size_t size, const char *file, int line)
+{
+ n = (n > 0) ? n : 1;
+ size = (size > 0) ? size : 1;
+
+ if (n > 0 && UINT_MAX / n <= size)
+ sysdie("realloc too large at %s line %d", file, line);
+ p = realloc(p, n * size);
+ if (p == NULL)
+ sysdie("failed to realloc %lu bytes at %s line %d",
+ (unsigned long) (n * size), file, line);
+ return p;
+}
+
+
+/*
+ * Copy a string, reporting a fatal error and exiting on failure.
+ */
+static char *
+x_strdup(const char *s, const char *file, int line)
+{
+ char *p;
+ size_t len;
+
+ len = strlen(s) + 1;
+ p = (char *) malloc(len);
+ if (p == NULL)
+ sysdie("failed to strdup %lu bytes at %s line %d", (unsigned long) len,
+ file, line);
+ memcpy(p, s, len);
+ return p;
+}
+
+
+/*
+ * Copy the first n characters of a string, reporting a fatal error and
+ * existing on failure.
+ *
+ * Avoid using the system strndup function since it may not exist (on Mac OS
+ * X, for example), and there's no need to introduce another portability
+ * requirement.
+ */
+char *
+x_strndup(const char *s, size_t size, const char *file, int line)
+{
+ const char *p;
+ size_t len;
+ char *copy;
+
+ /* Don't assume that the source string is nul-terminated. */
+ for (p = s; (size_t)(p - s) < size && *p != '\0'; p++)
+ ;
+ len = (size_t)(p - s);
+ copy = (char *) malloc(len + 1);
+ if (copy == NULL)
+ sysdie("failed to strndup %lu bytes at %s line %d",
+ (unsigned long) len, file, line);
+ memcpy(copy, s, len);
+ copy[len] = '\0';
+ return copy;
+}
+
+
+/*
+ * Form a new string by concatenating multiple strings. The arguments must be
+ * terminated by (const char *) 0.
+ *
+ * This function only exists because we can't assume asprintf. We can't
+ * simulate asprintf with snprintf because we're only assuming SUSv3, which
+ * does not require that snprintf with a NULL buffer return the required
+ * length. When those constraints are relaxed, this should be ripped out and
+ * replaced with asprintf or a more trivial replacement with snprintf.
+ */
+static char *
+concat(const char *first, ...)
+{
+ va_list args;
+ char *result;
+ const char *string;
+ size_t offset;
+ size_t length = 0;
+
+ /*
+ * Find the total memory required. Ensure we don't overflow length. We
+ * aren't guaranteed to have SIZE_MAX, so use UINT_MAX as an acceptable
+ * substitute (see the x_nrealloc comments).
+ */
+ va_start(args, first);
+ for (string = first; string != NULL; string = va_arg(args, const char *)) {
+ if (length >= UINT_MAX - strlen(string)) {
+ errno = EINVAL;
+ sysdie("strings too long in concat");
+ }
+ length += strlen(string);
+ }
+ va_end(args);
+ length++;
+
+ /* Create the string. */
+ result = xmalloc(length);
+ va_start(args, first);
+ offset = 0;
+ for (string = first; string != NULL; string = va_arg(args, const char *)) {
+ memcpy(result + offset, string, strlen(string));
+ offset += strlen(string);
+ }
+ va_end(args);
+ result[offset] = '\0';
+ return result;
+}
+
+
+/*
+ * Given a struct timeval, return the number of seconds it represents as a
+ * double. Use difftime() to convert a time_t to a double.
+ */
+static double
+tv_seconds(const struct timeval *tv)
+{
+ return difftime(tv->tv_sec, 0) + (double) tv->tv_usec * 1e-6;
+}
+
+
+/*
+ * Given two struct timevals, return the difference in seconds.
+ */
+static double
+tv_diff(const struct timeval *tv1, const struct timeval *tv0)
+{
+ return tv_seconds(tv1) - tv_seconds(tv0);
+}
+
+
+/*
+ * Given two struct timevals, return the sum in seconds as a double.
+ */
+static double
+tv_sum(const struct timeval *tv1, const struct timeval *tv2)
+{
+ return tv_seconds(tv1) + tv_seconds(tv2);
+}
+
+
+/*
+ * Given a pointer to a string, skip any leading whitespace and return a
+ * pointer to the first non-whitespace character.
+ */
+static const char *
+skip_whitespace(const char *p)
+{
+ while (isspace((unsigned char) (*p)))
+ p++;
+ return p;
+}
+
+
+/*
+ * Given a pointer to a string, skip any non-whitespace characters and return
+ * a pointer to the first whitespace character, or to the end of the string.
+ */
+static const char *
+skip_non_whitespace(const char *p)
+{
+ while (*p != '\0' && !isspace((unsigned char) (*p)))
+ p++;
+ return p;
+}
+
+
+/*
+ * Start a program, connecting its stdout to a pipe on our end and its stderr
+ * to /dev/null, and storing the file descriptor to read from in the two
+ * argument. Returns the PID of the new process. Errors are fatal.
+ */
+static pid_t
+test_start(char *const *command, int *fd)
+{
+ int fds[2], infd, errfd;
+ pid_t child;
+
+ /* Create a pipe used to capture the output from the test program. */
+ if (pipe(fds) == -1) {
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("can't create pipe");
+ }
+
+ /* Fork a child process, massage the file descriptors, and exec. */
+ child = fork();
+ switch (child) {
+ case -1:
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("can't fork");
+
+ /* In the child. Set up our standard output. */
+ case 0:
+ close(fds[0]);
+ close(STDOUT_FILENO);
+ if (dup2(fds[1], STDOUT_FILENO) < 0)
+ _exit(CHILDERR_DUP);
+ close(fds[1]);
+
+ /* Point standard input at /dev/null. */
+ close(STDIN_FILENO);
+ infd = open("/dev/null", O_RDONLY);
+ if (infd < 0)
+ _exit(CHILDERR_STDIN);
+ if (infd != STDIN_FILENO) {
+ if (dup2(infd, STDIN_FILENO) < 0)
+ _exit(CHILDERR_DUP);
+ close(infd);
+ }
+
+ /* Point standard error at /dev/null. */
+ close(STDERR_FILENO);
+ errfd = open("/dev/null", O_WRONLY);
+ if (errfd < 0)
+ _exit(CHILDERR_STDERR);
+ if (errfd != STDERR_FILENO) {
+ if (dup2(errfd, STDERR_FILENO) < 0)
+ _exit(CHILDERR_DUP);
+ close(errfd);
+ }
+
+ /* Now, exec our process. */
+ if (execv(command[0], command) == -1)
+ _exit(CHILDERR_EXEC);
+ break;
+
+ /* In parent. Close the extra file descriptor. */
+ default:
+ close(fds[1]);
+ break;
+ }
+ *fd = fds[0];
+ return child;
+}
+
+
+/*
+ * Back up over the output saying what test we were executing.
+ */
+static void
+test_backspace(struct testset *ts)
+{
+ unsigned int i;
+
+ if (!isatty(STDOUT_FILENO))
+ return;
+ for (i = 0; i < ts->length; i++)
+ putchar('\b');
+ for (i = 0; i < ts->length; i++)
+ putchar(' ');
+ for (i = 0; i < ts->length; i++)
+ putchar('\b');
+ ts->length = 0;
+}
+
+
+/*
+ * Allocate or resize the array of test results to be large enough to contain
+ * the test number in.
+ */
+static void
+resize_results(struct testset *ts, unsigned long n)
+{
+ unsigned long i;
+ size_t s;
+
+ /* If there's already enough space, return quickly. */
+ if (n <= ts->allocated)
+ return;
+
+ /*
+ * If no space has been allocated, do the initial allocation. Otherwise,
+ * resize. Start with 32 test cases and then add 1024 with each resize to
+ * try to reduce the number of reallocations.
+ */
+ if (ts->allocated == 0) {
+ s = (n > 32) ? n : 32;
+ ts->results = xcalloc(s, enum test_status);
+ } else {
+ s = (n > ts->allocated + 1024) ? n : ts->allocated + 1024;
+ ts->results = xreallocarray(ts->results, s, enum test_status);
+ }
+
+ /* Set the results for the newly-allocated test array. */
+ for (i = ts->allocated; i < s; i++)
+ ts->results[i] = TEST_INVALID;
+ ts->allocated = s;
+}
+
+
+/*
+ * Report an invalid test number and set the appropriate flags. Pulled into a
+ * separate function since we do this in several places.
+ */
+static void
+invalid_test_number(struct testset *ts, long n, enum test_verbose verbose)
+{
+ if (!verbose)
+ test_backspace(ts);
+ printf("ABORTED (invalid test number %ld)\n", n);
+ ts->aborted = 1;
+ ts->reported = 1;
+}
+
+
+/*
+ * Read the plan line of test output, which should contain the range of test
+ * numbers. We may initialize the testset structure here if we haven't yet
+ * seen a test. Return true if initialization succeeded and the test should
+ * continue, false otherwise.
+ */
+static int
+test_plan(const char *line, struct testset *ts, enum test_verbose verbose)
+{
+ long n;
+
+ /*
+ * Accept a plan without the leading 1.. for compatibility with older
+ * versions of runtests. This will only be allowed if we've not yet seen
+ * a test result.
+ */
+ line = skip_whitespace(line);
+ if (strncmp(line, "1..", 3) == 0)
+ line += 3;
+
+ /*
+ * Get the count and check it for validity.
+ *
+ * If we have something of the form "1..0 # skip foo", the whole file was
+ * skipped; record that. If we do skip the whole file, zero out all of
+ * our statistics, since they're no longer relevant.
+ *
+ * strtol is called with a second argument to advance the line pointer
+ * past the count to make it simpler to detect the # skip case.
+ */
+ n = strtol(line, (char **) &line, 10);
+ if (n == 0) {
+ line = skip_whitespace(line);
+ if (*line == '#') {
+ line = skip_whitespace(line + 1);
+ if (strncasecmp(line, "skip", 4) == 0) {
+ line = skip_whitespace(line + 4);
+ if (*line != '\0') {
+ ts->reason = xstrdup(line);
+ ts->reason[strlen(ts->reason) - 1] = '\0';
+ }
+ ts->all_skipped = 1;
+ ts->aborted = 1;
+ ts->count = 0;
+ ts->passed = 0;
+ ts->skipped = 0;
+ ts->failed = 0;
+ return 0;
+ }
+ }
+ }
+ if (n <= 0) {
+ puts("ABORTED (invalid test count)");
+ ts->aborted = 1;
+ ts->reported = 1;
+ return 0;
+ }
+
+ /*
+ * If we are doing lazy planning, check the plan against the largest test
+ * number that we saw and fail now if we saw a check outside the plan
+ * range.
+ */
+ if (ts->plan == PLAN_PENDING && (unsigned long) n < ts->count) {
+ invalid_test_number(ts, (long) ts->count, verbose);
+ return 0;
+ }
+
+ /*
+ * Otherwise, allocated or resize the results if needed and update count,
+ * and then record that we've seen a plan.
+ */
+ resize_results(ts, (unsigned long) n);
+ ts->count = (unsigned long) n;
+ if (ts->plan == PLAN_INIT)
+ ts->plan = PLAN_FIRST;
+ else if (ts->plan == PLAN_PENDING)
+ ts->plan = PLAN_FINAL;
+ return 1;
+}
+
+
+/*
+ * Given a single line of output from a test, parse it and return the success
+ * status of that test. Anything printed to stdout not matching the form
+ * /^(not )?ok \d+/ is ignored. Sets ts->current to the test number that just
+ * reported status.
+ */
+static void
+test_checkline(const char *line, struct testset *ts, enum test_verbose verbose)
+{
+ enum test_status status = TEST_PASS;
+ const char *bail;
+ char *end;
+ long number;
+ unsigned long current;
+ int outlen;
+
+ /* Before anything, check for a test abort. */
+ bail = strstr(line, "Bail out!");
+ if (bail != NULL) {
+ bail = skip_whitespace(bail + strlen("Bail out!"));
+ if (*bail != '\0') {
+ size_t length;
+
+ length = strlen(bail);
+ if (bail[length - 1] == '\n')
+ length--;
+ if (!verbose)
+ test_backspace(ts);
+ printf("ABORTED (%.*s)\n", (int) length, bail);
+ ts->reported = 1;
+ }
+ ts->aborted = 1;
+ return;
+ }
+
+ /*
+ * If the given line isn't newline-terminated, it was too big for an
+ * fgets(), which means ignore it.
+ */
+ if (line[strlen(line) - 1] != '\n')
+ return;
+
+ /* If the line begins with a hash mark, ignore it. */
+ if (line[0] == '#')
+ return;
+
+ /* If we haven't yet seen a plan, look for one. */
+ if (ts->plan == PLAN_INIT && isdigit((unsigned char) (*line))) {
+ if (!test_plan(line, ts, verbose))
+ return;
+ } else if (strncmp(line, "1..", 3) == 0) {
+ if (ts->plan == PLAN_PENDING) {
+ if (!test_plan(line, ts, verbose))
+ return;
+ } else {
+ if (!verbose)
+ test_backspace(ts);
+ puts("ABORTED (multiple plans)");
+ ts->aborted = 1;
+ ts->reported = 1;
+ return;
+ }
+ }
+
+ /* Parse the line, ignoring something we can't parse. */
+ if (strncmp(line, "not ", 4) == 0) {
+ status = TEST_FAIL;
+ line += 4;
+ }
+ if (strncmp(line, "ok", 2) != 0)
+ return;
+ line = skip_whitespace(line + 2);
+ errno = 0;
+ number = strtol(line, &end, 10);
+ if (errno != 0 || end == line)
+ current = ts->current + 1;
+ else if (number <= 0) {
+ invalid_test_number(ts, number, verbose);
+ return;
+ } else
+ current = (unsigned long) number;
+ if (current > ts->count && ts->plan == PLAN_FIRST) {
+ invalid_test_number(ts, (long) current, verbose);
+ return;
+ }
+
+ /* We have a valid test result. Tweak the results array if needed. */
+ if (ts->plan == PLAN_INIT || ts->plan == PLAN_PENDING) {
+ ts->plan = PLAN_PENDING;
+ resize_results(ts, current);
+ if (current > ts->count)
+ ts->count = current;
+ }
+
+ /*
+ * Handle directives. We should probably do something more interesting
+ * with unexpected passes of todo tests.
+ */
+ while (isdigit((unsigned char) (*line)))
+ line++;
+ line = skip_whitespace(line);
+ if (*line == '#') {
+ line = skip_whitespace(line + 1);
+ if (strncasecmp(line, "skip", 4) == 0)
+ status = TEST_SKIP;
+ if (strncasecmp(line, "todo", 4) == 0)
+ status = (status == TEST_FAIL) ? TEST_SKIP : TEST_FAIL;
+ }
+
+ /* Make sure that the test number is in range and not a duplicate. */
+ if (ts->results[current - 1] != TEST_INVALID) {
+ if (!verbose)
+ test_backspace(ts);
+ printf("ABORTED (duplicate test number %lu)\n", current);
+ ts->aborted = 1;
+ ts->reported = 1;
+ return;
+ }
+
+ /* Good results. Increment our various counters. */
+ switch (status) {
+ case TEST_PASS:
+ ts->passed++;
+ break;
+ case TEST_FAIL:
+ ts->failed++;
+ break;
+ case TEST_SKIP:
+ ts->skipped++;
+ break;
+ case TEST_INVALID:
+ break;
+ }
+ ts->current = current;
+ ts->results[current - 1] = status;
+ if (!verbose && isatty(STDOUT_FILENO)) {
+ test_backspace(ts);
+ if (ts->plan == PLAN_PENDING)
+ outlen = printf("%lu/?", current);
+ else
+ outlen = printf("%lu/%lu", current, ts->count);
+ ts->length = (outlen >= 0) ? (unsigned int) outlen : 0;
+ fflush(stdout);
+ }
+}
+
+
+/*
+ * Print out a range of test numbers, returning the number of characters it
+ * took up. Takes the first number, the last number, the number of characters
+ * already printed on the line, and the limit of number of characters the line
+ * can hold. Add a comma and a space before the range if chars indicates that
+ * something has already been printed on the line, and print ... instead if
+ * chars plus the space needed would go over the limit (use a limit of 0 to
+ * disable this).
+ */
+static unsigned int
+test_print_range(unsigned long first, unsigned long last, unsigned long chars,
+ unsigned int limit)
+{
+ unsigned int needed = 0;
+ unsigned long n;
+
+ for (n = first; n > 0; n /= 10)
+ needed++;
+ if (last > first) {
+ for (n = last; n > 0; n /= 10)
+ needed++;
+ needed++;
+ }
+ if (chars > 0)
+ needed += 2;
+ if (limit > 0 && chars + needed > limit) {
+ needed = 0;
+ if (chars <= limit) {
+ if (chars > 0) {
+ printf(", ");
+ needed += 2;
+ }
+ printf("...");
+ needed += 3;
+ }
+ } else {
+ if (chars > 0)
+ printf(", ");
+ if (last > first)
+ printf("%lu-", first);
+ printf("%lu", last);
+ }
+ return needed;
+}
+
+
+/*
+ * Summarize a single test set. The second argument is 0 if the set exited
+ * cleanly, a positive integer representing the exit status if it exited
+ * with a non-zero status, and a negative integer representing the signal
+ * that terminated it if it was killed by a signal.
+ */
+static void
+test_summarize(struct testset *ts, int status)
+{
+ unsigned long i;
+ unsigned long missing = 0;
+ unsigned long failed = 0;
+ unsigned long first = 0;
+ unsigned long last = 0;
+
+ if (ts->aborted) {
+ fputs("ABORTED", stdout);
+ if (ts->count > 0)
+ printf(" (passed %lu/%lu)", ts->passed, ts->count - ts->skipped);
+ } else {
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_INVALID) {
+ if (missing == 0)
+ fputs("MISSED ", stdout);
+ if (first && i == last)
+ last = i + 1;
+ else {
+ if (first)
+ test_print_range(first, last, missing - 1, 0);
+ missing++;
+ first = i + 1;
+ last = i + 1;
+ }
+ }
+ }
+ if (first)
+ test_print_range(first, last, missing - 1, 0);
+ first = 0;
+ last = 0;
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_FAIL) {
+ if (missing && !failed)
+ fputs("; ", stdout);
+ if (failed == 0)
+ fputs("FAILED ", stdout);
+ if (first && i == last)
+ last = i + 1;
+ else {
+ if (first)
+ test_print_range(first, last, failed - 1, 0);
+ failed++;
+ first = i + 1;
+ last = i + 1;
+ }
+ }
+ }
+ if (first)
+ test_print_range(first, last, failed - 1, 0);
+ if (!missing && !failed) {
+ fputs(!status ? "ok" : "dubious", stdout);
+ if (ts->skipped > 0) {
+ if (ts->skipped == 1)
+ printf(" (skipped %lu test)", ts->skipped);
+ else
+ printf(" (skipped %lu tests)", ts->skipped);
+ }
+ }
+ }
+ if (status > 0)
+ printf(" (exit status %d)", status);
+ else if (status < 0)
+ printf(" (killed by signal %d%s)", -status,
+ WCOREDUMP(ts->status) ? ", core dumped" : "");
+ putchar('\n');
+}
+
+
+/*
+ * Given a test set, analyze the results, classify the exit status, handle a
+ * few special error messages, and then pass it along to test_summarize() for
+ * the regular output. Returns true if the test set ran successfully and all
+ * tests passed or were skipped, false otherwise.
+ */
+static int
+test_analyze(struct testset *ts)
+{
+ if (ts->reported)
+ return 0;
+ if (ts->all_skipped) {
+ if (ts->reason == NULL)
+ puts("skipped");
+ else
+ printf("skipped (%s)\n", ts->reason);
+ return 1;
+ } else if (WIFEXITED(ts->status) && WEXITSTATUS(ts->status) != 0) {
+ switch (WEXITSTATUS(ts->status)) {
+ case CHILDERR_DUP:
+ if (!ts->reported)
+ puts("ABORTED (can't dup file descriptors)");
+ break;
+ case CHILDERR_EXEC:
+ if (!ts->reported)
+ puts("ABORTED (execution failed -- not found?)");
+ break;
+ case CHILDERR_STDIN:
+ case CHILDERR_STDERR:
+ if (!ts->reported)
+ puts("ABORTED (can't open /dev/null)");
+ break;
+ default:
+ test_summarize(ts, WEXITSTATUS(ts->status));
+ break;
+ }
+ return 0;
+ } else if (WIFSIGNALED(ts->status)) {
+ test_summarize(ts, -WTERMSIG(ts->status));
+ return 0;
+ } else if (ts->plan != PLAN_FIRST && ts->plan != PLAN_FINAL) {
+ puts("ABORTED (no valid test plan)");
+ ts->aborted = 1;
+ return 0;
+ } else {
+ test_summarize(ts, 0);
+ return (ts->failed == 0);
+ }
+}
+
+
+/*
+ * Runs a single test set, accumulating and then reporting the results.
+ * Returns true if the test set was successfully run and all tests passed,
+ * false otherwise.
+ */
+static int
+test_run(struct testset *ts, enum test_verbose verbose)
+{
+ pid_t testpid, child;
+ int outfd, status;
+ unsigned long i;
+ FILE *output;
+ char buffer[BUFSIZ];
+
+ /* Run the test program. */
+ testpid = test_start(ts->command, &outfd);
+ output = fdopen(outfd, "r");
+ if (!output) {
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("fdopen failed");
+ }
+
+ /*
+ * Pass each line of output to test_checkline(), and print the line if
+ * verbosity is requested.
+ */
+ while (!ts->aborted && fgets(buffer, sizeof(buffer), output)) {
+ if (verbose)
+ printf("%s", buffer);
+ test_checkline(buffer, ts, verbose);
+ }
+ if (ferror(output) || ts->plan == PLAN_INIT)
+ ts->aborted = 1;
+ if (!verbose)
+ test_backspace(ts);
+
+ /*
+ * Consume the rest of the test output, close the output descriptor,
+ * retrieve the exit status, and pass that information to test_analyze()
+ * for eventual output.
+ */
+ while (fgets(buffer, sizeof(buffer), output))
+ if (verbose)
+ printf("%s", buffer);
+ fclose(output);
+ child = waitpid(testpid, &ts->status, 0);
+ if (child == (pid_t) -1) {
+ if (!ts->reported) {
+ puts("ABORTED");
+ fflush(stdout);
+ }
+ sysdie("waitpid for %u failed", (unsigned int) testpid);
+ }
+ if (ts->all_skipped)
+ ts->aborted = 0;
+ status = test_analyze(ts);
+
+ /* Convert missing tests to failed tests. */
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_INVALID) {
+ ts->failed++;
+ ts->results[i] = TEST_FAIL;
+ status = 0;
+ }
+ }
+ return status;
+}
+
+
+/* Summarize a list of test failures. */
+static void
+test_fail_summary(const struct testlist *fails)
+{
+ struct testset *ts;
+ unsigned int chars;
+ unsigned long i, first, last, total;
+ double failed;
+
+ puts(header);
+
+ /* Failed Set Fail/Total (%) Skip Stat Failing (25)
+ -------------------------- -------------- ---- ---- -------------- */
+ for (; fails; fails = fails->next) {
+ ts = fails->ts;
+ total = ts->count - ts->skipped;
+ failed = (double) ts->failed;
+ printf("%-26.26s %4lu/%-4lu %3.0f%% %4lu ", ts->file, ts->failed,
+ total, total ? (failed * 100.0) / (double) total : 0,
+ ts->skipped);
+ if (WIFEXITED(ts->status))
+ printf("%4d ", WEXITSTATUS(ts->status));
+ else
+ printf(" -- ");
+ if (ts->aborted) {
+ puts("aborted");
+ continue;
+ }
+ chars = 0;
+ first = 0;
+ last = 0;
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_FAIL) {
+ if (first != 0 && i == last)
+ last = i + 1;
+ else {
+ if (first != 0)
+ chars += test_print_range(first, last, chars, 19);
+ first = i + 1;
+ last = i + 1;
+ }
+ }
+ }
+ if (first != 0)
+ test_print_range(first, last, chars, 19);
+ putchar('\n');
+ }
+}
+
+
+/*
+ * Check whether a given file path is a valid test. Currently, this checks
+ * whether it is executable and is a regular file. Returns true or false.
+ */
+static int
+is_valid_test(const char *path)
+{
+ struct stat st;
+
+ if (access(path, X_OK) < 0)
+ return 0;
+ if (stat(path, &st) < 0)
+ return 0;
+ if (!S_ISREG(st.st_mode))
+ return 0;
+ return 1;
+}
+
+
+/*
+ * Given the name of a test, a pointer to the testset struct, and the source
+ * and build directories, find the test. We try first relative to the current
+ * directory, then in the build directory (if not NULL), then in the source
+ * directory. In each of those directories, we first try a "-t" extension and
+ * then a ".t" extension. When we find an executable program, we return the
+ * path to that program. If none of those paths are executable, just fill in
+ * the name of the test as is.
+ *
+ * The caller is responsible for freeing the path member of the testset
+ * struct.
+ */
+static char *
+find_test(const char *name, const char *source, const char *build)
+{
+ char *path = NULL;
+ const char *bases[3], *suffix, *base;
+ unsigned int i, j;
+ const char *suffixes[3] = {"-t", ".t", ""};
+
+ /* Possible base directories. */
+ bases[0] = ".";
+ bases[1] = build;
+ bases[2] = source;
+
+ /* Try each suffix with each base. */
+ for (i = 0; i < ARRAY_SIZE(suffixes); i++) {
+ suffix = suffixes[i];
+ for (j = 0; j < ARRAY_SIZE(bases); j++) {
+ base = bases[j];
+ if (base == NULL)
+ continue;
+ path = concat(base, "/", name, suffix, (const char *) 0);
+ if (is_valid_test(path))
+ return path;
+ free(path);
+ path = NULL;
+ }
+ }
+ if (path == NULL)
+ path = xstrdup(name);
+ return path;
+}
+
+
+/*
+ * Parse a single line of a test list and store the test name and command to
+ * execute it in the given testset struct.
+ *
+ * Normally, each line is just the name of the test, which is located in the
+ * test directory and turned into a command to run. However, each line may
+ * have whitespace-separated options, which change the command that's run.
+ * Current supported options are:
+ *
+ * valgrind
+ * Run the test under valgrind if C_TAP_VALGRIND is set. The contents
+ * of that environment variable are taken as the valgrind command (with
+ * options) to run. The command is parsed with a simple split on
+ * whitespace and no quoting is supported.
+ *
+ * libtool
+ * If running under valgrind, use libtool to invoke valgrind. This avoids
+ * running valgrind on the wrapper shell script generated by libtool. If
+ * set, C_TAP_LIBTOOL must be set to the full path to the libtool program
+ * to use to run valgrind and thus the test. Ignored if the test isn't
+ * being run under valgrind.
+ */
+static void
+parse_test_list_line(const char *line, struct testset *ts, const char *source,
+ const char *build)
+{
+ const char *p, *end, *option, *libtool;
+ const char *valgrind = NULL;
+ unsigned int use_libtool = 0;
+ unsigned int use_valgrind = 0;
+ size_t len, i;
+
+ /* Determine the name of the test. */
+ p = skip_non_whitespace(line);
+ ts->file = xstrndup(line, p - line);
+
+ /* Check if any test options are set. */
+ p = skip_whitespace(p);
+ while (*p != '\0') {
+ end = skip_non_whitespace(p);
+ if (strncmp(p, "libtool", end - p) == 0) {
+ use_libtool = 1;
+ } else if (strncmp(p, "valgrind", end - p) == 0) {
+ valgrind = getenv("C_TAP_VALGRIND");
+ use_valgrind = (valgrind != NULL);
+ } else {
+ option = xstrndup(p, end - p);
+ die("unknown test list option %s", option);
+ }
+ p = skip_whitespace(end);
+ }
+
+ /* Construct the argv to run the test. First, find the length. */
+ len = 1;
+ if (use_valgrind && valgrind != NULL) {
+ p = skip_whitespace(valgrind);
+ while (*p != '\0') {
+ len++;
+ p = skip_whitespace(skip_non_whitespace(p));
+ }
+ if (use_libtool)
+ len += 2;
+ }
+
+ /* Now, build the command. */
+ ts->command = xcalloc(len + 1, char *);
+ i = 0;
+ if (use_valgrind && valgrind != NULL) {
+ if (use_libtool) {
+ libtool = getenv("C_TAP_LIBTOOL");
+ if (libtool == NULL)
+ die("valgrind with libtool requested, but C_TAP_LIBTOOL is not"
+ " set");
+ ts->command[i++] = xstrdup(libtool);
+ ts->command[i++] = xstrdup("--mode=execute");
+ }
+ p = skip_whitespace(valgrind);
+ while (*p != '\0') {
+ end = skip_non_whitespace(p);
+ ts->command[i++] = xstrndup(p, end - p);
+ p = skip_whitespace(end);
+ }
+ }
+ if (i != len - 1)
+ die("internal error while constructing command line");
+ ts->command[i++] = find_test(ts->file, source, build);
+ ts->command[i] = NULL;
+}
+
+
+/*
+ * Read a list of tests from a file, returning the list of tests as a struct
+ * testlist, or NULL if there were no tests (such as a file containing only
+ * comments). Reports an error to standard error and exits if the list of
+ * tests cannot be read.
+ */
+static struct testlist *
+read_test_list(const char *filename, const char *source, const char *build)
+{
+ FILE *file;
+ unsigned int line;
+ size_t length;
+ char buffer[BUFSIZ];
+ const char *start;
+ struct testlist *listhead, *current;
+
+ /* Create the initial container list that will hold our results. */
+ listhead = xcalloc(1, struct testlist);
+ current = NULL;
+
+ /*
+ * Open our file of tests to run and read it line by line, creating a new
+ * struct testlist and struct testset for each line.
+ */
+ file = fopen(filename, "r");
+ if (file == NULL)
+ sysdie("can't open %s", filename);
+ line = 0;
+ while (fgets(buffer, sizeof(buffer), file)) {
+ line++;
+ length = strlen(buffer) - 1;
+ if (buffer[length] != '\n') {
+ fprintf(stderr, "%s:%u: line too long\n", filename, line);
+ exit(1);
+ }
+ buffer[length] = '\0';
+
+ /* Skip comments, leading spaces, and blank lines. */
+ start = skip_whitespace(buffer);
+ if (strlen(start) == 0)
+ continue;
+ if (start[0] == '#')
+ continue;
+
+ /* Allocate the new testset structure. */
+ if (current == NULL)
+ current = listhead;
+ else {
+ current->next = xcalloc(1, struct testlist);
+ current = current->next;
+ }
+ current->ts = xcalloc(1, struct testset);
+ current->ts->plan = PLAN_INIT;
+
+ /* Parse the line and store the results in the testset struct. */
+ parse_test_list_line(start, current->ts, source, build);
+ }
+ fclose(file);
+
+ /* If there were no tests, current is still NULL. */
+ if (current == NULL) {
+ free(listhead);
+ return NULL;
+ }
+
+ /* Return the results. */
+ return listhead;
+}
+
+
+/*
+ * Build a list of tests from command line arguments. Takes the argv and argc
+ * representing the command line arguments and returns a newly allocated test
+ * list, or NULL if there were no tests. The caller is responsible for
+ * freeing.
+ */
+static struct testlist *
+build_test_list(char *argv[], int argc, const char *source, const char *build)
+{
+ int i;
+ struct testlist *listhead, *current;
+
+ /* Create the initial container list that will hold our results. */
+ listhead = xcalloc(1, struct testlist);
+ current = NULL;
+
+ /* Walk the list of arguments and create test sets for them. */
+ for (i = 0; i < argc; i++) {
+ if (current == NULL)
+ current = listhead;
+ else {
+ current->next = xcalloc(1, struct testlist);
+ current = current->next;
+ }
+ current->ts = xcalloc(1, struct testset);
+ current->ts->plan = PLAN_INIT;
+ current->ts->file = xstrdup(argv[i]);
+ current->ts->command = xcalloc(2, char *);
+ current->ts->command[0] = find_test(current->ts->file, source, build);
+ current->ts->command[1] = NULL;
+ }
+
+ /* If there were no tests, current is still NULL. */
+ if (current == NULL) {
+ free(listhead);
+ return NULL;
+ }
+
+ /* Return the results. */
+ return listhead;
+}
+
+
+/* Free a struct testset. */
+static void
+free_testset(struct testset *ts)
+{
+ size_t i;
+
+ free(ts->file);
+ for (i = 0; ts->command[i] != NULL; i++)
+ free(ts->command[i]);
+ free(ts->command);
+ free(ts->results);
+ free(ts->reason);
+ free(ts);
+}
+
+
+/*
+ * Run a batch of tests. Takes two additional parameters: the root of the
+ * source directory and the root of the build directory. Test programs will
+ * be first searched for in the current directory, then the build directory,
+ * then the source directory. Returns true iff all tests passed, and always
+ * frees the test list that's passed in.
+ */
+static int
+test_batch(struct testlist *tests, enum test_verbose verbose)
+{
+ size_t length, i;
+ size_t longest = 0;
+ unsigned int count = 0;
+ struct testset *ts;
+ struct timeval start, end;
+ struct rusage stats;
+ struct testlist *failhead = NULL;
+ struct testlist *failtail = NULL;
+ struct testlist *current, *next;
+ int succeeded;
+ unsigned long total = 0;
+ unsigned long passed = 0;
+ unsigned long skipped = 0;
+ unsigned long failed = 0;
+ unsigned long aborted = 0;
+
+ /* Walk the list of tests to find the longest name. */
+ for (current = tests; current != NULL; current = current->next) {
+ length = strlen(current->ts->file);
+ if (length > longest)
+ longest = length;
+ }
+
+ /*
+ * Add two to longest and round up to the nearest tab stop. This is how
+ * wide the column for printing the current test name will be.
+ */
+ longest += 2;
+ if (longest % 8)
+ longest += 8 - (longest % 8);
+
+ /* Start the wall clock timer. */
+ gettimeofday(&start, NULL);
+
+ /* Now, plow through our tests again, running each one. */
+ for (current = tests; current != NULL; current = current->next) {
+ ts = current->ts;
+
+ /* Print out the name of the test file. */
+ fputs(ts->file, stdout);
+ if (verbose)
+ fputs("\n\n", stdout);
+ else
+ for (i = strlen(ts->file); i < longest; i++)
+ putchar('.');
+ if (isatty(STDOUT_FILENO))
+ fflush(stdout);
+
+ /* Run the test. */
+ succeeded = test_run(ts, verbose);
+ fflush(stdout);
+ if (verbose)
+ putchar('\n');
+
+ /* Record cumulative statistics. */
+ aborted += ts->aborted;
+ total += ts->count + ts->all_skipped;
+ passed += ts->passed;
+ skipped += ts->skipped + ts->all_skipped;
+ failed += ts->failed;
+ count++;
+
+ /* If the test fails, we shuffle it over to the fail list. */
+ if (!succeeded) {
+ if (failhead == NULL) {
+ failhead = xcalloc(1, struct testlist);
+ failtail = failhead;
+ } else {
+ failtail->next = xcalloc(1, struct testlist);
+ failtail = failtail->next;
+ }
+ failtail->ts = ts;
+ failtail->next = NULL;
+ }
+ }
+ total -= skipped;
+
+ /* Stop the timer and get our child resource statistics. */
+ gettimeofday(&end, NULL);
+ getrusage(RUSAGE_CHILDREN, &stats);
+
+ /* Summarize the failures and free the failure list. */
+ if (failhead != NULL) {
+ test_fail_summary(failhead);
+ while (failhead != NULL) {
+ next = failhead->next;
+ free(failhead);
+ failhead = next;
+ }
+ }
+
+ /* Free the memory used by the test lists. */
+ while (tests != NULL) {
+ next = tests->next;
+ free_testset(tests->ts);
+ free(tests);
+ tests = next;
+ }
+
+ /* Print out the final test summary. */
+ putchar('\n');
+ if (aborted != 0) {
+ if (aborted == 1)
+ printf("Aborted %lu test set", aborted);
+ else
+ printf("Aborted %lu test sets", aborted);
+ printf(", passed %lu/%lu tests", passed, total);
+ } else if (failed == 0)
+ fputs("All tests successful", stdout);
+ else
+ printf("Failed %lu/%lu tests, %.2f%% okay", failed, total,
+ (double) (total - failed) * 100.0 / (double) total);
+ if (skipped != 0) {
+ if (skipped == 1)
+ printf(", %lu test skipped", skipped);
+ else
+ printf(", %lu tests skipped", skipped);
+ }
+ puts(".");
+ printf("Files=%u, Tests=%lu", count, total);
+ printf(", %.2f seconds", tv_diff(&end, &start));
+ printf(" (%.2f usr + %.2f sys = %.2f CPU)\n", tv_seconds(&stats.ru_utime),
+ tv_seconds(&stats.ru_stime),
+ tv_sum(&stats.ru_utime, &stats.ru_stime));
+ return (failed == 0 && aborted == 0);
+}
+
+
+/*
+ * Run a single test case. This involves just running the test program after
+ * having done the environment setup and finding the test program.
+ */
+static void
+test_single(const char *program, const char *source, const char *build)
+{
+ char *path;
+
+ path = find_test(program, source, build);
+ if (execl(path, path, (char *) 0) == -1)
+ sysdie("cannot exec %s", path);
+}
+
+
+/*
+ * Main routine. Set the C_TAP_SOURCE, C_TAP_BUILD, SOURCE, and BUILD
+ * environment variables and then, given a file listing tests, run each test
+ * listed.
+ */
+int
+main(int argc, char *argv[])
+{
+ int option;
+ int status = 0;
+ int single = 0;
+ enum test_verbose verbose = CONCISE;
+ char *c_tap_source_env = NULL;
+ char *c_tap_build_env = NULL;
+ char *source_env = NULL;
+ char *build_env = NULL;
+ const char *program;
+ const char *shortlist;
+ const char *list = NULL;
+ const char *source = C_TAP_SOURCE;
+ const char *build = C_TAP_BUILD;
+ struct testlist *tests;
+
+ program = argv[0];
+ while ((option = getopt(argc, argv, "b:hl:os:v")) != EOF) {
+ switch (option) {
+ case 'b':
+ build = optarg;
+ break;
+ case 'h':
+ printf(usage_message, program, program, program, usage_extra);
+ exit(0);
+ case 'l':
+ list = optarg;
+ break;
+ case 'o':
+ single = 1;
+ break;
+ case 's':
+ source = optarg;
+ break;
+ case 'v':
+ verbose = VERBOSE;
+ break;
+ default:
+ exit(1);
+ }
+ }
+ argv += optind;
+ argc -= optind;
+ if ((list == NULL && argc < 1) || (list != NULL && argc > 0)) {
+ fprintf(stderr, usage_message, program, program, program, usage_extra);
+ exit(1);
+ }
+
+ /*
+ * If C_TAP_VERBOSE is set in the environment, that also turns on verbose
+ * mode.
+ */
+ if (getenv("C_TAP_VERBOSE") != NULL)
+ verbose = VERBOSE;
+
+ /*
+ * Set C_TAP_SOURCE and C_TAP_BUILD environment variables. Also set
+ * SOURCE and BUILD for backward compatibility, although we're trying to
+ * migrate to the ones with a C_TAP_* prefix.
+ */
+ if (source != NULL) {
+ c_tap_source_env = concat("C_TAP_SOURCE=", source, (const char *) 0);
+ if (putenv(c_tap_source_env) != 0)
+ sysdie("cannot set C_TAP_SOURCE in the environment");
+ source_env = concat("SOURCE=", source, (const char *) 0);
+ if (putenv(source_env) != 0)
+ sysdie("cannot set SOURCE in the environment");
+ }
+ if (build != NULL) {
+ c_tap_build_env = concat("C_TAP_BUILD=", build, (const char *) 0);
+ if (putenv(c_tap_build_env) != 0)
+ sysdie("cannot set C_TAP_BUILD in the environment");
+ build_env = concat("BUILD=", build, (const char *) 0);
+ if (putenv(build_env) != 0)
+ sysdie("cannot set BUILD in the environment");
+ }
+
+ /* Run the tests as instructed. */
+ if (single)
+ test_single(argv[0], source, build);
+ else if (list != NULL) {
+ shortlist = strrchr(list, '/');
+ if (shortlist == NULL)
+ shortlist = list;
+ else
+ shortlist++;
+ printf(banner, shortlist);
+ tests = read_test_list(list, source, build);
+ status = test_batch(tests, verbose) ? 0 : 1;
+ } else {
+ tests = build_test_list(argv, argc, source, build);
+ status = test_batch(tests, verbose) ? 0 : 1;
+ }
+
+ /* For valgrind cleanliness, free all our memory. */
+ if (source_env != NULL) {
+ putenv((char *) "C_TAP_SOURCE=");
+ putenv((char *) "SOURCE=");
+ free(c_tap_source_env);
+ free(source_env);
+ }
+ if (build_env != NULL) {
+ putenv((char *) "C_TAP_BUILD=");
+ putenv((char *) "BUILD=");
+ free(c_tap_build_env);
+ free(build_env);
+ }
+ exit(status);
+}
diff --git a/tests/style/obsolete-strings-t b/tests/style/obsolete-strings-t
new file mode 100755
index 000000000000..430f07219cef
--- /dev/null
+++ b/tests/style/obsolete-strings-t
@@ -0,0 +1,104 @@
+#!/usr/bin/perl
+#
+# Check for obsolete strings in source files.
+#
+# Examine all source files in a distribution for obsolete strings and report
+# on files that fail this check. This catches various transitions I want to
+# do globally in all my packages, like changing my personal URLs to https.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Copyright 2016, 2018-2020 Russ Allbery <eagle@eyrie.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA qw(skip_unless_author);
+use Test::RRA::Automake qw(all_files automake_setup);
+
+use File::Basename qw(basename);
+use Test::More;
+
+# Bad patterns to search for.
+my @BAD_REGEXES = (qr{ http:// \S+ [.]eyrie[.]org }xms);
+my @BAD_STRINGS = qw(rra@stanford.edu RRA_MAINTAINER_TESTS);
+
+# File names to exclude from this check.
+my %EXCLUDE
+ = map { $_ => 1 } qw(NEWS changelog obsolete-strings.t obsolete-strings-t);
+
+# Only run this test for the package author, since it doesn't indicate any
+# user-noticable flaw in the package itself.
+skip_unless_author('Obsolete strings tests');
+
+# Set up Automake testing.
+automake_setup();
+
+# Check a single file for one of the bad patterns.
+#
+# $path - Path to the file
+#
+# Returns: undef
+sub check_file {
+ my ($path) = @_;
+ my $filename = basename($path);
+
+ # Ignore excluded and binary files.
+ return if $EXCLUDE{$filename};
+ return if !-T $path;
+
+ # Scan the file.
+ open(my $fh, '<', $path) or BAIL_OUT("Cannot open $path");
+ while (defined(my $line = <$fh>)) {
+ for my $regex (@BAD_REGEXES) {
+ if ($line =~ $regex) {
+ ok(0, "$path contains $regex");
+ close($fh) or BAIL_OUT("Cannot close $path");
+ return;
+ }
+ }
+ for my $string (@BAD_STRINGS) {
+ if (index($line, $string) != -1) {
+ ok(0, "$path contains $string");
+ close($fh) or BAIL_OUT("Cannot close $path");
+ return;
+ }
+ }
+ }
+ close($fh) or BAIL_OUT("Cannot close $path");
+ ok(1, $path);
+ return;
+}
+
+# Scan every file for any of the bad patterns or strings. We don't declare a
+# plan since we skip a lot of files and don't want to precalculate the file
+# list.
+my @paths = all_files();
+for my $path (@paths) {
+ check_file($path);
+}
+done_testing();
diff --git a/tests/tap/basic.c b/tests/tap/basic.c
new file mode 100644
index 000000000000..b5f42d0211a4
--- /dev/null
+++ b/tests/tap/basic.c
@@ -0,0 +1,1029 @@
+/*
+ * Some utility routines for writing tests.
+ *
+ * Here are a variety of utility routines for writing tests compatible with
+ * the TAP protocol. All routines of the form ok() or is*() take a test
+ * number and some number of appropriate arguments, check to be sure the
+ * results match the expected output using the arguments, and print out
+ * something appropriate for that test number. Other utility routines help in
+ * constructing more complex tests, skipping tests, reporting errors, setting
+ * up the TAP output format, or finding things in the test environment.
+ *
+ * This file is part of C TAP Harness. The current version plus supporting
+ * documentation is at <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009-2019 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2001-2002, 2004-2008, 2011-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef _WIN32
+# include <direct.h>
+#else
+# include <sys/stat.h>
+#endif
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <tests/tap/basic.h>
+
+/* Windows provides mkdir and rmdir under different names. */
+#ifdef _WIN32
+# define mkdir(p, m) _mkdir(p)
+# define rmdir(p) _rmdir(p)
+#endif
+
+/*
+ * The test count. Always contains the number that will be used for the next
+ * test status. This is exported to callers of the library.
+ */
+unsigned long testnum = 1;
+
+/*
+ * Status information stored so that we can give a test summary at the end of
+ * the test case. We store the planned final test and the count of failures.
+ * We can get the highest test count from testnum.
+ */
+static unsigned long _planned = 0;
+static unsigned long _failed = 0;
+
+/*
+ * Store the PID of the process that called plan() and only summarize
+ * results when that process exits, so as to not misreport results in forked
+ * processes.
+ */
+static pid_t _process = 0;
+
+/*
+ * If true, we're doing lazy planning and will print out the plan based on the
+ * last test number at the end of testing.
+ */
+static int _lazy = 0;
+
+/*
+ * If true, the test was aborted by calling bail(). Currently, this is only
+ * used to ensure that we pass a false value to any cleanup functions even if
+ * all tests to that point have passed.
+ */
+static int _aborted = 0;
+
+/*
+ * Registered cleanup functions. These are stored as a linked list and run in
+ * registered order by finish when the test program exits. Each function is
+ * passed a boolean value indicating whether all tests were successful.
+ */
+struct cleanup_func {
+ test_cleanup_func func;
+ test_cleanup_func_with_data func_with_data;
+ void *data;
+ struct cleanup_func *next;
+};
+static struct cleanup_func *cleanup_funcs = NULL;
+
+/*
+ * Registered diag files. Any output found in these files will be printed out
+ * as if it were passed to diag() before any other output we do. This allows
+ * background processes to log to a file and have that output interleaved with
+ * the test output.
+ */
+struct diag_file {
+ char *name;
+ FILE *file;
+ char *buffer;
+ size_t bufsize;
+ struct diag_file *next;
+};
+static struct diag_file *diag_files = NULL;
+
+/*
+ * Print a specified prefix and then the test description. Handles turning
+ * the argument list into a va_args structure suitable for passing to
+ * print_desc, which has to be done in a macro. Assumes that format is the
+ * argument immediately before the variadic arguments.
+ */
+#define PRINT_DESC(prefix, format) \
+ do { \
+ if (format != NULL) { \
+ va_list args; \
+ printf("%s", prefix); \
+ va_start(args, format); \
+ vprintf(format, args); \
+ va_end(args); \
+ } \
+ } while (0)
+
+
+/*
+ * Form a new string by concatenating multiple strings. The arguments must be
+ * terminated by (const char *) 0.
+ *
+ * This function only exists because we can't assume asprintf. We can't
+ * simulate asprintf with snprintf because we're only assuming SUSv3, which
+ * does not require that snprintf with a NULL buffer return the required
+ * length. When those constraints are relaxed, this should be ripped out and
+ * replaced with asprintf or a more trivial replacement with snprintf.
+ */
+static char *
+concat(const char *first, ...)
+{
+ va_list args;
+ char *result;
+ const char *string;
+ size_t offset;
+ size_t length = 0;
+
+ /*
+ * Find the total memory required. Ensure we don't overflow length. See
+ * the comment for breallocarray for why we're using UINT_MAX here.
+ */
+ va_start(args, first);
+ for (string = first; string != NULL; string = va_arg(args, const char *)) {
+ if (length >= UINT_MAX - strlen(string))
+ bail("strings too long in concat");
+ length += strlen(string);
+ }
+ va_end(args);
+ length++;
+
+ /* Create the string. */
+ result = bcalloc_type(length, char);
+ va_start(args, first);
+ offset = 0;
+ for (string = first; string != NULL; string = va_arg(args, const char *)) {
+ memcpy(result + offset, string, strlen(string));
+ offset += strlen(string);
+ }
+ va_end(args);
+ result[offset] = '\0';
+ return result;
+}
+
+
+/*
+ * Helper function for check_diag_files to handle a single line in a diag
+ * file.
+ *
+ * The general scheme here used is as follows: read one line of output. If we
+ * get NULL, check for an error. If there was one, bail out of the test
+ * program; otherwise, return, and the enclosing loop will check for EOF.
+ *
+ * If we get some data, see if it ends in a newline. If it doesn't end in a
+ * newline, we have one of two cases: our buffer isn't large enough, in which
+ * case we resize it and try again, or we have incomplete data in the file, in
+ * which case we rewind the file and will try again next time.
+ *
+ * Returns a boolean indicating whether the last line was incomplete.
+ */
+static int
+handle_diag_file_line(struct diag_file *file, fpos_t where)
+{
+ int size;
+ size_t length;
+
+ /* Read the next line from the file. */
+ size = file->bufsize > INT_MAX ? INT_MAX : (int) file->bufsize;
+ if (fgets(file->buffer, size, file->file) == NULL) {
+ if (ferror(file->file))
+ sysbail("cannot read from %s", file->name);
+ return 0;
+ }
+
+ /*
+ * See if the line ends in a newline. If not, see which error case we
+ * have.
+ */
+ length = strlen(file->buffer);
+ if (file->buffer[length - 1] != '\n') {
+ int incomplete = 0;
+
+ /* Check whether we ran out of buffer space and resize if so. */
+ if (length < file->bufsize - 1)
+ incomplete = 1;
+ else {
+ file->bufsize += BUFSIZ;
+ file->buffer =
+ breallocarray_type(file->buffer, file->bufsize, char);
+ }
+
+ /*
+ * On either incomplete lines or too small of a buffer, rewind
+ * and read the file again (on the next pass, if incomplete).
+ * It's simpler than trying to double-buffer the file.
+ */
+ if (fsetpos(file->file, &where) < 0)
+ sysbail("cannot set position in %s", file->name);
+ return incomplete;
+ }
+
+ /* We saw a complete line. Print it out. */
+ printf("# %s", file->buffer);
+ return 0;
+}
+
+
+/*
+ * Check all registered diag_files for any output. We only print out the
+ * output if we see a complete line; otherwise, we wait for the next newline.
+ */
+static void
+check_diag_files(void)
+{
+ struct diag_file *file;
+ fpos_t where;
+ int incomplete;
+
+ /*
+ * Walk through each file and read each line of output available.
+ */
+ for (file = diag_files; file != NULL; file = file->next) {
+ clearerr(file->file);
+
+ /* Store the current position in case we have to rewind. */
+ if (fgetpos(file->file, &where) < 0)
+ sysbail("cannot get position in %s", file->name);
+
+ /* Continue until we get EOF or an incomplete line of data. */
+ incomplete = 0;
+ while (!feof(file->file) && !incomplete) {
+ incomplete = handle_diag_file_line(file, where);
+ }
+ }
+}
+
+
+/*
+ * Our exit handler. Called on completion of the test to report a summary of
+ * results provided we're still in the original process. This also handles
+ * printing out the plan if we used plan_lazy(), although that's suppressed if
+ * we never ran a test (due to an early bail, for example), and running any
+ * registered cleanup functions.
+ */
+static void
+finish(void)
+{
+ int success, primary;
+ struct cleanup_func *current;
+ unsigned long highest = testnum - 1;
+ struct diag_file *file, *tmp;
+
+ /* Check for pending diag_file output. */
+ check_diag_files();
+
+ /* Free the diag_files. */
+ file = diag_files;
+ while (file != NULL) {
+ tmp = file;
+ file = file->next;
+ fclose(tmp->file);
+ free(tmp->name);
+ free(tmp->buffer);
+ free(tmp);
+ }
+ diag_files = NULL;
+
+ /*
+ * Determine whether all tests were successful, which is needed before
+ * calling cleanup functions since we pass that fact to the functions.
+ */
+ if (_planned == 0 && _lazy)
+ _planned = highest;
+ success = (!_aborted && _planned == highest && _failed == 0);
+
+ /*
+ * If there are any registered cleanup functions, we run those first. We
+ * always run them, even if we didn't run a test. Don't do anything
+ * except free the diag_files and call cleanup functions if we aren't the
+ * primary process (the process in which plan or plan_lazy was called),
+ * and tell the cleanup functions that fact.
+ */
+ primary = (_process == 0 || getpid() == _process);
+ while (cleanup_funcs != NULL) {
+ if (cleanup_funcs->func_with_data) {
+ void *data = cleanup_funcs->data;
+
+ cleanup_funcs->func_with_data(success, primary, data);
+ } else {
+ cleanup_funcs->func(success, primary);
+ }
+ current = cleanup_funcs;
+ cleanup_funcs = cleanup_funcs->next;
+ free(current);
+ }
+ if (!primary)
+ return;
+
+ /* Don't do anything further if we never planned a test. */
+ if (_planned == 0)
+ return;
+
+ /* If we're aborting due to bail, don't print summaries. */
+ if (_aborted)
+ return;
+
+ /* Print out the lazy plan if needed. */
+ fflush(stderr);
+ if (_lazy && _planned > 0)
+ printf("1..%lu\n", _planned);
+
+ /* Print out a summary of the results. */
+ if (_planned > highest)
+ diag("Looks like you planned %lu test%s but only ran %lu", _planned,
+ (_planned > 1 ? "s" : ""), highest);
+ else if (_planned < highest)
+ diag("Looks like you planned %lu test%s but ran %lu extra", _planned,
+ (_planned > 1 ? "s" : ""), highest - _planned);
+ else if (_failed > 0)
+ diag("Looks like you failed %lu test%s of %lu", _failed,
+ (_failed > 1 ? "s" : ""), _planned);
+ else if (_planned != 1)
+ diag("All %lu tests successful or skipped", _planned);
+ else
+ diag("%lu test successful or skipped", _planned);
+}
+
+
+/*
+ * Initialize things. Turns on line buffering on stdout and then prints out
+ * the number of tests in the test suite. We intentionally don't check for
+ * pending diag_file output here, since it should really come after the plan.
+ */
+void
+plan(unsigned long count)
+{
+ if (setvbuf(stdout, NULL, _IOLBF, BUFSIZ) != 0)
+ sysdiag("cannot set stdout to line buffered");
+ fflush(stderr);
+ printf("1..%lu\n", count);
+ testnum = 1;
+ _planned = count;
+ _process = getpid();
+ if (atexit(finish) != 0) {
+ sysdiag("cannot register exit handler");
+ diag("cleanups will not be run");
+ }
+}
+
+
+/*
+ * Initialize things for lazy planning, where we'll automatically print out a
+ * plan at the end of the program. Turns on line buffering on stdout as well.
+ */
+void
+plan_lazy(void)
+{
+ if (setvbuf(stdout, NULL, _IOLBF, BUFSIZ) != 0)
+ sysdiag("cannot set stdout to line buffered");
+ testnum = 1;
+ _process = getpid();
+ _lazy = 1;
+ if (atexit(finish) != 0)
+ sysbail("cannot register exit handler to display plan");
+}
+
+
+/*
+ * Skip the entire test suite and exits. Should be called instead of plan(),
+ * not after it, since it prints out a special plan line. Ignore diag_file
+ * output here, since it's not clear if it's allowed before the plan.
+ */
+void
+skip_all(const char *format, ...)
+{
+ fflush(stderr);
+ printf("1..0 # skip");
+ PRINT_DESC(" ", format);
+ putchar('\n');
+ exit(0);
+}
+
+
+/*
+ * Takes a boolean success value and assumes the test passes if that value
+ * is true and fails if that value is false.
+ */
+int
+ok(int success, const char *format, ...)
+{
+ fflush(stderr);
+ check_diag_files();
+ printf("%sok %lu", success ? "" : "not ", testnum++);
+ if (!success)
+ _failed++;
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Same as ok(), but takes the format arguments as a va_list.
+ */
+int
+okv(int success, const char *format, va_list args)
+{
+ fflush(stderr);
+ check_diag_files();
+ printf("%sok %lu", success ? "" : "not ", testnum++);
+ if (!success)
+ _failed++;
+ if (format != NULL) {
+ printf(" - ");
+ vprintf(format, args);
+ }
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Skip a test.
+ */
+void
+skip(const char *reason, ...)
+{
+ fflush(stderr);
+ check_diag_files();
+ printf("ok %lu # skip", testnum++);
+ PRINT_DESC(" ", reason);
+ putchar('\n');
+}
+
+
+/*
+ * Report the same status on the next count tests.
+ */
+int
+ok_block(unsigned long count, int success, const char *format, ...)
+{
+ unsigned long i;
+
+ fflush(stderr);
+ check_diag_files();
+ for (i = 0; i < count; i++) {
+ printf("%sok %lu", success ? "" : "not ", testnum++);
+ if (!success)
+ _failed++;
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ }
+ return success;
+}
+
+
+/*
+ * Skip the next count tests.
+ */
+void
+skip_block(unsigned long count, const char *reason, ...)
+{
+ unsigned long i;
+
+ fflush(stderr);
+ check_diag_files();
+ for (i = 0; i < count; i++) {
+ printf("ok %lu # skip", testnum++);
+ PRINT_DESC(" ", reason);
+ putchar('\n');
+ }
+}
+
+
+/*
+ * Takes two boolean values and requires the truth value of both match.
+ */
+int
+is_bool(int left, int right, const char *format, ...)
+{
+ int success;
+
+ fflush(stderr);
+ check_diag_files();
+ success = (!!left == !!right);
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ diag(" left: %s", !!left ? "true" : "false");
+ diag("right: %s", !!right ? "true" : "false");
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Takes two integer values and requires they match.
+ */
+int
+is_int(long left, long right, const char *format, ...)
+{
+ int success;
+
+ fflush(stderr);
+ check_diag_files();
+ success = (left == right);
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ diag(" left: %ld", left);
+ diag("right: %ld", right);
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Takes two strings and requires they match (using strcmp). NULL arguments
+ * are permitted and handled correctly.
+ */
+int
+is_string(const char *left, const char *right, const char *format, ...)
+{
+ int success;
+
+ fflush(stderr);
+ check_diag_files();
+
+ /* Compare the strings, being careful of NULL. */
+ if (left == NULL)
+ success = (right == NULL);
+ else if (right == NULL)
+ success = 0;
+ else
+ success = (strcmp(left, right) == 0);
+
+ /* Report the results. */
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ diag(" left: %s", left == NULL ? "(null)" : left);
+ diag("right: %s", right == NULL ? "(null)" : right);
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Takes two unsigned longs and requires they match. On failure, reports them
+ * in hex.
+ */
+int
+is_hex(unsigned long left, unsigned long right, const char *format, ...)
+{
+ int success;
+
+ fflush(stderr);
+ check_diag_files();
+ success = (left == right);
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ diag(" left: %lx", (unsigned long) left);
+ diag("right: %lx", (unsigned long) right);
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Takes pointers to a regions of memory and requires that len bytes from each
+ * match. Otherwise reports any bytes which didn't match.
+ */
+int
+is_blob(const void *left, const void *right, size_t len, const char *format,
+ ...)
+{
+ int success;
+ size_t i;
+
+ fflush(stderr);
+ check_diag_files();
+ success = (memcmp(left, right, len) == 0);
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ const unsigned char *left_c = (const unsigned char *) left;
+ const unsigned char *right_c = (const unsigned char *) right;
+
+ for (i = 0; i < len; i++) {
+ if (left_c[i] != right_c[i])
+ diag("offset %lu: left %02x, right %02x", (unsigned long) i,
+ left_c[i], right_c[i]);
+ }
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Bail out with an error.
+ */
+void
+bail(const char *format, ...)
+{
+ va_list args;
+
+ _aborted = 1;
+ fflush(stderr);
+ check_diag_files();
+ fflush(stdout);
+ printf("Bail out! ");
+ va_start(args, format);
+ vprintf(format, args);
+ va_end(args);
+ printf("\n");
+ exit(255);
+}
+
+
+/*
+ * Bail out with an error, appending strerror(errno).
+ */
+void
+sysbail(const char *format, ...)
+{
+ va_list args;
+ int oerrno = errno;
+
+ _aborted = 1;
+ fflush(stderr);
+ check_diag_files();
+ fflush(stdout);
+ printf("Bail out! ");
+ va_start(args, format);
+ vprintf(format, args);
+ va_end(args);
+ printf(": %s\n", strerror(oerrno));
+ exit(255);
+}
+
+
+/*
+ * Report a diagnostic to stderr. Always returns 1 to allow embedding in
+ * compound statements.
+ */
+int
+diag(const char *format, ...)
+{
+ va_list args;
+
+ fflush(stderr);
+ check_diag_files();
+ fflush(stdout);
+ printf("# ");
+ va_start(args, format);
+ vprintf(format, args);
+ va_end(args);
+ printf("\n");
+ return 1;
+}
+
+
+/*
+ * Report a diagnostic to stderr, appending strerror(errno). Always returns 1
+ * to allow embedding in compound statements.
+ */
+int
+sysdiag(const char *format, ...)
+{
+ va_list args;
+ int oerrno = errno;
+
+ fflush(stderr);
+ check_diag_files();
+ fflush(stdout);
+ printf("# ");
+ va_start(args, format);
+ vprintf(format, args);
+ va_end(args);
+ printf(": %s\n", strerror(oerrno));
+ return 1;
+}
+
+
+/*
+ * Register a new file for diag_file processing.
+ */
+void
+diag_file_add(const char *name)
+{
+ struct diag_file *file, *prev;
+
+ file = bcalloc_type(1, struct diag_file);
+ file->name = bstrdup(name);
+ file->file = fopen(file->name, "r");
+ if (file->file == NULL)
+ sysbail("cannot open %s", name);
+ file->buffer = bcalloc_type(BUFSIZ, char);
+ file->bufsize = BUFSIZ;
+ if (diag_files == NULL)
+ diag_files = file;
+ else {
+ for (prev = diag_files; prev->next != NULL; prev = prev->next)
+ ;
+ prev->next = file;
+ }
+}
+
+
+/*
+ * Remove a file from diag_file processing. If the file is not found, do
+ * nothing, since there are some situations where it can be removed twice
+ * (such as if it's removed from a cleanup function, since cleanup functions
+ * are called after freeing all the diag_files).
+ */
+void
+diag_file_remove(const char *name)
+{
+ struct diag_file *file;
+ struct diag_file **prev = &diag_files;
+
+ for (file = diag_files; file != NULL; file = file->next) {
+ if (strcmp(file->name, name) == 0) {
+ *prev = file->next;
+ fclose(file->file);
+ free(file->name);
+ free(file->buffer);
+ free(file);
+ return;
+ }
+ prev = &file->next;
+ }
+}
+
+
+/*
+ * Allocate cleared memory, reporting a fatal error with bail on failure.
+ */
+void *
+bcalloc(size_t n, size_t size)
+{
+ void *p;
+
+ p = calloc(n, size);
+ if (p == NULL)
+ sysbail("failed to calloc %lu", (unsigned long) (n * size));
+ return p;
+}
+
+
+/*
+ * Allocate memory, reporting a fatal error with bail on failure.
+ */
+void *
+bmalloc(size_t size)
+{
+ void *p;
+
+ p = malloc(size);
+ if (p == NULL)
+ sysbail("failed to malloc %lu", (unsigned long) size);
+ return p;
+}
+
+
+/*
+ * Reallocate memory, reporting a fatal error with bail on failure.
+ */
+void *
+brealloc(void *p, size_t size)
+{
+ p = realloc(p, size);
+ if (p == NULL)
+ sysbail("failed to realloc %lu bytes", (unsigned long) size);
+ return p;
+}
+
+
+/*
+ * The same as brealloc, but determine the size by multiplying an element
+ * count by a size, similar to calloc. The multiplication is checked for
+ * integer overflow.
+ *
+ * We should technically use SIZE_MAX here for the overflow check, but
+ * SIZE_MAX is C99 and we're only assuming C89 + SUSv3, which does not
+ * guarantee that it exists. They do guarantee that UINT_MAX exists, and we
+ * can assume that UINT_MAX <= SIZE_MAX.
+ *
+ * (In theory, C89 and C99 permit size_t to be smaller than unsigned int, but
+ * I disbelieve in the existence of such systems and they will have to cope
+ * without overflow checks.)
+ */
+void *
+breallocarray(void *p, size_t n, size_t size)
+{
+ if (n > 0 && UINT_MAX / n <= size)
+ bail("reallocarray too large");
+ if (n == 0)
+ n = 1;
+ p = realloc(p, n * size);
+ if (p == NULL)
+ sysbail("failed to realloc %lu bytes", (unsigned long) (n * size));
+ return p;
+}
+
+
+/*
+ * Copy a string, reporting a fatal error with bail on failure.
+ */
+char *
+bstrdup(const char *s)
+{
+ char *p;
+ size_t len;
+
+ len = strlen(s) + 1;
+ p = (char *) malloc(len);
+ if (p == NULL)
+ sysbail("failed to strdup %lu bytes", (unsigned long) len);
+ memcpy(p, s, len);
+ return p;
+}
+
+
+/*
+ * Copy up to n characters of a string, reporting a fatal error with bail on
+ * failure. Don't use the system strndup function, since it may not exist and
+ * the TAP library doesn't assume any portability support.
+ */
+char *
+bstrndup(const char *s, size_t n)
+{
+ const char *p;
+ char *copy;
+ size_t length;
+
+ /* Don't assume that the source string is nul-terminated. */
+ for (p = s; (size_t)(p - s) < n && *p != '\0'; p++)
+ ;
+ length = (size_t)(p - s);
+ copy = (char *) malloc(length + 1);
+ if (copy == NULL)
+ sysbail("failed to strndup %lu bytes", (unsigned long) length);
+ memcpy(copy, s, length);
+ copy[length] = '\0';
+ return copy;
+}
+
+
+/*
+ * Locate a test file. Given the partial path to a file, look under
+ * C_TAP_BUILD and then C_TAP_SOURCE for the file and return the full path to
+ * the file. Returns NULL if the file doesn't exist. A non-NULL return
+ * should be freed with test_file_path_free().
+ */
+char *
+test_file_path(const char *file)
+{
+ char *base;
+ char *path = NULL;
+ const char *envs[] = {"C_TAP_BUILD", "C_TAP_SOURCE", NULL};
+ int i;
+
+ for (i = 0; envs[i] != NULL; i++) {
+ base = getenv(envs[i]);
+ if (base == NULL)
+ continue;
+ path = concat(base, "/", file, (const char *) 0);
+ if (access(path, R_OK) == 0)
+ break;
+ free(path);
+ path = NULL;
+ }
+ return path;
+}
+
+
+/*
+ * Free a path returned from test_file_path(). This function exists primarily
+ * for Windows, where memory must be freed from the same library domain that
+ * it was allocated from.
+ */
+void
+test_file_path_free(char *path)
+{
+ free(path);
+}
+
+
+/*
+ * Create a temporary directory, tmp, under C_TAP_BUILD if set and the current
+ * directory if it does not. Returns the path to the temporary directory in
+ * newly allocated memory, and calls bail on any failure. The return value
+ * should be freed with test_tmpdir_free.
+ *
+ * This function uses sprintf because it attempts to be independent of all
+ * other portability layers. The use immediately after a memory allocation
+ * should be safe without using snprintf or strlcpy/strlcat.
+ */
+char *
+test_tmpdir(void)
+{
+ const char *build;
+ char *path = NULL;
+
+ build = getenv("C_TAP_BUILD");
+ if (build == NULL)
+ build = ".";
+ path = concat(build, "/tmp", (const char *) 0);
+ if (access(path, X_OK) < 0)
+ if (mkdir(path, 0777) < 0)
+ sysbail("error creating temporary directory %s", path);
+ return path;
+}
+
+
+/*
+ * Free a path returned from test_tmpdir() and attempt to remove the
+ * directory. If we can't delete the directory, don't worry; something else
+ * that hasn't yet cleaned up may still be using it.
+ */
+void
+test_tmpdir_free(char *path)
+{
+ if (path != NULL)
+ rmdir(path);
+ free(path);
+}
+
+static void
+register_cleanup(test_cleanup_func func,
+ test_cleanup_func_with_data func_with_data, void *data)
+{
+ struct cleanup_func *cleanup, **last;
+
+ cleanup = bcalloc_type(1, struct cleanup_func);
+ cleanup->func = func;
+ cleanup->func_with_data = func_with_data;
+ cleanup->data = data;
+ cleanup->next = NULL;
+ last = &cleanup_funcs;
+ while (*last != NULL)
+ last = &(*last)->next;
+ *last = cleanup;
+}
+
+/*
+ * Register a cleanup function that is called when testing ends. All such
+ * registered functions will be run by finish.
+ */
+void
+test_cleanup_register(test_cleanup_func func)
+{
+ register_cleanup(func, NULL, NULL);
+}
+
+/*
+ * Same as above, but also allows an opaque pointer to be passed to the cleanup
+ * function.
+ */
+void
+test_cleanup_register_with_data(test_cleanup_func_with_data func, void *data)
+{
+ register_cleanup(NULL, func, data);
+}
diff --git a/tests/tap/basic.h b/tests/tap/basic.h
new file mode 100644
index 000000000000..45f15f2892a7
--- /dev/null
+++ b/tests/tap/basic.h
@@ -0,0 +1,192 @@
+/*
+ * Basic utility routines for the TAP protocol.
+ *
+ * This file is part of C TAP Harness. The current version plus supporting
+ * documentation is at <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009-2019 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2001-2002, 2004-2008, 2011-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_BASIC_H
+#define TAP_BASIC_H 1
+
+#include <stdarg.h> /* va_list */
+#include <stddef.h> /* size_t */
+#include <tests/tap/macros.h>
+
+/*
+ * Used for iterating through arrays. ARRAY_SIZE returns the number of
+ * elements in the array (useful for a < upper bound in a for loop) and
+ * ARRAY_END returns a pointer to the element past the end (ISO C99 makes it
+ * legal to refer to such a pointer as long as it's never dereferenced).
+ */
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+#define ARRAY_END(array) (&(array)[ARRAY_SIZE(array)])
+
+BEGIN_DECLS
+
+/*
+ * The test count. Always contains the number that will be used for the next
+ * test status.
+ */
+extern unsigned long testnum;
+
+/* Print out the number of tests and set standard output to line buffered. */
+void plan(unsigned long count);
+
+/*
+ * Prepare for lazy planning, in which the plan will be printed automatically
+ * at the end of the test program.
+ */
+void plan_lazy(void);
+
+/* Skip the entire test suite. Call instead of plan. */
+void skip_all(const char *format, ...)
+ __attribute__((__noreturn__, __format__(printf, 1, 2)));
+
+/*
+ * Basic reporting functions. The okv() function is the same as ok() but
+ * takes the test description as a va_list to make it easier to reuse the
+ * reporting infrastructure when writing new tests. ok() and okv() return the
+ * value of the success argument.
+ */
+int ok(int success, const char *format, ...)
+ __attribute__((__format__(printf, 2, 3)));
+int okv(int success, const char *format, va_list args)
+ __attribute__((__format__(printf, 2, 0)));
+void skip(const char *reason, ...) __attribute__((__format__(printf, 1, 2)));
+
+/*
+ * Report the same status on, or skip, the next count tests. ok_block()
+ * returns the value of the success argument.
+ */
+int ok_block(unsigned long count, int success, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+void skip_block(unsigned long count, const char *reason, ...)
+ __attribute__((__format__(printf, 2, 3)));
+
+/*
+ * Compare two values. Returns true if the test passes and false if it fails.
+ * is_bool takes an int since the bool type isn't fully portable yet, but
+ * interprets both arguments for their truth value, not for their numeric
+ * value.
+ */
+int is_bool(int, int, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+int is_int(long, long, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+int is_string(const char *, const char *, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+int is_hex(unsigned long, unsigned long, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+int is_blob(const void *, const void *, size_t, const char *format, ...)
+ __attribute__((__format__(printf, 4, 5)));
+
+/* Bail out with an error. sysbail appends strerror(errno). */
+void bail(const char *format, ...)
+ __attribute__((__noreturn__, __nonnull__, __format__(printf, 1, 2)));
+void sysbail(const char *format, ...)
+ __attribute__((__noreturn__, __nonnull__, __format__(printf, 1, 2)));
+
+/* Report a diagnostic to stderr prefixed with #. */
+int diag(const char *format, ...)
+ __attribute__((__nonnull__, __format__(printf, 1, 2)));
+int sysdiag(const char *format, ...)
+ __attribute__((__nonnull__, __format__(printf, 1, 2)));
+
+/*
+ * Register or unregister a file that contains supplementary diagnostics.
+ * Before any other output, all registered files will be read, line by line,
+ * and each line will be reported as a diagnostic as if it were passed to
+ * diag(). Nul characters are not supported in these files and will result in
+ * truncated output.
+ */
+void diag_file_add(const char *file) __attribute__((__nonnull__));
+void diag_file_remove(const char *file) __attribute__((__nonnull__));
+
+/* Allocate memory, reporting a fatal error with bail on failure. */
+void *bcalloc(size_t, size_t)
+ __attribute__((__alloc_size__(1, 2), __malloc__, __warn_unused_result__));
+void *bmalloc(size_t)
+ __attribute__((__alloc_size__(1), __malloc__, __warn_unused_result__));
+void *breallocarray(void *, size_t, size_t)
+ __attribute__((__alloc_size__(2, 3), __malloc__, __warn_unused_result__));
+void *brealloc(void *, size_t)
+ __attribute__((__alloc_size__(2), __malloc__, __warn_unused_result__));
+char *bstrdup(const char *)
+ __attribute__((__malloc__, __nonnull__, __warn_unused_result__));
+char *bstrndup(const char *, size_t)
+ __attribute__((__malloc__, __nonnull__, __warn_unused_result__));
+
+/*
+ * Macros that cast the return value from b* memory functions, making them
+ * usable in C++ code and providing some additional type safety.
+ */
+#define bcalloc_type(n, type) ((type *) bcalloc((n), sizeof(type)))
+#define breallocarray_type(p, n, type) \
+ ((type *) breallocarray((p), (n), sizeof(type)))
+
+/*
+ * Find a test file under C_TAP_BUILD or C_TAP_SOURCE, returning the full
+ * path. The returned path should be freed with test_file_path_free().
+ */
+char *test_file_path(const char *file)
+ __attribute__((__malloc__, __nonnull__, __warn_unused_result__));
+void test_file_path_free(char *path);
+
+/*
+ * Create a temporary directory relative to C_TAP_BUILD and return the path.
+ * The returned path should be freed with test_tmpdir_free().
+ */
+char *test_tmpdir(void) __attribute__((__malloc__, __warn_unused_result__));
+void test_tmpdir_free(char *path);
+
+/*
+ * Register a cleanup function that is called when testing ends. All such
+ * registered functions will be run during atexit handling (and are therefore
+ * subject to all the same constraints and caveats as atexit functions).
+ *
+ * The function must return void and will be passed two arguments: an int that
+ * will be true if the test completed successfully and false otherwise, and an
+ * int that will be true if the cleanup function is run in the primary process
+ * (the one that called plan or plan_lazy) and false otherwise. If
+ * test_cleanup_register_with_data is used instead, a generic pointer can be
+ * provided and will be passed to the cleanup function as a third argument.
+ *
+ * test_cleanup_register_with_data is the better API and should have been the
+ * only API. test_cleanup_register was an API error preserved for backward
+ * cmpatibility.
+ */
+typedef void (*test_cleanup_func)(int, int);
+typedef void (*test_cleanup_func_with_data)(int, int, void *);
+
+void test_cleanup_register(test_cleanup_func) __attribute__((__nonnull__));
+void test_cleanup_register_with_data(test_cleanup_func_with_data, void *)
+ __attribute__((__nonnull__));
+
+END_DECLS
+
+#endif /* TAP_BASIC_H */
diff --git a/tests/tap/kadmin.c b/tests/tap/kadmin.c
new file mode 100644
index 000000000000..8e70f9d0ec27
--- /dev/null
+++ b/tests/tap/kadmin.c
@@ -0,0 +1,138 @@
+/*
+ * Kerberos test setup requiring the kadmin API.
+ *
+ * This file collects Kerberos test setup functions that use the kadmin API to
+ * put principals into particular configurations for testing. Currently, the
+ * only implemented functionality is to mark a password as expired.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#ifdef HAVE_KADM5CLNT
+# include <portable/kadmin.h>
+# include <portable/krb5.h>
+#endif
+#include <portable/system.h>
+
+#include <time.h>
+
+#include <tests/tap/basic.h>
+#include <tests/tap/kadmin.h>
+#include <tests/tap/kerberos.h>
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+
+/*
+ * Given the principal to set an expiration on, set that principal to have an
+ * expired password. This requires that the realm admin server be configured
+ * either in DNS (with SRV records) or in krb5.conf (possibly the one
+ * KRB5_CONFIG is pointing to). Authentication is done using the keytab
+ * stored in config/admin-keytab.
+ *
+ * Returns true on success. Returns false if necessary configuration is
+ * missing so that the caller can choose whether to call bail or skip_all. If
+ * the configuration is present but the operation fails, bails.
+ */
+#ifdef HAVE_KADM5CLNT
+bool
+kerberos_expire_password(const char *principal, time_t expires)
+{
+ char *path, *user;
+ const char *realm;
+ krb5_context ctx;
+ krb5_principal admin = NULL;
+ krb5_principal princ = NULL;
+ kadm5_ret_t code;
+ kadm5_config_params params;
+ kadm5_principal_ent_rec ent;
+ void *handle;
+ bool okay = false;
+
+ /* Set up for making our call. */
+ path = test_file_path("config/admin-keytab");
+ if (path == NULL)
+ return false;
+ code = krb5_init_context(&ctx);
+ if (code != 0)
+ bail_krb5(ctx, code, "error initializing Kerberos");
+ admin = kerberos_keytab_principal(ctx, path);
+ realm = krb5_principal_get_realm(ctx, admin);
+ code = krb5_set_default_realm(ctx, realm);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot set default realm");
+ code = krb5_unparse_name(ctx, admin, &user);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot unparse admin principal");
+ code = krb5_parse_name(ctx, principal, &princ);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot parse principal %s", principal);
+
+ /*
+ * If the actual kadmin calls fail, we may be built with MIT Kerberos
+ * against a Heimdal server or vice versa. Return false to skip the
+ * tests.
+ */
+ memset(&params, 0, sizeof(params));
+ params.realm = (char *) realm;
+ params.mask = KADM5_CONFIG_REALM;
+ code = kadm5_init_with_skey_ctx(ctx, user, path, KADM5_ADMIN_SERVICE,
+ &params, KADM5_STRUCT_VERSION,
+ KADM5_API_VERSION, &handle);
+ if (code != 0) {
+ diag_krb5(ctx, code, "error initializing kadmin");
+ goto done;
+ }
+ memset(&ent, 0, sizeof(ent));
+ ent.principal = princ;
+ ent.pw_expiration = (krb5_timestamp) expires;
+ code = kadm5_modify_principal(handle, &ent, KADM5_PW_EXPIRATION);
+ if (code == 0)
+ okay = true;
+ else
+ diag_krb5(ctx, code, "error setting password expiration");
+
+done:
+ kadm5_destroy(handle);
+ krb5_free_unparsed_name(ctx, user);
+ krb5_free_principal(ctx, admin);
+ krb5_free_principal(ctx, princ);
+ krb5_free_context(ctx);
+ test_file_path_free(path);
+ return okay;
+}
+#else /* !HAVE_KADM5CLNT */
+bool
+kerberos_expire_password(const char *principal UNUSED, time_t expires UNUSED)
+{
+ return false;
+}
+#endif /* !HAVE_KADM5CLNT */
diff --git a/tests/tap/kadmin.h b/tests/tap/kadmin.h
new file mode 100644
index 000000000000..c4dc657237da
--- /dev/null
+++ b/tests/tap/kadmin.h
@@ -0,0 +1,58 @@
+/*
+ * Utility functions for tests needing Kerberos admin actions.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_KADMIN_H
+#define TAP_KADMIN_H 1
+
+#include <config.h>
+#include <portable/stdbool.h>
+
+#include <time.h>
+
+#include <tests/tap/macros.h>
+
+BEGIN_DECLS
+
+/*
+ * Given the principal to set an expiration on and the expiration time, set
+ * that principal's key to expire at that time. Authentication is done using
+ * the keytab stored in config/admin-keytab.
+ *
+ * Returns true on success. Returns false if necessary configuration is
+ * missing so that the caller can choose whether to call bail or skip_all. If
+ * the configuration is present but the operation fails, bails.
+ */
+bool kerberos_expire_password(const char *, time_t)
+ __attribute__((__nonnull__));
+
+END_DECLS
+
+#endif /* !TAP_KADMIN_H */
diff --git a/tests/tap/kerberos.c b/tests/tap/kerberos.c
new file mode 100644
index 000000000000..765d80290a64
--- /dev/null
+++ b/tests/tap/kerberos.c
@@ -0,0 +1,544 @@
+/*
+ * Utility functions for tests that use Kerberos.
+ *
+ * The core function is kerberos_setup, which loads Kerberos test
+ * configuration and returns a struct of information. It also supports
+ * obtaining initial tickets from the configured keytab and setting up
+ * KRB5CCNAME and KRB5_KTNAME if a Kerberos keytab is present. Also included
+ * are utility functions for setting up a krb5.conf file and reporting
+ * Kerberos errors or warnings during testing.
+ *
+ * Some of the functionality here is only available if the Kerberos libraries
+ * are available.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006-2007, 2009-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+#include <portable/system.h>
+
+#include <sys/stat.h>
+
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/macros.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+/*
+ * Disable the requirement that format strings be literals, since it's easier
+ * to handle the possible patterns for kinit commands as an array.
+ */
+#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 2) || defined(__clang__)
+# pragma GCC diagnostic ignored "-Wformat-nonliteral"
+#endif
+
+
+/*
+ * These variables hold the allocated configuration struct, the environment to
+ * point to a different Kerberos ticket cache, keytab, and configuration file,
+ * and the temporary directories used. We store them so that we can free them
+ * on exit for cleaner valgrind output, making it easier to find real memory
+ * leaks in the tested programs.
+ */
+static struct kerberos_config *config = NULL;
+static char *krb5ccname = NULL;
+static char *krb5_ktname = NULL;
+static char *krb5_config = NULL;
+static char *tmpdir_ticket = NULL;
+static char *tmpdir_conf = NULL;
+
+
+/*
+ * Obtain Kerberos tickets and fill in the principal config entry.
+ *
+ * There are two implementations of this function, one if we have native
+ * Kerberos libraries available and one if we don't. Uses keytab to obtain
+ * credentials, and fills in the cache member of the provided config struct.
+ */
+#ifdef HAVE_KRB5
+
+static void
+kerberos_kinit(void)
+{
+ char *name, *krbtgt;
+ krb5_error_code code;
+ krb5_context ctx;
+ krb5_ccache ccache;
+ krb5_principal kprinc;
+ krb5_keytab keytab;
+ krb5_get_init_creds_opt *opts;
+ krb5_creds creds;
+ const char *realm;
+
+ /*
+ * Determine the principal corresponding to that keytab. We copy the
+ * memory to ensure that it's allocated in the right memory domain on
+ * systems where that may matter (like Windows).
+ */
+ code = krb5_init_context(&ctx);
+ if (code != 0)
+ bail_krb5(ctx, code, "error initializing Kerberos");
+ kprinc = kerberos_keytab_principal(ctx, config->keytab);
+ code = krb5_unparse_name(ctx, kprinc, &name);
+ if (code != 0)
+ bail_krb5(ctx, code, "error unparsing name");
+ krb5_free_principal(ctx, kprinc);
+ config->principal = bstrdup(name);
+ krb5_free_unparsed_name(ctx, name);
+
+ /* Now do the Kerberos initialization. */
+ code = krb5_cc_default(ctx, &ccache);
+ if (code != 0)
+ bail_krb5(ctx, code, "error setting ticket cache");
+ code = krb5_parse_name(ctx, config->principal, &kprinc);
+ if (code != 0)
+ bail_krb5(ctx, code, "error parsing principal %s", config->principal);
+ realm = krb5_principal_get_realm(ctx, kprinc);
+ basprintf(&krbtgt, "krbtgt/%s@%s", realm, realm);
+ code = krb5_kt_resolve(ctx, config->keytab, &keytab);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot open keytab %s", config->keytab);
+ code = krb5_get_init_creds_opt_alloc(ctx, &opts);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot allocate credential options");
+ krb5_get_init_creds_opt_set_default_flags(ctx, NULL, realm, opts);
+ krb5_get_init_creds_opt_set_forwardable(opts, 0);
+ krb5_get_init_creds_opt_set_proxiable(opts, 0);
+ code = krb5_get_init_creds_keytab(ctx, &creds, kprinc, keytab, 0, krbtgt,
+ opts);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot get Kerberos tickets");
+ code = krb5_cc_initialize(ctx, ccache, kprinc);
+ if (code != 0)
+ bail_krb5(ctx, code, "error initializing ticket cache");
+ code = krb5_cc_store_cred(ctx, ccache, &creds);
+ if (code != 0)
+ bail_krb5(ctx, code, "error storing credentials");
+ krb5_cc_close(ctx, ccache);
+ krb5_free_cred_contents(ctx, &creds);
+ krb5_kt_close(ctx, keytab);
+ krb5_free_principal(ctx, kprinc);
+ krb5_get_init_creds_opt_free(ctx, opts);
+ krb5_free_context(ctx);
+ free(krbtgt);
+}
+
+#else /* !HAVE_KRB5 */
+
+static void
+kerberos_kinit(void)
+{
+ static const char *const format[] = {
+ "kinit --no-afslog -k -t %s %s >/dev/null 2>&1 </dev/null",
+ "kinit -k -t %s %s >/dev/null 2>&1 </dev/null",
+ "kinit -t %s %s >/dev/null 2>&1 </dev/null",
+ "kinit -k -K %s %s >/dev/null 2>&1 </dev/null"};
+ FILE *file;
+ char *path;
+ char principal[BUFSIZ], *command;
+ size_t i;
+ int status;
+
+ /* Read the principal corresponding to the keytab. */
+ path = test_file_path("config/principal");
+ if (path == NULL) {
+ test_file_path_free(config->keytab);
+ config->keytab = NULL;
+ return;
+ }
+ file = fopen(path, "r");
+ if (file == NULL) {
+ test_file_path_free(path);
+ return;
+ }
+ test_file_path_free(path);
+ if (fgets(principal, sizeof(principal), file) == NULL)
+ bail("cannot read %s", path);
+ fclose(file);
+ if (principal[strlen(principal) - 1] != '\n')
+ bail("no newline in %s", path);
+ principal[strlen(principal) - 1] = '\0';
+ config->principal = bstrdup(principal);
+
+ /* Now do the Kerberos initialization. */
+ for (i = 0; i < ARRAY_SIZE(format); i++) {
+ basprintf(&command, format[i], config->keytab, principal);
+ status = system(command);
+ free(command);
+ if (status != -1 && WEXITSTATUS(status) == 0)
+ break;
+ }
+ if (status == -1 || WEXITSTATUS(status) != 0)
+ bail("cannot get Kerberos tickets");
+}
+
+#endif /* !HAVE_KRB5 */
+
+
+/*
+ * Free all the memory associated with our Kerberos setup, but don't remove
+ * the ticket cache. This is used when cleaning up on exit from a non-primary
+ * process so that test programs that fork don't remove the ticket cache still
+ * used by the main program.
+ */
+static void
+kerberos_free(void)
+{
+ test_tmpdir_free(tmpdir_ticket);
+ tmpdir_ticket = NULL;
+ if (config != NULL) {
+ test_file_path_free(config->keytab);
+ free(config->principal);
+ free(config->cache);
+ free(config->userprinc);
+ free(config->username);
+ free(config->password);
+ free(config->pkinit_principal);
+ free(config->pkinit_cert);
+ free(config);
+ config = NULL;
+ }
+ if (krb5ccname != NULL) {
+ putenv((char *) "KRB5CCNAME=");
+ free(krb5ccname);
+ krb5ccname = NULL;
+ }
+ if (krb5_ktname != NULL) {
+ putenv((char *) "KRB5_KTNAME=");
+ free(krb5_ktname);
+ krb5_ktname = NULL;
+ }
+}
+
+
+/*
+ * Clean up at the end of a test. This removes the ticket cache and resets
+ * and frees the memory allocated for the environment variables so that
+ * valgrind output on test suites is cleaner. Most of the work is done by
+ * kerberos_free, but this function also deletes the ticket cache.
+ */
+void
+kerberos_cleanup(void)
+{
+ char *path;
+
+ if (tmpdir_ticket != NULL) {
+ basprintf(&path, "%s/krb5cc_test", tmpdir_ticket);
+ unlink(path);
+ free(path);
+ }
+ kerberos_free();
+}
+
+
+/*
+ * The cleanup handler for the TAP framework. Call kerberos_cleanup if we're
+ * in the primary process and kerberos_free if not. The first argument, which
+ * indicates whether the test succeeded or not, is ignored, since we need to
+ * do the same thing either way.
+ */
+static void
+kerberos_cleanup_handler(int success UNUSED, int primary)
+{
+ if (primary)
+ kerberos_cleanup();
+ else
+ kerberos_free();
+}
+
+
+/*
+ * Obtain Kerberos tickets for the principal specified in config/principal
+ * using the keytab specified in config/keytab, both of which are presumed to
+ * be in tests in either the build or the source tree. Also sets KRB5_KTNAME
+ * and KRB5CCNAME.
+ *
+ * Returns the contents of config/principal in newly allocated memory or NULL
+ * if Kerberos tests are apparently not configured. If Kerberos tests are
+ * configured but something else fails, calls bail.
+ */
+struct kerberos_config *
+kerberos_setup(enum kerberos_needs needs)
+{
+ char *path;
+ char buffer[BUFSIZ];
+ FILE *file = NULL;
+
+ /* If we were called before, clean up after the previous run. */
+ if (config != NULL)
+ kerberos_cleanup();
+ config = bcalloc(1, sizeof(struct kerberos_config));
+
+ /*
+ * If we have a config/keytab file, set the KRB5CCNAME and KRB5_KTNAME
+ * environment variables and obtain initial tickets.
+ */
+ config->keytab = test_file_path("config/keytab");
+ if (config->keytab == NULL) {
+ if (needs == TAP_KRB_NEEDS_KEYTAB || needs == TAP_KRB_NEEDS_BOTH)
+ skip_all("Kerberos tests not configured");
+ } else {
+ tmpdir_ticket = test_tmpdir();
+ basprintf(&config->cache, "%s/krb5cc_test", tmpdir_ticket);
+ basprintf(&krb5ccname, "KRB5CCNAME=%s/krb5cc_test", tmpdir_ticket);
+ basprintf(&krb5_ktname, "KRB5_KTNAME=%s", config->keytab);
+ putenv(krb5ccname);
+ putenv(krb5_ktname);
+ kerberos_kinit();
+ }
+
+ /*
+ * If we have a config/password file, read it and fill out the relevant
+ * members of our config struct.
+ */
+ path = test_file_path("config/password");
+ if (path != NULL)
+ file = fopen(path, "r");
+ if (file == NULL) {
+ if (needs == TAP_KRB_NEEDS_PASSWORD || needs == TAP_KRB_NEEDS_BOTH)
+ skip_all("Kerberos tests not configured");
+ } else {
+ if (fgets(buffer, sizeof(buffer), file) == NULL)
+ bail("cannot read %s", path);
+ if (buffer[strlen(buffer) - 1] != '\n')
+ bail("no newline in %s", path);
+ buffer[strlen(buffer) - 1] = '\0';
+ config->userprinc = bstrdup(buffer);
+ if (fgets(buffer, sizeof(buffer), file) == NULL)
+ bail("cannot read password from %s", path);
+ fclose(file);
+ if (buffer[strlen(buffer) - 1] != '\n')
+ bail("password too long in %s", path);
+ buffer[strlen(buffer) - 1] = '\0';
+ config->password = bstrdup(buffer);
+
+ /*
+ * Strip the realm from the principal and set realm and username.
+ * This is not strictly correct; it doesn't cope with escaped @-signs
+ * or enterprise names.
+ */
+ config->username = bstrdup(config->userprinc);
+ config->realm = strchr(config->username, '@');
+ if (config->realm == NULL)
+ bail("test principal has no realm");
+ *config->realm = '\0';
+ config->realm++;
+ }
+ test_file_path_free(path);
+
+ /*
+ * If we have PKINIT configuration, read it and fill out the relevant
+ * members of our config struct.
+ */
+ path = test_file_path("config/pkinit-principal");
+ if (path != NULL)
+ file = fopen(path, "r");
+ if (path != NULL && file != NULL) {
+ if (fgets(buffer, sizeof(buffer), file) == NULL)
+ bail("cannot read %s", path);
+ if (buffer[strlen(buffer) - 1] != '\n')
+ bail("no newline in %s", path);
+ buffer[strlen(buffer) - 1] = '\0';
+ fclose(file);
+ test_file_path_free(path);
+ path = test_file_path("config/pkinit-cert");
+ if (path != NULL) {
+ config->pkinit_principal = bstrdup(buffer);
+ config->pkinit_cert = bstrdup(path);
+ }
+ }
+ test_file_path_free(path);
+ if (config->pkinit_cert == NULL && (needs & TAP_KRB_NEEDS_PKINIT) != 0)
+ skip_all("PKINIT tests not configured");
+
+ /*
+ * Register the cleanup function so that the caller doesn't have to do
+ * explicit cleanup.
+ */
+ test_cleanup_register(kerberos_cleanup_handler);
+
+ /* Return the configuration. */
+ return config;
+}
+
+
+/*
+ * Clean up the krb5.conf file generated by kerberos_generate_conf and free
+ * the memory used to set the environment variable. This doesn't fail if the
+ * file and variable are already gone, allowing it to be harmlessly run
+ * multiple times.
+ *
+ * Normally called via an atexit handler.
+ */
+void
+kerberos_cleanup_conf(void)
+{
+ char *path;
+
+ if (tmpdir_conf != NULL) {
+ basprintf(&path, "%s/krb5.conf", tmpdir_conf);
+ unlink(path);
+ free(path);
+ test_tmpdir_free(tmpdir_conf);
+ tmpdir_conf = NULL;
+ }
+ putenv((char *) "KRB5_CONFIG=");
+ free(krb5_config);
+ krb5_config = NULL;
+}
+
+
+/*
+ * Generate a krb5.conf file for testing and set KRB5_CONFIG to point to it.
+ * The [appdefaults] section will be stripped out and the default realm will
+ * be set to the realm specified, if not NULL. This will use config/krb5.conf
+ * in preference, so users can configure the tests by creating that file if
+ * the system file isn't suitable.
+ *
+ * Depends on data/generate-krb5-conf being present in the test suite.
+ */
+void
+kerberos_generate_conf(const char *realm)
+{
+ char *path;
+ const char *argv[3];
+
+ if (tmpdir_conf != NULL)
+ kerberos_cleanup_conf();
+ path = test_file_path("data/generate-krb5-conf");
+ if (path == NULL)
+ bail("cannot find generate-krb5-conf");
+ argv[0] = path;
+ argv[1] = realm;
+ argv[2] = NULL;
+ run_setup(argv);
+ test_file_path_free(path);
+ tmpdir_conf = test_tmpdir();
+ basprintf(&krb5_config, "KRB5_CONFIG=%s/krb5.conf", tmpdir_conf);
+ putenv(krb5_config);
+ if (atexit(kerberos_cleanup_conf) != 0)
+ sysdiag("cannot register cleanup function");
+}
+
+
+/*
+ * The remaining functions in this file are only available if Kerberos
+ * libraries are available.
+ */
+#ifdef HAVE_KRB5
+
+
+/*
+ * Report a Kerberos error and bail out. Takes a long instead of a
+ * krb5_error_code because it can also handle a kadm5_ret_t (which may be a
+ * different size).
+ */
+void
+bail_krb5(krb5_context ctx, long code, const char *format, ...)
+{
+ const char *k5_msg = NULL;
+ char *message;
+ va_list args;
+
+ if (ctx != NULL)
+ k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code);
+ va_start(args, format);
+ bvasprintf(&message, format, args);
+ va_end(args);
+ if (k5_msg == NULL)
+ bail("%s", message);
+ else
+ bail("%s: %s", message, k5_msg);
+}
+
+
+/*
+ * Report a Kerberos error as a diagnostic to stderr. Takes a long instead of
+ * a krb5_error_code because it can also handle a kadm5_ret_t (which may be a
+ * different size).
+ */
+void
+diag_krb5(krb5_context ctx, long code, const char *format, ...)
+{
+ const char *k5_msg = NULL;
+ char *message;
+ va_list args;
+
+ if (ctx != NULL)
+ k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code);
+ va_start(args, format);
+ bvasprintf(&message, format, args);
+ va_end(args);
+ if (k5_msg == NULL)
+ diag("%s", message);
+ else
+ diag("%s: %s", message, k5_msg);
+ free(message);
+ if (k5_msg != NULL)
+ krb5_free_error_message(ctx, k5_msg);
+}
+
+
+/*
+ * Find the principal of the first entry of a keytab and return it. The
+ * caller is responsible for freeing the result with krb5_free_principal.
+ * Exit on error.
+ */
+krb5_principal
+kerberos_keytab_principal(krb5_context ctx, const char *path)
+{
+ krb5_keytab keytab;
+ krb5_kt_cursor cursor;
+ krb5_keytab_entry entry;
+ krb5_principal princ;
+ krb5_error_code status;
+
+ status = krb5_kt_resolve(ctx, path, &keytab);
+ if (status != 0)
+ bail_krb5(ctx, status, "error opening %s", path);
+ status = krb5_kt_start_seq_get(ctx, keytab, &cursor);
+ if (status != 0)
+ bail_krb5(ctx, status, "error reading %s", path);
+ status = krb5_kt_next_entry(ctx, keytab, &entry, &cursor);
+ if (status != 0)
+ bail("no principal found in keytab file %s", path);
+ status = krb5_copy_principal(ctx, entry.principal, &princ);
+ if (status != 0)
+ bail_krb5(ctx, status, "error copying principal from %s", path);
+ krb5_kt_free_entry(ctx, &entry);
+ krb5_kt_end_seq_get(ctx, keytab, &cursor);
+ krb5_kt_close(ctx, keytab);
+ return princ;
+}
+
+#endif /* HAVE_KRB5 */
diff --git a/tests/tap/kerberos.h b/tests/tap/kerberos.h
new file mode 100644
index 000000000000..53dd09619c96
--- /dev/null
+++ b/tests/tap/kerberos.h
@@ -0,0 +1,135 @@
+/*
+ * Utility functions for tests that use Kerberos.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006-2007, 2009, 2011-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_KERBEROS_H
+#define TAP_KERBEROS_H 1
+
+#include <config.h>
+#include <tests/tap/macros.h>
+
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+
+/* Holds the information parsed from the Kerberos test configuration. */
+struct kerberos_config {
+ char *keytab; /* Path to the keytab. */
+ char *principal; /* Principal whose keys are in the keytab. */
+ char *cache; /* Path to the Kerberos ticket cache. */
+ char *userprinc; /* The fully-qualified principal. */
+ char *username; /* The local (non-realm) part of principal. */
+ char *realm; /* The realm part of the principal. */
+ char *password; /* The password. */
+ char *pkinit_principal; /* Principal for PKINIT authentication. */
+ char *pkinit_cert; /* Path to certificates for PKINIT. */
+};
+
+/*
+ * Whether to skip all tests (by calling skip_all) in kerberos_setup if
+ * certain configuration information isn't available. "_BOTH" means that the
+ * tests require both keytab and password, but PKINIT is not required.
+ */
+enum kerberos_needs
+{
+ /* clang-format off */
+ TAP_KRB_NEEDS_NONE = 0x00,
+ TAP_KRB_NEEDS_KEYTAB = 0x01,
+ TAP_KRB_NEEDS_PASSWORD = 0x02,
+ TAP_KRB_NEEDS_BOTH = 0x01 | 0x02,
+ TAP_KRB_NEEDS_PKINIT = 0x04
+ /* clang-format on */
+};
+
+BEGIN_DECLS
+
+/*
+ * Set up Kerberos, returning the test configuration information. This
+ * obtains Kerberos tickets from config/keytab, if one is present, and stores
+ * them in a Kerberos ticket cache, sets KRB5_KTNAME and KRB5CCNAME. It also
+ * loads the principal and password from config/password, if it exists, and
+ * stores the principal, password, username, and realm in the returned struct.
+ *
+ * If there is no config/keytab file, KRB5_KTNAME and KRB5CCNAME won't be set
+ * and the keytab field will be NULL. If there is no config/password file,
+ * the principal field will be NULL. If the files exist but loading them
+ * fails, or authentication fails, kerberos_setup calls bail.
+ *
+ * kerberos_cleanup will be run as a cleanup function normally, freeing all
+ * resources and cleaning up temporary files on process exit. It can,
+ * however, be called directly if for some reason the caller needs to delete
+ * the Kerberos environment again. However, normally the caller can just call
+ * kerberos_setup again.
+ */
+struct kerberos_config *kerberos_setup(enum kerberos_needs)
+ __attribute__((__malloc__));
+void kerberos_cleanup(void);
+
+/*
+ * Generate a krb5.conf file for testing and set KRB5_CONFIG to point to it.
+ * The [appdefaults] section will be stripped out and the default realm will
+ * be set to the realm specified, if not NULL. This will use config/krb5.conf
+ * in preference, so users can configure the tests by creating that file if
+ * the system file isn't suitable.
+ *
+ * Depends on data/generate-krb5-conf being present in the test suite.
+ *
+ * kerberos_cleanup_conf will clean up after this function, but usually
+ * doesn't need to be called directly since it's registered as an atexit
+ * handler.
+ */
+void kerberos_generate_conf(const char *realm);
+void kerberos_cleanup_conf(void);
+
+/* These interfaces are only available with native Kerberos support. */
+#ifdef HAVE_KRB5
+
+/* Bail out with an error, appending the Kerberos error message. */
+void bail_krb5(krb5_context, long, const char *format, ...)
+ __attribute__((__noreturn__, __nonnull__(3), __format__(printf, 3, 4)));
+
+/* Report a diagnostic with Kerberos error to stderr prefixed with #. */
+void diag_krb5(krb5_context, long, const char *format, ...)
+ __attribute__((__nonnull__(3), __format__(printf, 3, 4)));
+
+/*
+ * Given a Kerberos context and the path to a keytab, retrieve the principal
+ * for the first entry in the keytab and return it. Calls bail on failure.
+ * The returned principal should be freed with krb5_free_principal.
+ */
+krb5_principal kerberos_keytab_principal(krb5_context, const char *path)
+ __attribute__((__nonnull__));
+
+#endif /* HAVE_KRB5 */
+
+END_DECLS
+
+#endif /* !TAP_MESSAGES_H */
diff --git a/tests/tap/libtap.sh b/tests/tap/libtap.sh
new file mode 100644
index 000000000000..1827a689e380
--- /dev/null
+++ b/tests/tap/libtap.sh
@@ -0,0 +1,248 @@
+# Shell function library for test cases.
+#
+# Note that while many of the functions in this library could benefit from
+# using "local" to avoid possibly hammering global variables, Solaris /bin/sh
+# doesn't support local and this library aspires to be portable to Solaris
+# Bourne shell. Instead, all private variables are prefixed with "tap_".
+#
+# This file provides a TAP-compatible shell function library useful for
+# writing test cases. It is part of C TAP Harness, which can be found at
+# <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2009-2012, 2016 Russ Allbery <eagle@eyrie.org>
+# Copyright 2006-2008, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+# Print out the number of test cases we expect to run.
+plan () {
+ count=1
+ planned="$1"
+ failed=0
+ echo "1..$1"
+ trap finish 0
+}
+
+# Prepare for lazy planning.
+plan_lazy () {
+ count=1
+ planned=0
+ failed=0
+ trap finish 0
+}
+
+# Report the test status on exit.
+finish () {
+ tap_highest=`expr "$count" - 1`
+ if [ "$planned" = 0 ] ; then
+ echo "1..$tap_highest"
+ planned="$tap_highest"
+ fi
+ tap_looks='# Looks like you'
+ if [ "$planned" -gt 0 ] ; then
+ if [ "$planned" -gt "$tap_highest" ] ; then
+ if [ "$planned" -gt 1 ] ; then
+ echo "$tap_looks planned $planned tests but only ran" \
+ "$tap_highest"
+ else
+ echo "$tap_looks planned $planned test but only ran" \
+ "$tap_highest"
+ fi
+ elif [ "$planned" -lt "$tap_highest" ] ; then
+ tap_extra=`expr "$tap_highest" - "$planned"`
+ if [ "$planned" -gt 1 ] ; then
+ echo "$tap_looks planned $planned tests but ran" \
+ "$tap_extra extra"
+ else
+ echo "$tap_looks planned $planned test but ran" \
+ "$tap_extra extra"
+ fi
+ elif [ "$failed" -gt 0 ] ; then
+ if [ "$failed" -gt 1 ] ; then
+ echo "$tap_looks failed $failed tests of $planned"
+ else
+ echo "$tap_looks failed $failed test of $planned"
+ fi
+ elif [ "$planned" -gt 1 ] ; then
+ echo "# All $planned tests successful or skipped"
+ else
+ echo "# $planned test successful or skipped"
+ fi
+ fi
+}
+
+# Skip the entire test suite. Should be run instead of plan.
+skip_all () {
+ tap_desc="$1"
+ if [ -n "$tap_desc" ] ; then
+ echo "1..0 # skip $tap_desc"
+ else
+ echo "1..0 # skip"
+ fi
+ exit 0
+}
+
+# ok takes a test description and a command to run and prints success if that
+# command is successful, false otherwise. The count starts at 1 and is
+# updated each time ok is printed.
+ok () {
+ tap_desc="$1"
+ if [ -n "$tap_desc" ] ; then
+ tap_desc=" - $tap_desc"
+ fi
+ shift
+ if "$@" ; then
+ echo ok "$count$tap_desc"
+ else
+ echo not ok "$count$tap_desc"
+ failed=`expr $failed + 1`
+ fi
+ count=`expr $count + 1`
+}
+
+# Skip the next test. Takes the reason why the test is skipped.
+skip () {
+ echo "ok $count # skip $*"
+ count=`expr $count + 1`
+}
+
+# Report the same status on a whole set of tests. Takes the count of tests,
+# the description, and then the command to run to determine the status.
+ok_block () {
+ tap_i=$count
+ tap_end=`expr $count + $1`
+ shift
+ while [ "$tap_i" -lt "$tap_end" ] ; do
+ ok "$@"
+ tap_i=`expr $tap_i + 1`
+ done
+}
+
+# Skip a whole set of tests. Takes the count and then the reason for skipping
+# the test.
+skip_block () {
+ tap_i=$count
+ tap_end=`expr $count + $1`
+ shift
+ while [ "$tap_i" -lt "$tap_end" ] ; do
+ skip "$@"
+ tap_i=`expr $tap_i + 1`
+ done
+}
+
+# Portable variant of printf '%s\n' "$*". In the majority of cases, this
+# function is slower than printf, because the latter is often implemented
+# as a builtin command. The value of the variable IFS is ignored.
+#
+# This macro must not be called via backticks inside double quotes, since this
+# will result in bizarre escaping behavior and lots of extra backslashes on
+# Solaris.
+puts () {
+ cat << EOH
+$@
+EOH
+}
+
+# Run a program expected to succeed, and print ok if it does and produces the
+# correct output. Takes the description, expected exit status, the expected
+# output, the command to run, and then any arguments for that command.
+# Standard output and standard error are combined when analyzing the output of
+# the command.
+#
+# If the command may contain system-specific error messages in its output,
+# add strip_colon_error before the command to post-process its output.
+ok_program () {
+ tap_desc="$1"
+ shift
+ tap_w_status="$1"
+ shift
+ tap_w_output="$1"
+ shift
+ tap_output=`"$@" 2>&1`
+ tap_status=$?
+ if [ $tap_status = $tap_w_status ] \
+ && [ x"$tap_output" = x"$tap_w_output" ] ; then
+ ok "$tap_desc" true
+ else
+ echo "# saw: ($tap_status) $tap_output"
+ echo "# not: ($tap_w_status) $tap_w_output"
+ ok "$tap_desc" false
+ fi
+}
+
+# Strip a colon and everything after it off the output of a command, as long
+# as that colon comes after at least one whitespace character. (This is done
+# to avoid stripping the name of the program from the start of an error
+# message.) This is used to remove system-specific error messages (coming
+# from strerror, for example).
+strip_colon_error() {
+ tap_output=`"$@" 2>&1`
+ tap_status=$?
+ tap_output=`puts "$tap_output" | sed 's/^\([^ ]* [^:]*\):.*/\1/'`
+ puts "$tap_output"
+ return $tap_status
+}
+
+# Bail out with an error message.
+bail () {
+ echo 'Bail out!' "$@"
+ exit 255
+}
+
+# Output a diagnostic on standard error, preceded by the required # mark.
+diag () {
+ echo '#' "$@"
+}
+
+# Search for the given file first in $C_TAP_BUILD and then in $C_TAP_SOURCE
+# and echo the path where the file was found, or the empty string if the file
+# wasn't found.
+#
+# This macro uses puts, so don't run it using backticks inside double quotes
+# or bizarre quoting behavior will happen with Solaris sh.
+test_file_path () {
+ if [ -n "$C_TAP_BUILD" ] && [ -f "$C_TAP_BUILD/$1" ] ; then
+ puts "$C_TAP_BUILD/$1"
+ elif [ -n "$C_TAP_SOURCE" ] && [ -f "$C_TAP_SOURCE/$1" ] ; then
+ puts "$C_TAP_SOURCE/$1"
+ else
+ echo ''
+ fi
+}
+
+# Create $C_TAP_BUILD/tmp for use by tests for storing temporary files and
+# return the path (via standard output).
+#
+# This macro uses puts, so don't run it using backticks inside double quotes
+# or bizarre quoting behavior will happen with Solaris sh.
+test_tmpdir () {
+ if [ -z "$C_TAP_BUILD" ] ; then
+ tap_tmpdir="./tmp"
+ else
+ tap_tmpdir="$C_TAP_BUILD"/tmp
+ fi
+ if [ ! -d "$tap_tmpdir" ] ; then
+ mkdir "$tap_tmpdir" || bail "Error creating $tap_tmpdir"
+ fi
+ puts "$tap_tmpdir"
+}
diff --git a/tests/tap/macros.h b/tests/tap/macros.h
new file mode 100644
index 000000000000..c2c8b5c7315d
--- /dev/null
+++ b/tests/tap/macros.h
@@ -0,0 +1,99 @@
+/*
+ * Helpful macros for TAP header files.
+ *
+ * This is not, strictly speaking, related to TAP, but any TAP add-on is
+ * probably going to need these macros, so define them in one place so that
+ * everyone can pull them in.
+ *
+ * This file is part of C TAP Harness. The current version plus supporting
+ * documentation is at <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+ *
+ * Copyright 2008, 2012-2013, 2015 Russ Allbery <eagle@eyrie.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_MACROS_H
+#define TAP_MACROS_H 1
+
+/*
+ * __attribute__ is available in gcc 2.5 and later, but only with gcc 2.7
+ * could you use the __format__ form of the attributes, which is what we use
+ * (to avoid confusion with other macros), and only with gcc 2.96 can you use
+ * the attribute __malloc__. 2.96 is very old, so don't bother trying to get
+ * the other attributes to work with GCC versions between 2.7 and 2.96.
+ */
+#ifndef __attribute__
+# if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 96)
+# define __attribute__(spec) /* empty */
+# endif
+#endif
+
+/*
+ * We use __alloc_size__, but it was only available in fairly recent versions
+ * of GCC. Suppress warnings about the unknown attribute if GCC is too old.
+ * We know that we're GCC at this point, so we can use the GCC variadic macro
+ * extension, which will still work with versions of GCC too old to have C99
+ * variadic macro support.
+ */
+#if !defined(__attribute__) && !defined(__alloc_size__)
+# if defined(__GNUC__) && !defined(__clang__)
+# if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 3)
+# define __alloc_size__(spec, args...) /* empty */
+# endif
+# endif
+#endif
+
+/* Suppress __warn_unused_result__ if gcc is too old. */
+#if !defined(__attribute__) && !defined(__warn_unused_result__)
+# if __GNUC__ < 3 || (__GNUC__ == 3 && __GNUC_MINOR__ < 4)
+# define __warn_unused_result__ /* empty */
+# endif
+#endif
+
+/*
+ * LLVM and Clang pretend to be GCC but don't support all of the __attribute__
+ * settings that GCC does. For them, suppress warnings about unknown
+ * attributes on declarations. This unfortunately will affect the entire
+ * compilation context, but there's no push and pop available.
+ */
+#if !defined(__attribute__) && (defined(__llvm__) || defined(__clang__))
+# pragma GCC diagnostic ignored "-Wattributes"
+#endif
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+/*
+ * BEGIN_DECLS is used at the beginning of declarations so that C++
+ * compilers don't mangle their names. END_DECLS is used at the end.
+ */
+#undef BEGIN_DECLS
+#undef END_DECLS
+#ifdef __cplusplus
+# define BEGIN_DECLS extern "C" {
+# define END_DECLS }
+#else
+# define BEGIN_DECLS /* empty */
+# define END_DECLS /* empty */
+#endif
+
+#endif /* TAP_MACROS_H */
diff --git a/tests/tap/perl/Test/RRA.pm b/tests/tap/perl/Test/RRA.pm
new file mode 100644
index 000000000000..6ea65c5701c3
--- /dev/null
+++ b/tests/tap/perl/Test/RRA.pm
@@ -0,0 +1,324 @@
+# Helper functions for test programs written in Perl.
+#
+# This module provides a collection of helper functions used by test programs
+# written in Perl. This is a general collection of functions that can be used
+# by both C packages with Automake and by stand-alone Perl modules. See
+# Test::RRA::Automake for additional functions specifically for C Automake
+# distributions.
+#
+# SPDX-License-Identifier: MIT
+
+package Test::RRA;
+
+use 5.010;
+use base qw(Exporter);
+use strict;
+use warnings;
+
+use Carp qw(croak);
+use File::Temp;
+
+# Abort if Test::More was loaded before Test::RRA to be sure that we get the
+# benefits of the Test::More probing below.
+if ($INC{'Test/More.pm'}) {
+ croak('Test::More loaded before Test::RRA');
+}
+
+# Red Hat's base perl package doesn't include Test::More (one has to install
+# the perl-core package in addition). Try to detect this and skip any Perl
+# tests if Test::More is not present. This relies on Test::RRA being included
+# before Test::More.
+eval {
+ require Test::More;
+ Test::More->import();
+};
+if ($@) {
+ print "1..0 # SKIP Test::More required for test\n"
+ or croak('Cannot write to stdout');
+ exit 0;
+}
+
+# Declare variables that should be set in BEGIN for robustness.
+our (@EXPORT_OK, $VERSION);
+
+# Set $VERSION and everything export-related in a BEGIN block for robustness
+# against circular module loading (not that we load any modules, but
+# consistency is good).
+BEGIN {
+ @EXPORT_OK = qw(
+ is_file_contents skip_unless_author skip_unless_automated use_prereq
+ );
+
+ # This version should match the corresponding rra-c-util release, but with
+ # two digits for the minor version, including a leading zero if necessary,
+ # so that it will sort properly.
+ $VERSION = '10.00';
+}
+
+# Compare a string to the contents of a file, similar to the standard is()
+# function, but to show the line-based unified diff between them if they
+# differ.
+#
+# $got - The output that we received
+# $expected - The path to the file containing the expected output
+# $message - The message to use when reporting the test results
+#
+# Returns: undef
+# Throws: Exception on failure to read or write files or run diff
+sub is_file_contents {
+ my ($got, $expected, $message) = @_;
+
+ # If they're equal, this is simple.
+ open(my $fh, '<', $expected) or BAIL_OUT("Cannot open $expected: $!\n");
+ my $data = do { local $/ = undef; <$fh> };
+ close($fh) or BAIL_OUT("Cannot close $expected: $!\n");
+ if ($got eq $data) {
+ is($got, $data, $message);
+ return;
+ }
+
+ # Otherwise, we show a diff, but only if we have IPC::System::Simple and
+ # diff succeeds. Otherwise, we fall back on showing the full expected and
+ # seen output.
+ eval {
+ require IPC::System::Simple;
+
+ my $tmp = File::Temp->new();
+ my $tmpname = $tmp->filename;
+ print {$tmp} $got or BAIL_OUT("Cannot write to $tmpname: $!\n");
+ my @command = ('diff', '-u', $expected, $tmpname);
+ my $diff = IPC::System::Simple::capturex([0 .. 1], @command);
+ diag($diff);
+ };
+ if ($@) {
+ diag('Expected:');
+ diag($expected);
+ diag('Seen:');
+ diag($data);
+ }
+
+ # Report failure.
+ ok(0, $message);
+ return;
+}
+
+# Skip this test unless author tests are requested. Takes a short description
+# of what tests this script would perform, which is used in the skip message.
+# Calls plan skip_all, which will terminate the program.
+#
+# $description - Short description of the tests
+#
+# Returns: undef
+sub skip_unless_author {
+ my ($description) = @_;
+ if (!$ENV{AUTHOR_TESTING}) {
+ plan(skip_all => "$description only run for author");
+ }
+ return;
+}
+
+# Skip this test unless doing automated testing or release testing. This is
+# used for tests that should be run by CPAN smoke testing or during releases,
+# but not for manual installs by end users. Takes a short description of what
+# tests this script would perform, which is used in the skip message. Calls
+# plan skip_all, which will terminate the program.
+#
+# $description - Short description of the tests
+#
+# Returns: undef
+sub skip_unless_automated {
+ my ($description) = @_;
+ for my $env (qw(AUTOMATED_TESTING RELEASE_TESTING AUTHOR_TESTING)) {
+ return if $ENV{$env};
+ }
+ plan(skip_all => "$description normally skipped");
+ return;
+}
+
+# Attempt to load a module and skip the test if the module could not be
+# loaded. If the module could be loaded, call its import function manually.
+# If the module could not be loaded, calls plan skip_all, which will terminate
+# the program.
+#
+# The special logic here is based on Test::More and is required to get the
+# imports to happen in the caller's namespace.
+#
+# $module - Name of the module to load
+# @imports - Any arguments to import, possibly including a version
+#
+# Returns: undef
+sub use_prereq {
+ my ($module, @imports) = @_;
+
+ # If the first import looks like a version, pass it as a bare string.
+ my $version = q{};
+ if (@imports >= 1 && $imports[0] =~ m{ \A \d+ (?: [.][\d_]+ )* \z }xms) {
+ $version = shift(@imports);
+ }
+
+ # Get caller information to put imports in the correct package.
+ my ($package) = caller;
+
+ # Do the import with eval, and try to isolate it from the surrounding
+ # context as much as possible. Based heavily on Test::More::_eval.
+ ## no critic (BuiltinFunctions::ProhibitStringyEval)
+ ## no critic (ValuesAndExpressions::ProhibitImplicitNewlines)
+ my ($result, $error, $sigdie);
+ {
+ local $@ = undef;
+ local $! = undef;
+ local $SIG{__DIE__} = undef;
+ $result = eval qq{
+ package $package;
+ use $module $version \@imports;
+ 1;
+ };
+ $error = $@;
+ $sigdie = $SIG{__DIE__} || undef;
+ }
+
+ # If the use failed for any reason, skip the test.
+ if (!$result || $error) {
+ my $name = length($version) > 0 ? "$module $version" : $module;
+ plan(skip_all => "$name required for test");
+ }
+
+ # If the module set $SIG{__DIE__}, we cleared that via local. Restore it.
+ ## no critic (Variables::RequireLocalizedPunctuationVars)
+ if (defined($sigdie)) {
+ $SIG{__DIE__} = $sigdie;
+ }
+ return;
+}
+
+1;
+__END__
+
+=for stopwords
+Allbery Allbery's DESC bareword sublicense MERCHANTABILITY NONINFRINGEMENT
+rra-c-util CPAN diff
+
+=head1 NAME
+
+Test::RRA - Support functions for Perl tests
+
+=head1 SYNOPSIS
+
+ use Test::RRA
+ qw(skip_unless_author skip_unless_automated use_prereq);
+
+ # Skip this test unless author tests are requested.
+ skip_unless_author('Coding style tests');
+
+ # Skip this test unless doing automated or release testing.
+ skip_unless_automated('POD syntax tests');
+
+ # Load modules, skipping the test if they're not available.
+ use_prereq('Perl6::Slurp', 'slurp');
+ use_prereq('Test::Script::Run', '0.04');
+
+=head1 DESCRIPTION
+
+This module collects utility functions that are useful for Perl test scripts.
+It assumes Russ Allbery's Perl module layout and test conventions and will
+only be useful for other people if they use the same conventions.
+
+This module B<must> be loaded before Test::More or it will abort during
+import. It will skip the test (by printing a skip message to standard output
+and exiting with status 0, equivalent to C<plan skip_all>) during import if
+Test::More is not available. This allows tests written in Perl using this
+module to be skipped if run on a system with Perl but not Test::More, such as
+Red Hat systems with the C<perl> package but not the C<perl-core> package
+installed.
+
+=head1 FUNCTIONS
+
+None of these functions are imported by default. The ones used by a script
+should be explicitly imported.
+
+=over 4
+
+=item is_file_contents(GOT, EXPECTED, MESSAGE)
+
+Check a string against the contents of a file, showing the differences if any
+using diff (if IPC::System::Simple and diff are available). GOT is the output
+the test received. EXPECTED is the path to a file containing the expected
+output (not the output itself). MESSAGE is a message to display alongside the
+test results.
+
+=item skip_unless_author(DESC)
+
+Checks whether AUTHOR_TESTING is set in the environment and skips the whole
+test (by calling C<plan skip_all> from Test::More) if it is not. DESC is a
+description of the tests being skipped. A space and C<only run for author>
+will be appended to it and used as the skip reason.
+
+=item skip_unless_automated(DESC)
+
+Checks whether AUTHOR_TESTING, AUTOMATED_TESTING, or RELEASE_TESTING are set
+in the environment and skips the whole test (by calling C<plan skip_all> from
+Test::More) if they are not. This should be used by tests that should not run
+during end-user installs of the module, but which should run as part of CPAN
+smoke testing and release testing.
+
+DESC is a description of the tests being skipped. A space and C<normally
+skipped> will be appended to it and used as the skip reason.
+
+=item use_prereq(MODULE[, VERSION][, IMPORT ...])
+
+Attempts to load MODULE with the given VERSION and import arguments. If this
+fails for any reason, the test will be skipped (by calling C<plan skip_all>
+from Test::More) with a skip reason saying that MODULE is required for the
+test.
+
+VERSION will be passed to C<use> as a version bareword if it looks like a
+version number. The remaining IMPORT arguments will be passed as the value of
+an array.
+
+=back
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2016, 2018-2019, 2021 Russ Allbery <eagle@eyrie.org>
+
+Copyright 2013-2014 The Board of Trustees of the Leland Stanford Junior
+University
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=head1 SEE ALSO
+
+Test::More(3), Test::RRA::Automake(3), Test::RRA::Config(3)
+
+This module is maintained in the rra-c-util package. The current version is
+available from L<https://www.eyrie.org/~eagle/software/rra-c-util/>.
+
+The functions to control when tests are run use environment variables defined
+by the L<Lancaster
+Consensus|https://github.com/Perl-Toolchain-Gang/toolchain-site/blob/master/lancaster-consensus.md>.
+
+=cut
+
+# Local Variables:
+# copyright-at-end-flag: t
+# End:
diff --git a/tests/tap/perl/Test/RRA/Automake.pm b/tests/tap/perl/Test/RRA/Automake.pm
new file mode 100644
index 000000000000..261feab81e27
--- /dev/null
+++ b/tests/tap/perl/Test/RRA/Automake.pm
@@ -0,0 +1,487 @@
+# Helper functions for Perl test programs in Automake distributions.
+#
+# This module provides a collection of helper functions used by test programs
+# written in Perl and included in C source distributions that use Automake.
+# They embed knowledge of how I lay out my source trees and test suites with
+# Autoconf and Automake. They may be usable by others, but doing so will
+# require closely following the conventions implemented by the rra-c-util
+# utility collection.
+#
+# All the functions here assume that C_TAP_BUILD and C_TAP_SOURCE are set in
+# the environment. This is normally done via the C TAP Harness runtests
+# wrapper.
+#
+# SPDX-License-Identifier: MIT
+
+package Test::RRA::Automake;
+
+use 5.010;
+use base qw(Exporter);
+use strict;
+use warnings;
+
+use Exporter;
+use File::Find qw(find);
+use File::Spec;
+use Test::More;
+use Test::RRA::Config qw($LIBRARY_PATH);
+
+# Used below for use lib calls.
+my ($PERL_BLIB_ARCH, $PERL_BLIB_LIB);
+
+# Determine the path to the build tree of any embedded Perl module package in
+# this source package. We do this in a BEGIN block because we're going to use
+# the results in a use lib command below.
+BEGIN {
+ $PERL_BLIB_ARCH = File::Spec->catdir(qw(perl blib arch));
+ $PERL_BLIB_LIB = File::Spec->catdir(qw(perl blib lib));
+
+ # If C_TAP_BUILD is set, we can come up with better values.
+ if (defined($ENV{C_TAP_BUILD})) {
+ my ($vol, $dirs) = File::Spec->splitpath($ENV{C_TAP_BUILD}, 1);
+ my @dirs = File::Spec->splitdir($dirs);
+ pop(@dirs);
+ $PERL_BLIB_ARCH = File::Spec->catdir(@dirs, qw(perl blib arch));
+ $PERL_BLIB_LIB = File::Spec->catdir(@dirs, qw(perl blib lib));
+ }
+}
+
+# Prefer the modules built as part of our source package. Otherwise, we may
+# not find Perl modules while testing, or find the wrong versions.
+use lib $PERL_BLIB_ARCH;
+use lib $PERL_BLIB_LIB;
+
+# Declare variables that should be set in BEGIN for robustness.
+our (@EXPORT_OK, $VERSION);
+
+# Set $VERSION and everything export-related in a BEGIN block for robustness
+# against circular module loading (not that we load any modules, but
+# consistency is good).
+BEGIN {
+ @EXPORT_OK = qw(
+ all_files automake_setup perl_dirs test_file_path test_tmpdir
+ );
+
+ # This version should match the corresponding rra-c-util release, but with
+ # two digits for the minor version, including a leading zero if necessary,
+ # so that it will sort properly.
+ $VERSION = '10.00';
+}
+
+# Directories to skip globally when looking for all files, or for directories
+# that could contain Perl files.
+my @GLOBAL_SKIP = qw(
+ .git .pc _build autom4te.cache build-aux perl/_build perl/blib
+);
+
+# Additional paths to skip when building a list of all files in the
+# distribution. This primarily skips build artifacts that aren't interesting
+# to any of the tests. These match any path component.
+my @FILES_SKIP = qw(
+ .deps .dirstamp .libs aclocal.m4 config.h config.h.in config.h.in~
+ config.log config.status configure configure~
+);
+
+# The temporary directory created by test_tmpdir, if any. If this is set,
+# attempt to remove the directory stored here on program exit (but ignore
+# failure to do so).
+my $TMPDIR;
+
+# Returns a list of all files in the distribution.
+#
+# Returns: List of files
+sub all_files {
+ my @files;
+
+ # Turn the skip lists into hashes for ease of querying.
+ my %skip = map { $_ => 1 } @GLOBAL_SKIP;
+ my %files_skip = map { $_ => 1 } @FILES_SKIP;
+
+ # Wanted function for find. Prune anything matching either of the skip
+ # lists, or *.lo files, and then add all regular files to the list.
+ my $wanted = sub {
+ my $file = $_;
+ my $path = $File::Find::name;
+ $path =~ s{ \A [.]/ }{}xms;
+ if ($skip{$path} || $files_skip{$file} || $file =~ m{ [.]lo\z }xms) {
+ $File::Find::prune = 1;
+ return;
+ }
+ if (!-d $file) {
+ push(@files, $path);
+ }
+ };
+
+ # Do the recursive search and return the results.
+ find($wanted, q{.});
+ return @files;
+}
+
+# Perform initial test setup for running a Perl test in an Automake package.
+# This verifies that C_TAP_BUILD and C_TAP_SOURCE are set and then changes
+# directory to the C_TAP_SOURCE directory by default. Sets LD_LIBRARY_PATH if
+# the $LIBRARY_PATH configuration option is set. Calls BAIL_OUT if
+# C_TAP_BUILD or C_TAP_SOURCE are missing or if anything else fails.
+#
+# $args_ref - Reference to a hash of arguments to configure behavior:
+# chdir_build - If set to a true value, changes to C_TAP_BUILD instead of
+# C_TAP_SOURCE
+#
+# Returns: undef
+sub automake_setup {
+ my ($args_ref) = @_;
+
+ # Bail if C_TAP_BUILD or C_TAP_SOURCE are not set.
+ if (!$ENV{C_TAP_BUILD}) {
+ BAIL_OUT('C_TAP_BUILD not defined (run under runtests)');
+ }
+ if (!$ENV{C_TAP_SOURCE}) {
+ BAIL_OUT('C_TAP_SOURCE not defined (run under runtests)');
+ }
+
+ # C_TAP_BUILD or C_TAP_SOURCE will be the test directory. Change to the
+ # parent.
+ my $start;
+ if ($args_ref->{chdir_build}) {
+ $start = $ENV{C_TAP_BUILD};
+ } else {
+ $start = $ENV{C_TAP_SOURCE};
+ }
+ my ($vol, $dirs) = File::Spec->splitpath($start, 1);
+ my @dirs = File::Spec->splitdir($dirs);
+ pop(@dirs);
+
+ # Simplify relative paths at the end of the directory.
+ my $ups = 0;
+ my $i = $#dirs;
+ while ($i > 2 && $dirs[$i] eq File::Spec->updir) {
+ $ups++;
+ $i--;
+ }
+ for (1 .. $ups) {
+ pop(@dirs);
+ pop(@dirs);
+ }
+ my $root = File::Spec->catpath($vol, File::Spec->catdir(@dirs), q{});
+ chdir($root) or BAIL_OUT("cannot chdir to $root: $!");
+
+ # If C_TAP_BUILD is a subdirectory of C_TAP_SOURCE, add it to the global
+ # ignore list.
+ my ($buildvol, $builddirs) = File::Spec->splitpath($ENV{C_TAP_BUILD}, 1);
+ my @builddirs = File::Spec->splitdir($builddirs);
+ pop(@builddirs);
+ if ($buildvol eq $vol && @builddirs == @dirs + 1) {
+ while (@dirs && $builddirs[0] eq $dirs[0]) {
+ shift(@builddirs);
+ shift(@dirs);
+ }
+ if (@builddirs == 1) {
+ push(@GLOBAL_SKIP, $builddirs[0]);
+ }
+ }
+
+ # Set LD_LIBRARY_PATH if the $LIBRARY_PATH configuration option is set.
+ ## no critic (Variables::RequireLocalizedPunctuationVars)
+ if (defined($LIBRARY_PATH)) {
+ @builddirs = File::Spec->splitdir($builddirs);
+ pop(@builddirs);
+ my $libdir = File::Spec->catdir(@builddirs, $LIBRARY_PATH);
+ my $path = File::Spec->catpath($buildvol, $libdir, q{});
+ if (-d "$path/.libs") {
+ $path .= '/.libs';
+ }
+ if ($ENV{LD_LIBRARY_PATH}) {
+ $ENV{LD_LIBRARY_PATH} .= ":$path";
+ } else {
+ $ENV{LD_LIBRARY_PATH} = $path;
+ }
+ }
+ return;
+}
+
+# Returns a list of directories that may contain Perl scripts and that should
+# be passed to Perl test infrastructure that expects a list of directories to
+# recursively check. The list will be all eligible top-level directories in
+# the package except for the tests directory, which is broken out to one
+# additional level. Calls BAIL_OUT on any problems
+#
+# $args_ref - Reference to a hash of arguments to configure behavior:
+# skip - A reference to an array of directories to skip
+#
+# Returns: List of directories possibly containing Perl scripts to test
+sub perl_dirs {
+ my ($args_ref) = @_;
+
+ # Add the global skip list. We also ignore the perl directory if it
+ # exists since, in my packages, it is treated as a Perl module
+ # distribution and has its own standalone test suite.
+ my @skip = $args_ref->{skip} ? @{ $args_ref->{skip} } : ();
+ push(@skip, @GLOBAL_SKIP, 'perl');
+
+ # Separate directories to skip under tests from top-level directories.
+ my @skip_tests = grep { m{ \A tests/ }xms } @skip;
+ @skip = grep { !m{ \A tests }xms } @skip;
+ for my $skip_dir (@skip_tests) {
+ $skip_dir =~ s{ \A tests/ }{}xms;
+ }
+
+ # Convert the skip lists into hashes for convenience.
+ my %skip = map { $_ => 1 } @skip, 'tests';
+ my %skip_tests = map { $_ => 1 } @skip_tests;
+
+ # Build the list of top-level directories to test.
+ opendir(my $rootdir, q{.}) or BAIL_OUT("cannot open .: $!");
+ my @dirs = grep { -d && !$skip{$_} } readdir($rootdir);
+ closedir($rootdir);
+ @dirs = File::Spec->no_upwards(@dirs);
+
+ # Add the list of subdirectories of the tests directory.
+ if (-d 'tests') {
+ opendir(my $testsdir, q{tests}) or BAIL_OUT("cannot open tests: $!");
+
+ # Skip if found in %skip_tests or if not a directory.
+ my $is_skipped = sub {
+ my ($dir) = @_;
+ return 1 if $skip_tests{$dir};
+ $dir = File::Spec->catdir('tests', $dir);
+ return -d $dir ? 0 : 1;
+ };
+
+ # Build the filtered list of subdirectories of tests.
+ my @test_dirs = grep { !$is_skipped->($_) } readdir($testsdir);
+ closedir($testsdir);
+ @test_dirs = File::Spec->no_upwards(@test_dirs);
+
+ # Add the tests directory to the start of the directory name.
+ push(@dirs, map { File::Spec->catdir('tests', $_) } @test_dirs);
+ }
+ return @dirs;
+}
+
+# Find a configuration file for the test suite. Searches relative to
+# C_TAP_BUILD first and then C_TAP_SOURCE and returns whichever is found
+# first. Calls BAIL_OUT if the file could not be found.
+#
+# $file - Partial path to the file
+#
+# Returns: Full path to the file
+sub test_file_path {
+ my ($file) = @_;
+ BASE:
+ for my $base ($ENV{C_TAP_BUILD}, $ENV{C_TAP_SOURCE}) {
+ next if !defined($base);
+ if (-e "$base/$file") {
+ return "$base/$file";
+ }
+ }
+ BAIL_OUT("cannot find $file");
+ return;
+}
+
+# Create a temporary directory for tests to use for transient files and return
+# the path to that directory. The directory is automatically removed on
+# program exit. The directory permissions use the current umask. Calls
+# BAIL_OUT if the directory could not be created.
+#
+# Returns: Path to a writable temporary directory
+sub test_tmpdir {
+ my $path;
+
+ # If we already figured out what directory to use, reuse the same path.
+ # Otherwise, create a directory relative to C_TAP_BUILD if set.
+ if (defined($TMPDIR)) {
+ $path = $TMPDIR;
+ } else {
+ my $base;
+ if (defined($ENV{C_TAP_BUILD})) {
+ $base = $ENV{C_TAP_BUILD};
+ } else {
+ $base = File::Spec->curdir;
+ }
+ $path = File::Spec->catdir($base, 'tmp');
+ }
+
+ # Create the directory if it doesn't exist.
+ if (!-d $path) {
+ if (!mkdir($path, 0777)) {
+ BAIL_OUT("cannot create directory $path: $!");
+ }
+ }
+
+ # Store the directory name for cleanup and return it.
+ $TMPDIR = $path;
+ return $path;
+}
+
+# On program exit, remove $TMPDIR if set and if possible. Report errors with
+# diag but otherwise ignore them.
+END {
+ if (defined($TMPDIR) && -d $TMPDIR) {
+ local $! = undef;
+ if (!rmdir($TMPDIR)) {
+ diag("cannot remove temporary directory $TMPDIR: $!");
+ }
+ }
+}
+
+1;
+__END__
+
+=for stopwords
+Allbery Automake Automake-aware Automake-based rra-c-util ARGS subdirectories
+sublicense MERCHANTABILITY NONINFRINGEMENT umask
+
+=head1 NAME
+
+Test::RRA::Automake - Automake-aware support functions for Perl tests
+
+=head1 SYNOPSIS
+
+ use Test::RRA::Automake qw(automake_setup perl_dirs test_file_path);
+ automake_setup({ chdir_build => 1 });
+
+ # Paths to directories that may contain Perl scripts.
+ my @dirs = perl_dirs({ skip => [qw(lib)] });
+
+ # Configuration for Kerberos tests.
+ my $keytab = test_file_path('config/keytab');
+
+=head1 DESCRIPTION
+
+This module collects utility functions that are useful for test scripts
+written in Perl and included in a C Automake-based package. They assume the
+layout of a package that uses rra-c-util and C TAP Harness for the test
+structure.
+
+Loading this module will also add the directories C<perl/blib/arch> and
+C<perl/blib/lib> to the Perl library search path, relative to C_TAP_BUILD if
+that environment variable is set. This is harmless for C Automake projects
+that don't contain an embedded Perl module, and for those projects that do,
+this will allow subsequent C<use> calls to find modules that are built as part
+of the package build process.
+
+The automake_setup() function should be called before calling any other
+functions provided by this module.
+
+=head1 FUNCTIONS
+
+None of these functions are imported by default. The ones used by a script
+should be explicitly imported. On failure, all of these functions call
+BAIL_OUT (from Test::More).
+
+=over 4
+
+=item all_files()
+
+Returns a list of all "interesting" files in the distribution that a test
+suite may want to look at. This excludes various products of the build system,
+the build directory if it's under the source directory, and a few other
+uninteresting directories like F<.git>. The returned paths will be paths
+relative to the root of the package.
+
+=item automake_setup([ARGS])
+
+Verifies that the C_TAP_BUILD and C_TAP_SOURCE environment variables are set
+and then changes directory to the top of the source tree (which is one
+directory up from the C_TAP_SOURCE path, since C_TAP_SOURCE points to the top
+of the tests directory).
+
+If ARGS is given, it should be a reference to a hash of configuration options.
+Only one option is supported: C<chdir_build>. If it is set to a true value,
+automake_setup() changes directories to the top of the build tree instead.
+
+=item perl_dirs([ARGS])
+
+Returns a list of directories that may contain Perl scripts that should be
+tested by test scripts that test all Perl in the source tree (such as syntax
+or coding style checks). The paths will be simple directory names relative to
+the current directory or two-part directory names under the F<tests>
+directory. (Directories under F<tests> are broken out separately since it's
+common to want to apply different policies to different subdirectories of
+F<tests>.)
+
+If ARGS is given, it should be a reference to a hash of configuration options.
+Only one option is supported: C<skip>, whose value should be a reference to an
+array of additional top-level directories or directories starting with
+C<tests/> that should be skipped.
+
+=item test_file_path(FILE)
+
+Given FILE, which should be a relative path, locates that file relative to the
+test directory in either the source or build tree. FILE will be checked for
+relative to the environment variable C_TAP_BUILD first, and then relative to
+C_TAP_SOURCE. test_file_path() returns the full path to FILE or calls
+BAIL_OUT if FILE could not be found.
+
+=item test_tmpdir()
+
+Create a temporary directory for tests to use for transient files and return
+the path to that directory. The directory is created relative to the
+C_TAP_BUILD environment variable, which must be set. Permissions on the
+directory are set using the current umask. test_tmpdir() returns the full
+path to the temporary directory or calls BAIL_OUT if it could not be created.
+
+The directory is automatically removed if possible on program exit. Failure
+to remove the directory on exit is reported with diag() and otherwise ignored.
+
+=back
+
+=head1 ENVIRONMENT
+
+=over 4
+
+=item C_TAP_BUILD
+
+The root of the tests directory in Automake build directory for this package,
+used to find files as documented above.
+
+=item C_TAP_SOURCE
+
+The root of the tests directory in the source tree for this package, used to
+find files as documented above.
+
+=back
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2014-2015, 2018-2021 Russ Allbery <eagle@eyrie.org>
+
+Copyright 2013 The Board of Trustees of the Leland Stanford Junior University
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=head1 SEE ALSO
+
+Test::More(3), Test::RRA(3), Test::RRA::Config(3)
+
+This module is maintained in the rra-c-util package. The current version is
+available from L<https://www.eyrie.org/~eagle/software/rra-c-util/>.
+
+The C TAP Harness test driver and libraries for TAP-based C testing are
+available from L<https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+
+=cut
+
+# Local Variables:
+# copyright-at-end-flag: t
+# End:
diff --git a/tests/tap/perl/Test/RRA/Config.pm b/tests/tap/perl/Test/RRA/Config.pm
new file mode 100644
index 000000000000..77f967e35b52
--- /dev/null
+++ b/tests/tap/perl/Test/RRA/Config.pm
@@ -0,0 +1,224 @@
+# Configuration for Perl test cases.
+#
+# In order to reuse the same Perl test cases in multiple packages, I use a
+# configuration file to store some package-specific data. This module loads
+# that configuration and provides the namespace for the configuration
+# settings.
+#
+# SPDX-License-Identifier: MIT
+
+package Test::RRA::Config;
+
+use 5.010;
+use base qw(Exporter);
+use strict;
+use warnings;
+
+use Test::More;
+
+# Declare variables that should be set in BEGIN for robustness.
+our (@EXPORT_OK, $VERSION);
+
+# Set $VERSION and everything export-related in a BEGIN block for robustness
+# against circular module loading (not that we load any modules, but
+# consistency is good).
+BEGIN {
+ @EXPORT_OK = qw(
+ $COVERAGE_LEVEL @COVERAGE_SKIP_TESTS @CRITIC_IGNORE $LIBRARY_PATH
+ $MINIMUM_VERSION %MINIMUM_VERSION @MODULE_VERSION_IGNORE
+ @POD_COVERAGE_EXCLUDE @STRICT_IGNORE @STRICT_PREREQ
+ );
+
+ # This version should match the corresponding rra-c-util release, but with
+ # two digits for the minor version, including a leading zero if necessary,
+ # so that it will sort properly.
+ $VERSION = '10.00';
+}
+
+# If C_TAP_BUILD or C_TAP_SOURCE are set in the environment, look for
+# data/perl.conf under those paths for a C Automake package. Otherwise, look
+# in t/data/perl.conf for a standalone Perl module or tests/data/perl.conf for
+# Perl tests embedded in a larger distribution. Don't use Test::RRA::Automake
+# since it may not exist.
+our $PATH;
+for my $base ($ENV{C_TAP_BUILD}, $ENV{C_TAP_SOURCE}, './t', './tests') {
+ next if !defined($base);
+ my $path = "$base/data/perl.conf";
+ if (-r $path) {
+ $PATH = $path;
+ last;
+ }
+}
+if (!defined($PATH)) {
+ BAIL_OUT('cannot find data/perl.conf');
+}
+
+# Pre-declare all of our variables and set any defaults.
+our $COVERAGE_LEVEL = 100;
+our @COVERAGE_SKIP_TESTS;
+our @CRITIC_IGNORE;
+our $LIBRARY_PATH;
+our $MINIMUM_VERSION = '5.010';
+our %MINIMUM_VERSION;
+our @MODULE_VERSION_IGNORE;
+our @POD_COVERAGE_EXCLUDE;
+our @STRICT_IGNORE;
+our @STRICT_PREREQ;
+
+# Load the configuration.
+if (!do($PATH)) {
+ my $error = $@ || $! || 'loading file did not return true';
+ BAIL_OUT("cannot load $PATH: $error");
+}
+
+1;
+__END__
+
+=for stopwords
+Allbery rra-c-util Automake perlcritic .libs namespace subdirectory sublicense
+MERCHANTABILITY NONINFRINGEMENT regexes
+
+=head1 NAME
+
+Test::RRA::Config - Perl test configuration
+
+=head1 SYNOPSIS
+
+ use Test::RRA::Config qw($MINIMUM_VERSION);
+ print "Required Perl version is $MINIMUM_VERSION\n";
+
+=head1 DESCRIPTION
+
+Test::RRA::Config encapsulates per-package configuration for generic Perl test
+programs that are shared between multiple packages using the rra-c-util
+infrastructure. It handles locating and loading the test configuration file
+for both C Automake packages and stand-alone Perl modules.
+
+Test::RRA::Config looks for a file named F<data/perl.conf> relative to the
+root of the test directory. That root is taken from the environment variables
+C_TAP_BUILD or C_TAP_SOURCE (in that order) if set, which will be the case for
+C Automake packages using C TAP Harness. If neither is set, it expects the
+root of the test directory to be a directory named F<t> relative to the
+current directory, which will be the case for stand-alone Perl modules.
+
+The following variables are supported:
+
+=over 4
+
+=item $COVERAGE_LEVEL
+
+The coverage level achieved by the test suite for Perl test coverage testing
+using Test::Strict, as a percentage. The test will fail if test coverage less
+than this percentage is achieved. If not given, defaults to 100.
+
+=item @COVERAGE_SKIP_TESTS
+
+Directories under F<t> whose tests should be skipped when doing coverage
+testing. This can be tests that won't contribute to coverage or tests that
+don't run properly under Devel::Cover for some reason (such as ones that use
+taint checking). F<docs> and F<style> will always be skipped regardless of
+this setting.
+
+=item @CRITIC_IGNORE
+
+Additional files or directories to ignore when doing recursive perlcritic
+testing. To ignore files that will be installed, the path should start with
+F<blib>.
+
+=item $LIBRARY_PATH
+
+Add this directory (or a F<.libs> subdirectory) relative to the top of the
+source tree to LD_LIBRARY_PATH when checking the syntax of Perl modules. This
+may be required to pick up libraries that are used by in-tree Perl modules so
+that Perl scripts can pass a syntax check.
+
+=item $MINIMUM_VERSION
+
+Default minimum version requirement for included Perl scripts. If not given,
+defaults to 5.010.
+
+=item %MINIMUM_VERSION
+
+Minimum version exceptions for specific directories. The keys should be
+minimum versions of Perl to enforce. The value for each key should be a
+reference to an array of either top-level directory names or directory names
+starting with F<tests/>. All files in those directories will have that
+minimum Perl version constraint imposed instead of $MINIMUM_VERSION.
+
+=item @MODULE_VERSION_IGNORE
+
+File names to ignore when checking that all modules in a distribution have the
+same version. Sometimes, some specific modules need separate, special version
+handling, such as modules defining database schemata for DBIx::Class, and
+can't follow the version of the larger package.
+
+=item @POD_COVERAGE_EXCLUDE
+
+Regexes that match method names that should be excluded from POD coverage
+testing. Normally, all methods have to be documented in the POD for a Perl
+module, but methods matching any of these regexes will be considered private
+and won't require documentation.
+
+=item @STRICT_IGNORE
+
+Additional directories to ignore when doing recursive Test::Strict testing for
+C<use strict> and C<use warnings>. The contents of this directory must be
+either top-level directory names or directory names starting with F<tests/>.
+
+=item @STRICT_PREREQ
+
+A list of Perl modules that have to be available in order to do meaningful
+Test::Strict testing. If any of the modules cannot be loaded via C<use>,
+Test::Strict checking will be skipped. There is currently no way to require
+specific versions of the modules.
+
+=back
+
+No variables are exported by default, but the variables can be imported into
+the local namespace to avoid long variable names.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2015-2016, 2019, 2021 Russ Allbery <eagle@eyrie.org>
+
+Copyright 2013-2014 The Board of Trustees of the Leland Stanford Junior
+University
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=head1 SEE ALSO
+
+perlcritic(1), Test::MinimumVersion(3), Test::RRA(3), Test::RRA::Automake(3),
+Test::Strict(3)
+
+This module is maintained in the rra-c-util package. The current version is
+available from L<https://www.eyrie.org/~eagle/software/rra-c-util/>.
+
+The C TAP Harness test driver and libraries for TAP-based C testing are
+available from L<https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+
+=cut
+
+# Local Variables:
+# copyright-at-end-flag: t
+# End:
diff --git a/tests/tap/process.c b/tests/tap/process.c
new file mode 100644
index 000000000000..2f797f8f7567
--- /dev/null
+++ b/tests/tap/process.c
@@ -0,0 +1,532 @@
+/*
+ * Utility functions for tests that use subprocesses.
+ *
+ * Provides utility functions for subprocess manipulation. Specifically,
+ * provides a function, run_setup, which runs a command and bails if it fails,
+ * using its error message as the bail output, and is_function_output, which
+ * runs a function in a subprocess and checks its output and exit status
+ * against expected values.
+ *
+ * Requires an Autoconf probe for sys/select.h and a replacement for a missing
+ * mkstemp.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2002, 2004-2005, 2013, 2016-2017 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009-2011, 2013-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#ifdef HAVE_SYS_SELECT_H
+# include <sys/select.h>
+#endif
+#include <sys/stat.h>
+#ifdef HAVE_SYS_TIME_H
+# include <sys/time.h>
+#endif
+#include <sys/wait.h>
+#include <time.h>
+
+#include <tests/tap/basic.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+/* May be defined by the build system. */
+#ifndef PATH_FAKEROOT
+# define PATH_FAKEROOT ""
+#endif
+
+/* How long to wait for the process to start in seconds. */
+#define PROCESS_WAIT 10
+
+/*
+ * Used to store information about a background process. This contains
+ * everything required to stop the process and clean up after it.
+ */
+struct process {
+ pid_t pid; /* PID of child process */
+ char *pidfile; /* PID file to delete on process stop */
+ char *tmpdir; /* Temporary directory for log file */
+ char *logfile; /* Log file of process output */
+ bool is_child; /* Whether we can waitpid for process */
+ struct process *next; /* Next process in global list */
+};
+
+/*
+ * Global list of started processes, which will be cleaned up automatically on
+ * program exit if they haven't been explicitly stopped with process_stop
+ * prior to that point.
+ */
+static struct process *processes = NULL;
+
+
+/*
+ * Given a function, an expected exit status, and expected output, runs that
+ * function in a subprocess, capturing stdout and stderr via a pipe, and
+ * returns the function output in newly allocated memory. Also captures the
+ * process exit status.
+ */
+static void
+run_child_function(test_function_type function, void *data, int *status,
+ char **output)
+{
+ int fds[2];
+ pid_t child;
+ char *buf;
+ ssize_t count, ret, buflen;
+ int rval;
+
+ /* Flush stdout before we start to avoid odd forking issues. */
+ fflush(stdout);
+
+ /* Set up the pipe and call the function, collecting its output. */
+ if (pipe(fds) == -1)
+ sysbail("can't create pipe");
+ child = fork();
+ if (child == (pid_t) -1) {
+ sysbail("can't fork");
+ } else if (child == 0) {
+ /* In child. Set up our stdout and stderr. */
+ close(fds[0]);
+ if (dup2(fds[1], 1) == -1)
+ _exit(255);
+ if (dup2(fds[1], 2) == -1)
+ _exit(255);
+
+ /* Now, run the function and exit successfully if it returns. */
+ (*function)(data);
+ fflush(stdout);
+ _exit(0);
+ } else {
+ /*
+ * In the parent; close the extra file descriptor, read the output if
+ * any, and then collect the exit status.
+ */
+ close(fds[1]);
+ buflen = BUFSIZ;
+ buf = bmalloc(buflen);
+ count = 0;
+ do {
+ ret = read(fds[0], buf + count, buflen - count - 1);
+ if (SSIZE_MAX - count <= ret)
+ bail("maximum output size exceeded in run_child_function");
+ if (ret > 0)
+ count += ret;
+ if (count >= buflen - 1) {
+ buflen += BUFSIZ;
+ buf = brealloc(buf, buflen);
+ }
+ } while (ret > 0);
+ buf[count] = '\0';
+ if (waitpid(child, &rval, 0) == (pid_t) -1)
+ sysbail("waitpid failed");
+ close(fds[0]);
+ }
+
+ /* Store the output and return. */
+ *status = rval;
+ *output = buf;
+}
+
+
+/*
+ * Given a function, data to pass to that function, an expected exit status,
+ * and expected output, runs that function in a subprocess, capturing stdout
+ * and stderr via a pipe, and compare the combination of stdout and stderr
+ * with the expected output and the exit status with the expected status.
+ * Expects the function to always exit (not die from a signal).
+ */
+void
+is_function_output(test_function_type function, void *data, int status,
+ const char *output, const char *format, ...)
+{
+ char *buf, *msg;
+ int rval;
+ va_list args;
+
+ run_child_function(function, data, &rval, &buf);
+
+ /* Now, check the results against what we expected. */
+ va_start(args, format);
+ bvasprintf(&msg, format, args);
+ va_end(args);
+ ok(WIFEXITED(rval), "%s (exited)", msg);
+ is_int(status, WEXITSTATUS(rval), "%s (status)", msg);
+ is_string(output, buf, "%s (output)", msg);
+ free(buf);
+ free(msg);
+}
+
+
+/*
+ * A helper function for run_setup. This is a function to run an external
+ * command, suitable for passing into run_child_function. The expected
+ * argument must be an argv array, with argv[0] being the command to run.
+ */
+static void
+exec_command(void *data)
+{
+ char *const *argv = data;
+
+ execvp(argv[0], argv);
+}
+
+
+/*
+ * Given a command expressed as an argv struct, with argv[0] the name or path
+ * to the command, run that command. If it exits with a non-zero status, use
+ * the part of its output up to the first newline as the error message when
+ * calling bail.
+ */
+void
+run_setup(const char *const argv[])
+{
+ char *output, *p;
+ int status;
+
+ run_child_function(exec_command, (void *) argv, &status, &output);
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ p = strchr(output, '\n');
+ if (p != NULL)
+ *p = '\0';
+ if (output[0] != '\0')
+ bail("%s", output);
+ else
+ bail("setup command failed with no output");
+ }
+ free(output);
+}
+
+
+/*
+ * Free the resources associated with tracking a process, without doing
+ * anything to the process. This is kept separate so that we can free
+ * resources during shutdown in a non-primary process.
+ */
+static void
+process_free(struct process *process)
+{
+ struct process **prev;
+
+ /* Do nothing if called with a NULL argument. */
+ if (process == NULL)
+ return;
+
+ /* Remove the process from the global list. */
+ prev = &processes;
+ while (*prev != NULL && *prev != process)
+ prev = &(*prev)->next;
+ if (*prev == process)
+ *prev = process->next;
+
+ /* Free resources. */
+ free(process->pidfile);
+ free(process->logfile);
+ test_tmpdir_free(process->tmpdir);
+ free(process);
+}
+
+
+/*
+ * Kill a process and wait for it to exit. Returns the status of the process.
+ * Calls bail on a system failure or a failure of the process to exit.
+ *
+ * We are quite aggressive with error reporting here because child processes
+ * that don't exit or that don't exist often indicate some form of test
+ * failure.
+ */
+static int
+process_kill(struct process *process)
+{
+ int result, i;
+ int status = -1;
+ struct timeval tv;
+ unsigned long pid = process->pid;
+
+ /* If the process is not a child, just kill it and hope. */
+ if (!process->is_child) {
+ if (kill(process->pid, SIGTERM) < 0 && errno != ESRCH)
+ sysbail("cannot send SIGTERM to process %lu", pid);
+ return 0;
+ }
+
+ /* Check if the process has already exited. */
+ result = waitpid(process->pid, &status, WNOHANG);
+ if (result < 0)
+ sysbail("cannot wait for child process %lu", pid);
+ else if (result > 0)
+ return status;
+
+ /*
+ * Kill the process and wait for it to exit. I don't want to go to the
+ * work of setting up a SIGCHLD handler or a full event loop here, so we
+ * effectively poll every tenth of a second for process exit (and
+ * hopefully faster when it does since the SIGCHLD may interrupt our
+ * select, although we're racing with it.
+ */
+ if (kill(process->pid, SIGTERM) < 0 && errno != ESRCH)
+ sysbail("cannot send SIGTERM to child process %lu", pid);
+ for (i = 0; i < PROCESS_WAIT * 10; i++) {
+ tv.tv_sec = 0;
+ tv.tv_usec = 100000;
+ select(0, NULL, NULL, NULL, &tv);
+ result = waitpid(process->pid, &status, WNOHANG);
+ if (result < 0)
+ sysbail("cannot wait for child process %lu", pid);
+ else if (result > 0)
+ return status;
+ }
+
+ /* The process still hasn't exited. Bail. */
+ bail("child process %lu did not exit on SIGTERM", pid);
+
+ /* Not reached, but some compilers may get confused. */
+ return status;
+}
+
+
+/*
+ * Stop a particular process given its process struct. This kills the
+ * process, waits for it to exit if possible (giving it at most five seconds),
+ * and then removes it from the global processes struct so that it isn't
+ * stopped again during global shutdown.
+ */
+void
+process_stop(struct process *process)
+{
+ int status;
+ unsigned long pid = process->pid;
+
+ /* Stop the process. */
+ status = process_kill(process);
+
+ /* Call diag to flush logs as well as provide exit status. */
+ if (process->is_child)
+ diag("stopped process %lu (exit status %d)", pid, status);
+ else
+ diag("stopped process %lu", pid);
+
+ /* Remove the log and PID file. */
+ diag_file_remove(process->logfile);
+ unlink(process->pidfile);
+ unlink(process->logfile);
+
+ /* Free resources. */
+ process_free(process);
+}
+
+
+/*
+ * Stop all running processes. This is called as a cleanup handler during
+ * process shutdown. The first argument, which says whether the test was
+ * successful, is ignored, since the same actions should be performed
+ * regardless. The second argument says whether this is the primary process,
+ * in which case we do the full shutdown. Otherwise, we only free resources
+ * but don't stop the process.
+ */
+static void
+process_stop_all(int success UNUSED, int primary)
+{
+ while (processes != NULL) {
+ if (primary)
+ process_stop(processes);
+ else
+ process_free(processes);
+ }
+}
+
+
+/*
+ * Read the PID of a process from a file. This is necessary when running
+ * under fakeroot to get the actual PID of the remctld process.
+ */
+static pid_t
+read_pidfile(const char *path)
+{
+ FILE *file;
+ char buffer[BUFSIZ];
+ long pid;
+
+ file = fopen(path, "r");
+ if (file == NULL)
+ sysbail("cannot open %s", path);
+ if (fgets(buffer, sizeof(buffer), file) == NULL)
+ sysbail("cannot read from %s", path);
+ fclose(file);
+ pid = strtol(buffer, NULL, 10);
+ if (pid <= 0)
+ bail("cannot read PID from %s", path);
+ return (pid_t) pid;
+}
+
+
+/*
+ * Start a process and return its status information. The status information
+ * is also stored in the global processes linked list so that it can be
+ * stopped automatically on program exit.
+ *
+ * The boolean argument says whether to start the process under fakeroot. If
+ * true, PATH_FAKEROOT must be defined, generally by Autoconf. If it's not
+ * found, call skip_all.
+ *
+ * This is a helper function for process_start and process_start_fakeroot.
+ */
+static struct process *
+process_start_internal(const char *const argv[], const char *pidfile,
+ bool fakeroot)
+{
+ size_t i;
+ int log_fd;
+ const char *name;
+ struct timeval tv;
+ struct process *process;
+ const char **fakeroot_argv = NULL;
+ const char *path_fakeroot = PATH_FAKEROOT;
+
+ /* Check prerequisites. */
+ if (fakeroot && path_fakeroot[0] == '\0')
+ skip_all("fakeroot not found");
+
+ /* Create the process struct and log file. */
+ process = bcalloc(1, sizeof(struct process));
+ process->pidfile = bstrdup(pidfile);
+ process->tmpdir = test_tmpdir();
+ name = strrchr(argv[0], '/');
+ if (name != NULL)
+ name++;
+ else
+ name = argv[0];
+ basprintf(&process->logfile, "%s/%s.log.XXXXXX", process->tmpdir, name);
+ log_fd = mkstemp(process->logfile);
+ if (log_fd < 0)
+ sysbail("cannot create log file for %s", argv[0]);
+
+ /* If using fakeroot, rewrite argv accordingly. */
+ if (fakeroot) {
+ for (i = 0; argv[i] != NULL; i++)
+ ;
+ fakeroot_argv = bcalloc(2 + i + 1, sizeof(const char *));
+ fakeroot_argv[0] = path_fakeroot;
+ fakeroot_argv[1] = "--";
+ for (i = 0; argv[i] != NULL; i++)
+ fakeroot_argv[i + 2] = argv[i];
+ fakeroot_argv[i + 2] = NULL;
+ argv = fakeroot_argv;
+ }
+
+ /*
+ * Fork off the child process, redirect its standard output and standard
+ * error to the log file, and then exec the program.
+ */
+ process->pid = fork();
+ if (process->pid < 0)
+ sysbail("fork failed");
+ else if (process->pid == 0) {
+ if (dup2(log_fd, STDOUT_FILENO) < 0)
+ sysbail("cannot redirect standard output");
+ if (dup2(log_fd, STDERR_FILENO) < 0)
+ sysbail("cannot redirect standard error");
+ close(log_fd);
+ if (execv(argv[0], (char *const *) argv) < 0)
+ sysbail("exec of %s failed", argv[0]);
+ }
+ close(log_fd);
+ free(fakeroot_argv);
+
+ /*
+ * In the parent. Wait for the child to start by watching for the PID
+ * file to appear in 100ms intervals.
+ */
+ for (i = 0; i < PROCESS_WAIT * 10 && access(pidfile, F_OK) != 0; i++) {
+ tv.tv_sec = 0;
+ tv.tv_usec = 100000;
+ select(0, NULL, NULL, NULL, &tv);
+ }
+
+ /*
+ * If the PID file still hasn't appeared after ten seconds, attempt to
+ * kill the process and then bail.
+ */
+ if (access(pidfile, F_OK) != 0) {
+ kill(process->pid, SIGTERM);
+ alarm(5);
+ waitpid(process->pid, NULL, 0);
+ alarm(0);
+ bail("cannot start %s", argv[0]);
+ }
+
+ /*
+ * Read the PID back from the PID file. This usually isn't necessary for
+ * non-forking daemons, but always doing this makes this function general,
+ * and it's required when running under fakeroot.
+ */
+ if (fakeroot)
+ process->pid = read_pidfile(pidfile);
+ process->is_child = !fakeroot;
+
+ /* Register the log file as a source of diag messages. */
+ diag_file_add(process->logfile);
+
+ /*
+ * Add the process to our global list and set our cleanup handler if this
+ * is the first process we started.
+ */
+ if (processes == NULL)
+ test_cleanup_register(process_stop_all);
+ process->next = processes;
+ processes = process;
+
+ /* All done. */
+ return process;
+}
+
+
+/*
+ * Start a process and return the opaque process struct. The process must
+ * create pidfile with its PID when startup is complete.
+ */
+struct process *
+process_start(const char *const argv[], const char *pidfile)
+{
+ return process_start_internal(argv, pidfile, false);
+}
+
+
+/*
+ * Start a process under fakeroot and return the opaque process struct. If
+ * fakeroot is not available, calls skip_all. The process must create pidfile
+ * with its PID when startup is complete.
+ */
+struct process *
+process_start_fakeroot(const char *const argv[], const char *pidfile)
+{
+ return process_start_internal(argv, pidfile, true);
+}
diff --git a/tests/tap/process.h b/tests/tap/process.h
new file mode 100644
index 000000000000..4210c209ed0b
--- /dev/null
+++ b/tests/tap/process.h
@@ -0,0 +1,95 @@
+/*
+ * Utility functions for tests that use subprocesses.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009-2010, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_PROCESS_H
+#define TAP_PROCESS_H 1
+
+#include <config.h>
+#include <tests/tap/macros.h>
+
+/* Opaque data type for process_start and friends. */
+struct process;
+
+BEGIN_DECLS
+
+/*
+ * Run a function in a subprocess and check the exit status and expected
+ * output (stdout and stderr combined) against the provided values. Expects
+ * the function to always exit (not die from a signal). data is optional data
+ * that's passed into the function as its only argument.
+ *
+ * This reports as three separate tests: whether the function exited rather
+ * than was killed, whether the exit status was correct, and whether the
+ * output was correct.
+ */
+typedef void (*test_function_type)(void *);
+void is_function_output(test_function_type, void *data, int status,
+ const char *output, const char *format, ...)
+ __attribute__((__format__(printf, 5, 6), __nonnull__(1)));
+
+/*
+ * Run a setup program. Takes the program to run and its arguments as an argv
+ * vector, where argv[0] must be either the full path to the program or the
+ * program name if the PATH should be searched. If the program does not exit
+ * successfully, call bail, with the error message being the output from the
+ * program.
+ */
+void run_setup(const char *const argv[]) __attribute__((__nonnull__));
+
+/*
+ * process_start starts a process in the background, returning an opaque data
+ * struct that can be used to stop the process later. The standard output and
+ * standard error of the process will be sent to a log file registered with
+ * diag_file_add, so its output will be properly interleaved with the test
+ * case output.
+ *
+ * The process should create a PID file in the path given as the second
+ * argument when it's finished initialization.
+ *
+ * process_start_fakeroot is the same but starts the process under fakeroot.
+ * PATH_FAKEROOT must be defined (generally by Autoconf). If fakeroot is not
+ * found, process_start_fakeroot will call skip_all, so be sure to call this
+ * function before plan.
+ *
+ * process_stop can be called to explicitly stop the process. If it isn't
+ * called by the test program, it will be called automatically when the
+ * program exits.
+ */
+struct process *process_start(const char *const argv[], const char *pidfile)
+ __attribute__((__nonnull__));
+struct process *process_start_fakeroot(const char *const argv[],
+ const char *pidfile)
+ __attribute__((__nonnull__));
+void process_stop(struct process *);
+
+END_DECLS
+
+#endif /* TAP_PROCESS_H */
diff --git a/tests/tap/string.c b/tests/tap/string.c
new file mode 100644
index 000000000000..71cf571e6f03
--- /dev/null
+++ b/tests/tap/string.c
@@ -0,0 +1,67 @@
+/*
+ * String utilities for the TAP protocol.
+ *
+ * Additional string utilities that can't be included with C TAP Harness
+ * because they rely on additional portability code from rra-c-util.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Copyright 2011-2012 Russ Allbery <eagle@eyrie.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+
+/*
+ * vsprintf into a newly allocated string, reporting a fatal error with bail
+ * on failure.
+ */
+void
+bvasprintf(char **strp, const char *fmt, va_list args)
+{
+ int status;
+
+ status = vasprintf(strp, fmt, args);
+ if (status < 0)
+ sysbail("failed to allocate memory for vasprintf");
+}
+
+
+/*
+ * sprintf into a newly allocated string, reporting a fatal error with bail on
+ * failure.
+ */
+void
+basprintf(char **strp, const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ bvasprintf(strp, fmt, args);
+ va_end(args);
+}
diff --git a/tests/tap/string.h b/tests/tap/string.h
new file mode 100644
index 000000000000..651c38a26f06
--- /dev/null
+++ b/tests/tap/string.h
@@ -0,0 +1,51 @@
+/*
+ * String utilities for the TAP protocol.
+ *
+ * Additional string utilities that can't be included with C TAP Harness
+ * because they rely on additional portability code from rra-c-util.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Copyright 2011-2012 Russ Allbery <eagle@eyrie.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_STRING_H
+#define TAP_STRING_H 1
+
+#include <config.h>
+#include <tests/tap/macros.h>
+
+#include <stdarg.h> /* va_list */
+
+BEGIN_DECLS
+
+/* sprintf into an allocated string, calling bail on any failure. */
+void basprintf(char **, const char *, ...)
+ __attribute__((__nonnull__, __format__(printf, 2, 3)));
+void bvasprintf(char **, const char *, va_list)
+ __attribute__((__nonnull__, __format__(printf, 2, 0)));
+
+END_DECLS
+
+#endif /* !TAP_STRING_H */
diff --git a/tests/valgrind/logs-t b/tests/valgrind/logs-t
new file mode 100755
index 000000000000..5444d00a5730
--- /dev/null
+++ b/tests/valgrind/logs-t
@@ -0,0 +1,83 @@
+#!/usr/bin/perl
+#
+# Check for errors in valgrind logs.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Copyright 2018-2019, 2021 Russ Allbery <eagle@eyrie.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA;
+use Test::RRA::Automake qw(automake_setup);
+
+use File::Spec;
+use Test::More;
+
+# Skip this test if C_TAP_VALGRIND was not set.
+if (!exists $ENV{C_TAP_VALGRIND}) {
+ plan skip_all => 'Not testing under valgrind';
+}
+
+# Set up Automake testing.
+automake_setup({ chdir_build => 1 });
+
+# Gather the list of valgrind logs (and skip this test if there are none).
+opendir(my $logdir, File::Spec->catfile('tests', 'tmp', 'valgrind'))
+ or plan skip_all => 'No valgrind logs in tests/tmp/valgrind';
+my @logs = grep { m{ \A log [.] }xms } readdir $logdir;
+closedir($logdir) or BAIL_OUT("cannot close directory: $!");
+
+# Check each log file.
+plan tests => scalar(@logs);
+for my $file (@logs) {
+ my $path = File::Spec->catfile('tests', 'tmp', 'valgrind', $file);
+ open(my $log, '<', $path) or BAIL_OUT("cannot open $path: $!");
+ my $okay = 1;
+ my @log;
+ while (defined(my $line = <$log>)) {
+ push(@log, $line);
+ if ($line =~ m{ ERROR [ ] SUMMARY: [ ] (\d+) [ ] errors }xms) {
+ $okay = ($1 == 0);
+ }
+ }
+ close($log) or BAIL_OUT("cannot close $path: $!");
+ if ($okay) {
+ unlink($path);
+ } else {
+ for my $line (@log) {
+ print '# ', $line
+ or BAIL_OUT("cannot print to standard output: $!");
+ }
+ }
+ ok($okay, $path);
+}
+
+# Remove tests/tmp/valgrind if it's now empty.
+rmdir(File::Spec->catfile('tests', 'tmp', 'valgrind'));
+rmdir(File::Spec->catfile('tests', 'tmp'));