diff options
author | Cy Schubert <cy@FreeBSD.org> | 2025-04-17 02:13:41 +0000 |
---|---|---|
committer | Cy Schubert <cy@FreeBSD.org> | 2025-05-27 16:20:06 +0000 |
commit | 24f0b4ca2d565cdbb4fe7839ff28320706bf2386 (patch) | |
tree | bc9ce87edb73f767f5580887d0fc8c643b9d7a49 |
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 @@ -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 + +[](https://github.com/rra/pam-krb5/actions) +[](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. @@ -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(®ex, 0, sizeof(regex)); + status = regcomp(®ex, wanted, REG_EXTENDED | REG_NOSUB); + if (status != 0) { + regerror(status, ®ex, err, sizeof(err)); + bail("invalid regex /%s/: %s", wanted, err); + } + status = regexec(®ex, 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, ®ex, err, sizeof(err)); + bail("regexec failed for regex /%s/: %s", wanted, err); + } + regfree(®ex); +} +#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(¶ms, 0, sizeof(params)); + params.realm = (char *) realm; + params.mask = KADM5_CONFIG_REALM; + code = kadm5_init_with_skey_ctx(ctx, user, path, KADM5_ADMIN_SERVICE, + ¶ms, 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')); |