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
pam-krb5: Import/add pam-krb5 from eyeire.orgvendor/pam-krb5/4.11vendor/pam-krb5
From https://www.eyrie.org/~eagle/software/pam-krb5/: 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. The reason for this import is to provide an MIT KRB5 compatible pam_krb5 PAM module. The existing pam_krb5 in FreeBS only works with Heimdal. Sponsored by: The FreeBSD Foundation
-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'));