=== added directory '.bzr-builddeb' === added file '.bzr-builddeb/default.conf' --- .bzr-builddeb/default.conf 1970-01-01 00:00:00 +0000 +++ .bzr-builddeb/default.conf 2008-09-17 00:34:09 +0000 @@ -0,0 +1,2 @@ +[BUILDDEB] +split = True === added file '.bzrignore' --- .bzrignore 1970-01-01 00:00:00 +0000 +++ .bzrignore 2019-07-27 10:11:45 +0000 @@ -0,0 +1,17 @@ +*.5 +*.8 +*.8mandos +confdir +keydir +statedir +man +plugin-runner +plugins.d/askpass-fifo +plugins.d/mandos-client +plugins.d/password-prompt +plugins.d/splashy +plugins.d/usplash +plugins.d/plymouth +plugin-helpers/mandos-client-iprouteadddel +dracut-module/password-agent +.tramp_history === added file 'COPYING' --- COPYING 1970-01-01 00:00:00 +0000 +++ COPYING 2008-08-15 20:17:32 +0000 @@ -0,0 +1,676 @@ + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + === added file 'DBUS-API' --- DBUS-API 1970-01-01 00:00:00 +0000 +++ DBUS-API 2019-02-10 04:20:26 +0000 @@ -0,0 +1,155 @@ + -*- mode: org; coding: utf-8 -*- + + Mandos Server D-Bus Interface + +This file documents the D-Bus interface to the Mandos server. + +* Bus: System bus + Bus name: "se.recompile.Mandos" + + +* Object Paths: + + | Path | Object | + |-----------------------+-------------------| + | "/" | The Mandos Server | + + (To get a list of paths to client objects, use the standard D-Bus + org.freedesktop.DBus.ObjectManager interface, which the server + object supports.) + + +* Mandos Server Interface: + Interface name: "se.recompile.Mandos" + +** Methods: +*** RemoveClient(o: ObjectPath) → nothing + Removes a client + +** Signals: +*** ClientNotFound(s: KeyID, s: Address) + A client connected from Address using KeyID, but was + rejected because it was not found in the server. The key ID + is represented as a string of hexadecimal digits. The address is + an IPv4 or IPv6 address in its normal string format. + + +* Mandos Client Interface: + Interface name: "se.recompile.Mandos.Client" + +** Methods +*** Approve(b: Approve) → nothing + Approve or deny a connected client waiting for approval. If + denied, a client will not be sent its secret. + +*** CheckedOK() → nothing + Assert that this client has been checked and found to be alive. + This will restart the timeout before disabling this client. See + also the "LastCheckedOK" property. + +** Properties + + Note: Many of these properties directly correspond to a setting in + "clients.conf", in which case they are fully documented in + mandos-clients.conf(5). + + | Name | Type | Access | clients.conf | + |-------------------------+------+------------+---------------------| + | ApprovedByDefault | b | Read/Write | approved_by_default | + | ApprovalDelay (a) | t | Read/Write | approval_delay | + | ApprovalDuration (a) | t | Read/Write | approval_duration | + | ApprovalPending (b) | b | Read | N/A | + | Checker | s | Read/Write | checker | + | CheckerRunning (c) | b | Read/Write | N/A | + | Created (d) | s | Read | N/A | + | Enabled (e) | b | Read/Write | N/A | + | Expires (f) | s | Read | N/A | + | ExtendedTimeout (a) | t | Read/Write | extended_timeout | + | Fingerprint | s | Read | fingerprint | + | KeyID | s | Read | key_id | + | Host | s | Read/Write | host | + | Interval (a) | t | Read/Write | interval | + | LastApprovalRequest (g) | s | Read | N/A | + | LastCheckedOK (h) | s | Read/Write | N/A | + | LastCheckerStatus (i) | n | Read | N/A | + | LastEnabled (j) | s | Read | N/A | + | Name | s | Read | (Section name) | + | Secret (k) | ay | Write | secret (or secfile) | + | Timeout (a) | t | Read/Write | timeout | + + a) Represented as milliseconds. + + b) An approval is currently pending. + + c) Changing this property can either start a new checker or abort a + running one. + + d) The creation time of this client object, as an RFC 3339 string. + + e) Changing this property enables or disables a client. + + f) The date and time this client will be disabled, as an RFC 3339 + string, or an empty string if this is not scheduled. + + g) The date and time of the last approval request, as an RFC 3339 + string, or an empty string if this has not happened. + + h) The date and time a checker was last successful, as an RFC 3339 + string, or an empty string if this has not happened. Setting + this property is equivalent to calling CheckedOK(), i.e. the + current time is set, regardless of the string sent. Please + always use an empty string when setting this property, to allow + for possible future expansion. + + i) The exit status of the last checker, -1 if it did not exit + cleanly, -2 if a checker has not yet returned. + + j) The date and time this client was last enabled, as an RFC 3339 + string, or an empty string if this has not happened. + + k) A raw byte array, not hexadecimal digits. + +** Signals +*** CheckerCompleted(n: Exitcode, x: Waitstatus, s: Command) + A checker (Command) has completed. Exitcode is either the exit + code or -1 for abnormal exit. In any case, the full Waitstatus + (as from wait(2)) is also available. + +*** CheckerStarted(s: Command) + A checker command (Command) has just been started. + +*** GotSecret() + This client has been sent its secret. + +*** NeedApproval(t: Timeout, b: ApprovedByDefault) + This client will be approved or denied in exactly Timeout + milliseconds, depending on ApprovedByDefault. Approve() can now + usefully be called on this client object. + +*** Rejected(s: Reason) + This client was not given its secret for a specified Reason. + +* Copyright + + Copyright © 2010-2019 Teddy Hogeborn + Copyright © 2010-2019 Björn Påhlsson + +** License: + + This file is part of Mandos. + + Mandos 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. + + Mandos 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 Mandos. If not, see . + + +#+STARTUP: showall === added file 'INSTALL' --- INSTALL 1970-01-01 00:00:00 +0000 +++ INSTALL 2019-11-03 19:17:57 +0000 @@ -0,0 +1,157 @@ +-*- org -*- + +* Prerequisites + +** Operating System + + Debian 8.0 "jessie" or Ubuntu 15.10 "Wily Werewolf" (or later). + + This is mostly for the support scripts which make sure that the + client is installed and started in the initial RAM disk environment + and that the initial RAM file system image file is automatically + made unreadable. The server and client programs themselves *could* + be run in other distributions, but they *are* specific to GNU/Linux + systems, and are not written with portabillity to other Unixes in + mind. + +** Libraries + + The following libraries and packages are needed. (It is possible + that it might work with older versions of some of these, but these + versions are confirmed to work. Newer versions are almost + certainly OK.) + +*** Documentation + These are required to build the manual pages for both the server + and client: + + + DocBook 4.5 http://www.docbook.org/ + Note: DocBook 5.0 is not compatible. + + DocBook XSL stylesheets 1.71.0 + http://wiki.docbook.org/DocBookXslStylesheets + + Package names: + docbook docbook-xsl + + To build just the documentation, run the command "make doc". Then + the manual page "mandos.8", for example, can be read by running + "man -l mandos.8". + +*** Mandos Server + + GnuTLS 3.3 https://www.gnutls.org/ + (but not 3.6.0 or later, until 3.6.6, which works) + + Avahi 0.6.16 https://www.avahi.org/ + + Python 3 https://www.python.org/ + Note: Python 2.7 is still supported, if the "mandos", + "mandos-ctl", and "mandos-monitor" files are edited to contain + "#!/usr/bin/python" instead of python3. + + dbus-python 0.82.4 https://dbus.freedesktop.org/doc/dbus-python/ + + PyGObject 3.8 https://wiki.gnome.org/Projects/PyGObject + + pkg-config https://www.freedesktop.org/wiki/Software/pkg-config/ + + Urwid 1.0.1 http://urwid.org/ + (Only needed by the "mandos-monitor" tool.) + + Strongly recommended: + + fping 2.4b2-to-ipv6 http://www.fping.org/ + + ssh-keyscan from OpenSSH http://www.openssh.com/ + + Package names: + avahi-daemon python3 python3-dbus python3-gi python3-urwid + pkg-config fping ssh-client + +*** Mandos Client + + GNU C Library 2.17 https://gnu.org/software/libc/ + + GnuTLS 3.3 https://www.gnutls.org/ + (but not 3.6.0 or later, until 3.6.6 which works) + + Avahi 0.6.16 https://www.avahi.org/ + + GnuPG 1.4.9 https://www.gnupg.org/ + + GPGME 1.1.6 https://www.gnupg.org/related_software/gpgme/ + + pkg-config https://www.freedesktop.org/wiki/Software/pkg-config/ + + libnl-route 3 https://www.infradead.org/~tgr/libnl/ + + GLib 2.40 http://www.gtk.org/ + + One of: + + initramfs-tools 0.85i + https://tracker.debian.org/pkg/initramfs-tools + + dracut 044+241 + http://www.kernel.org/pub/linux/utils/boot/dracut/dracut.html + + Strongly recommended: + + OpenSSH http://www.openssh.com/ + + Package names: + initramfs-tools dracut libgnutls-dev gnutls-bin libavahi-core-dev + gnupg libgpgme11-dev pkg-config ssh libnl-route-3-dev + libglib2.0-dev + +* Installing the Mandos server + + 1. Do "make doc". + + 2. On the computer to run as a Mandos server, run the following + command: + For Debian: su - -c 'make install-server' + For Ubuntu: sudo make install-server + + (This creates a configuration without any clients configured; you + need an actually configured client to do that; see below.) + +* Installing the Mandos client. + + 1. Do "make all doc". + + 2. On the computer to run as a Mandos client, run the following + command: + For Debian: su - -c 'make install-client' + For Ubuntu: sudo make install-client + + This will also create an OpenPGP key, which will take some time + and entropy, so be patient. + + 3. Run the following command: + For Debian: su - -c 'mandos-keygen --password' + For Ubuntu: sudo mandos-keygen --password + + When prompted, enter the password/passphrase for the encrypted + root file system on this client computer. The command will + output a section of text, starting with a [section header]. Copy + and append this to the file "/etc/mandos/clients.conf" *on the + server computer*. + + 4. Configure the client to use any special configuration needed for + your local system. Note: This is not necessary if the server is + present on the same wired local network as the client. If you do + make changes to /etc/mandos/plugin-runner.conf, the initrd.img + file must be updated, possibly using the following command: + + # update-initramfs -k all -u + + 5. On the server computer, start the server by running the command + For Debian: su - -c 'invoke-rc.d mandos start' + For Ubuntu: sudo service mandos start + + At this point, it is possible to verify that the correct password + will be received by the client by running the command: + + # /usr/lib/mandos/plugins.d/mandos-client \ + --pubkey=/etc/keys/mandos/pubkey.txt \ + --seckey=/etc/keys/mandos/seckey.txt \ + --tls-privkey=/etc/keys/mandos/tls-privkey.pem \ + --tls-pubkey=/etc/keys/mandos/tls-pubkey.pem; echo + + This command should retrieve the password from the server, + decrypt it, and output it to standard output. + + After this, the client computer should be able to reboot without + needing a password entered on the console, as long as it does not + take more than five minutes to reboot. + +* Further customizations + + You may want to tighten or loosen the timeouts in the server + configuration files; see mandos.conf(5) and mandos-clients.conf(5). + If IPsec is not used and SSH is not installed, it is suggested that + a more cryptographically secure checker program is used and + configured, since, without IPsec, ping packets can be faked. + +#+STARTUP: showall === modified file 'Makefile' --- Makefile 2008-07-22 01:59:47 +0000 +++ Makefile 2019-09-03 19:06:41 +0000 @@ -1,28 +1,605 @@ -WARN=-O -Wall -Wformat=2 -Winit-self -Wmissing-include-dirs -Wswitch-default -Wswitch-enum -Wunused-parameter -Wstrict-aliasing=2 -Wextra -Wfloat-equal -Wundef -Wshadow -Wunsafe-loop-optimizations -Wpointer-arith -Wbad-function-cast -Wcast-qual -Wcast-align -Wwrite-strings -Wconversion -Wstrict-prototypes -Wold-style-definition -Wpacked -Wnested-externs -Wunreachable-code -Winline -Wvolatile-register-var -DEBUG=-ggdb3 +WARN:=-O -Wall -Wextra -Wdouble-promotion -Wformat=2 -Winit-self \ + -Wmissing-include-dirs -Wswitch-default -Wswitch-enum \ + -Wunused -Wuninitialized -Wstrict-overflow=5 \ + -Wsuggest-attribute=pure -Wsuggest-attribute=const \ + -Wsuggest-attribute=noreturn -Wfloat-equal -Wundef -Wshadow \ + -Wunsafe-loop-optimizations -Wpointer-arith \ + -Wbad-function-cast -Wcast-qual -Wcast-align -Wwrite-strings \ + -Wconversion -Wlogical-op -Waggregate-return \ + -Wstrict-prototypes -Wold-style-definition \ + -Wmissing-format-attribute -Wnormalized=nfc -Wpacked \ + -Wredundant-decls -Wnested-externs -Winline -Wvla \ + -Wvolatile-register-var -Woverlength-strings + +#DEBUG:=-ggdb3 -fsanitize=address $(SANITIZE) +## Check which sanitizing options can be used +#SANITIZE:=$(foreach option,$(ALL_SANITIZE_OPTIONS),$(shell \ +# echo 'int main(){}' | $(CC) --language=c $(option) \ +# /dev/stdin -o /dev/null >/dev/null 2>&1 && echo $(option))) +# +ALL_SANITIZE_OPTIONS:=-fsanitize=leak -fsanitize=undefined \ + -fsanitize=shift -fsanitize=integer-divide-by-zero \ + -fsanitize=unreachable -fsanitize=vla-bound -fsanitize=null \ + -fsanitize=return -fsanitize=signed-integer-overflow \ + -fsanitize=bounds -fsanitize=alignment \ + -fsanitize=object-size -fsanitize=float-divide-by-zero \ + -fsanitize=float-cast-overflow -fsanitize=nonnull-attribute \ + -fsanitize=returns-nonnull-attribute -fsanitize=bool \ + -fsanitize=enum -fsanitize-address-use-after-scope + +# For info about _FORTIFY_SOURCE, see feature_test_macros(7) +# and . +FORTIFY:=-D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIC +LINK_FORTIFY_LD:=-z relro -z now +LINK_FORTIFY:= + +# If BROKEN_PIE is set, do not build with -pie +ifndef BROKEN_PIE +FORTIFY += -fPIE +LINK_FORTIFY += -pie +endif #COVERAGE=--coverage -OPTIMIZE=-Os -LANGUAGE=-std=gnu99 +OPTIMIZE:=-Os -fno-strict-aliasing +LANGUAGE:=-std=gnu11 +FEATURES:=-D_FILE_OFFSET_BITS=64 +htmldir:=man +version:=1.8.9 +SED:=sed +PKG_CONFIG?=pkg-config + +USER:=$(firstword $(subst :, ,$(shell getent passwd _mandos \ + || getent passwd nobody || echo 65534))) +GROUP:=$(firstword $(subst :, ,$(shell getent group _mandos \ + || getent group nogroup || echo 65534))) + +LINUXVERSION:=$(shell uname --kernel-release) + +## Use these settings for a traditional /usr/local install +# PREFIX:=$(DESTDIR)/usr/local +# CONFDIR:=$(DESTDIR)/etc/mandos +# KEYDIR:=$(DESTDIR)/etc/mandos/keys +# MANDIR:=$(PREFIX)/man +# INITRAMFSTOOLS:=$(DESTDIR)/etc/initramfs-tools +# DRACUTMODULE:=$(DESTDIR)/usr/lib/dracut/modules.d/90mandos +# STATEDIR:=$(DESTDIR)/var/lib/mandos +# LIBDIR:=$(PREFIX)/lib +## + +## These settings are for a package-type install +PREFIX:=$(DESTDIR)/usr +CONFDIR:=$(DESTDIR)/etc/mandos +KEYDIR:=$(DESTDIR)/etc/keys/mandos +MANDIR:=$(PREFIX)/share/man +INITRAMFSTOOLS:=$(DESTDIR)/usr/share/initramfs-tools +DRACUTMODULE:=$(DESTDIR)/usr/lib/dracut/modules.d/90mandos +STATEDIR:=$(DESTDIR)/var/lib/mandos +LIBDIR:=$(shell \ + for d in \ + "/usr/lib/`dpkg-architecture \ + -qDEB_HOST_MULTIARCH 2>/dev/null`" \ + "`rpm --eval='%{_libdir}' 2>/dev/null`" /usr/lib; do \ + if [ -d "$$d" -a "$$d" = "$${d%/}" ]; then \ + echo "$(DESTDIR)$$d"; \ + break; \ + fi; \ + done) +## + +SYSTEMD:=$(DESTDIR)$(shell $(PKG_CONFIG) systemd \ + --variable=systemdsystemunitdir) +TMPFILES:=$(DESTDIR)$(shell $(PKG_CONFIG) systemd \ + --variable=tmpfilesdir) +SYSUSERS:=$(DESTDIR)$(shell $(PKG_CONFIG) systemd \ + --variable=sysusersdir) + +GNUTLS_CFLAGS:=$(shell $(PKG_CONFIG) --cflags-only-I gnutls) +GNUTLS_LIBS:=$(shell $(PKG_CONFIG) --libs gnutls) +AVAHI_CFLAGS:=$(shell $(PKG_CONFIG) --cflags-only-I avahi-core) +AVAHI_LIBS:=$(shell $(PKG_CONFIG) --libs avahi-core) +GPGME_CFLAGS:=$(shell gpgme-config --cflags; getconf LFS_CFLAGS) +GPGME_LIBS:=$(shell gpgme-config --libs; getconf LFS_LIBS; \ + getconf LFS_LDFLAGS) +LIBNL3_CFLAGS:=$(shell $(PKG_CONFIG) --cflags-only-I libnl-route-3.0) +LIBNL3_LIBS:=$(shell $(PKG_CONFIG) --libs libnl-route-3.0) +GLIB_CFLAGS:=$(shell $(PKG_CONFIG) --cflags glib-2.0) +GLIB_LIBS:=$(shell $(PKG_CONFIG) --libs glib-2.0) # Do not change these two -CFLAGS=$(WARN) $(COVERAGE) $(DEBUG) $(OPTIMIZE) $(LANGUAGE) -LDFLAGS=$(COVERAGE) - -PROGS=plugbasedclient plugins.d/mandosclient plugins.d/passprompt - -objects=$(shell for p in $(PROGS); do echo $${p}.o; done) - -all: $(PROGS) - -plugbasedclient: plugbasedclient.o - $(LINK.o) -lgnutls $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@ - -plugins.d/mandosclient: plugins.d/mandosclient.o - $(LINK.o) -lgnutls -lavahi-core -lgpgme $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@ - -plugins.d/passprompt: plugins.d/passprompt.o - $(LINK.o) $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@ - -.PHONY : clean +CFLAGS+=$(WARN) $(DEBUG) $(FORTIFY) $(COVERAGE) $(OPTIMIZE) \ + $(LANGUAGE) $(FEATURES) -DVERSION='"$(version)"' +LDFLAGS+=-Xlinker --as-needed $(COVERAGE) $(LINK_FORTIFY) $(strip \ + ) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag)) + +# Commands to format a DocBook document into a manual page +DOCBOOKTOMAN=$(strip cd $(dir $<); xsltproc --nonet --xinclude \ + --param man.charmap.use.subset 0 \ + --param make.year.ranges 1 \ + --param make.single.year.ranges 1 \ + --param man.output.quietly 1 \ + --param man.authors.section.enabled 0 \ + /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \ + $(notdir $<); \ + if locale --all 2>/dev/null | grep --regexp='^en_US\.utf8$$' \ + && command -v man >/dev/null; then LANG=en_US.UTF-8 \ + MANWIDTH=80 man --warnings --encoding=UTF-8 --local-file \ + $(notdir $@); fi >/dev/null) + +DOCBOOKTOHTML=$(strip xsltproc --nonet --xinclude \ + --param make.year.ranges 1 \ + --param make.single.year.ranges 1 \ + --param man.output.quietly 1 \ + --param man.authors.section.enabled 0 \ + --param citerefentry.link 1 \ + --output $@ \ + /usr/share/xml/docbook/stylesheet/nwalsh/xhtml/docbook.xsl \ + $<; $(HTMLPOST) $@) +# Fix citerefentry links +HTMLPOST:=$(SED) --in-place \ + --expression='s/\(\)\([^<]*\)\(<\/span>(\)\([^)]*\)\()<\/span><\/a>\)/\1\3.\5\2\3\4\5\6/g' + +PLUGINS:=plugins.d/password-prompt plugins.d/mandos-client \ + plugins.d/usplash plugins.d/splashy plugins.d/askpass-fifo \ + plugins.d/plymouth +PLUGIN_HELPERS:=plugin-helpers/mandos-client-iprouteadddel +CPROGS:=plugin-runner dracut-module/password-agent $(PLUGINS) \ + $(PLUGIN_HELPERS) +PROGS:=mandos mandos-keygen mandos-ctl mandos-monitor $(CPROGS) +DOCS:=mandos.8 mandos-keygen.8 mandos-monitor.8 mandos-ctl.8 \ + mandos.conf.5 mandos-clients.conf.5 plugin-runner.8mandos \ + dracut-module/password-agent.8mandos \ + plugins.d/mandos-client.8mandos \ + plugins.d/password-prompt.8mandos plugins.d/usplash.8mandos \ + plugins.d/splashy.8mandos plugins.d/askpass-fifo.8mandos \ + plugins.d/plymouth.8mandos intro.8mandos + +htmldocs:=$(addsuffix .xhtml,$(DOCS)) + +objects:=$(addsuffix .o,$(CPROGS)) + +all: $(PROGS) mandos.lsm + +doc: $(DOCS) + +html: $(htmldocs) + +%.5: %.xml common.ent legalnotice.xml + $(DOCBOOKTOMAN) +%.5.xhtml: %.xml common.ent legalnotice.xml + $(DOCBOOKTOHTML) + +%.8: %.xml common.ent legalnotice.xml + $(DOCBOOKTOMAN) +%.8.xhtml: %.xml common.ent legalnotice.xml + $(DOCBOOKTOHTML) + +%.8mandos: %.xml common.ent legalnotice.xml + $(DOCBOOKTOMAN) +%.8mandos.xhtml: %.xml common.ent legalnotice.xml + $(DOCBOOKTOHTML) + +intro.8mandos: intro.xml common.ent legalnotice.xml + $(DOCBOOKTOMAN) +intro.8mandos.xhtml: intro.xml common.ent legalnotice.xml + $(DOCBOOKTOHTML) + +mandos.8: mandos.xml common.ent mandos-options.xml overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos.8.xhtml: mandos.xml common.ent mandos-options.xml \ + overview.xml legalnotice.xml + $(DOCBOOKTOHTML) + +mandos-keygen.8: mandos-keygen.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos-keygen.8.xhtml: mandos-keygen.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + +mandos-monitor.8: mandos-monitor.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos-monitor.8.xhtml: mandos-monitor.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + +mandos-ctl.8: mandos-ctl.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos-ctl.8.xhtml: mandos-ctl.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + +mandos.conf.5: mandos.conf.xml common.ent mandos-options.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos.conf.5.xhtml: mandos.conf.xml common.ent mandos-options.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + +plugin-runner.8mandos: plugin-runner.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +plugin-runner.8mandos.xhtml: plugin-runner.xml common.ent \ + overview.xml legalnotice.xml + $(DOCBOOKTOHTML) + +dracut-module/password-agent.8mandos: \ + dracut-module/password-agent.xml common.ent \ + overview.xml legalnotice.xml + $(DOCBOOKTOMAN) +dracut-module/password-agent.8mandos.xhtml: \ + dracut-module/password-agent.xml common.ent \ + overview.xml legalnotice.xml + $(DOCBOOKTOHTML) + +plugins.d/mandos-client.8mandos: plugins.d/mandos-client.xml \ + common.ent \ + mandos-options.xml \ + overview.xml legalnotice.xml + $(DOCBOOKTOMAN) +plugins.d/mandos-client.8mandos.xhtml: plugins.d/mandos-client.xml \ + common.ent \ + mandos-options.xml \ + overview.xml legalnotice.xml + $(DOCBOOKTOHTML) + +# Update all these files with version number $(version) +common.ent: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\($$/\1$(version)">/' \ + $@) + +mandos: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@) + +mandos-keygen: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(VERSION="\)[^"]*"$$/\1$(version)"/' \ + $@) + +mandos-ctl: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@) + +mandos-monitor: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@) + +mandos.lsm: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(Version:\).*/\1\t$(version)/' \ + $@) + $(strip $(SED) --in-place \ + --expression='s/^\(Entered-date:\).*/\1\t$(shell date --rfc-3339=date --reference=Makefile)/' \ + $@) + $(strip $(SED) --in-place \ + --expression='s/\(mandos_\)[0-9.]\+\(\.orig\.tar\.gz\)/\1$(version)\2/' \ + $@) + +# Need to add the GnuTLS, Avahi and GPGME libraries +plugins.d/mandos-client: plugins.d/mandos-client.c + $(LINK.c) $^ $(GNUTLS_CFLAGS) $(AVAHI_CFLAGS) $(strip\ + ) $(GPGME_CFLAGS) $(GNUTLS_LIBS) $(strip\ + ) $(AVAHI_LIBS) $(GPGME_LIBS) $(LOADLIBES) $(strip\ + ) $(LDLIBS) -o $@ + +# Need to add the libnl-route library +plugin-helpers/mandos-client-iprouteadddel: plugin-helpers/mandos-client-iprouteadddel.c + $(LINK.c) $(LIBNL3_CFLAGS) $^ $(LIBNL3_LIBS) $(strip\ + ) $(LOADLIBES) $(LDLIBS) -o $@ + +# Need to add the GLib and pthread libraries +dracut-module/password-agent: dracut-module/password-agent.c + $(LINK.c) $(GLIB_CFLAGS) $^ $(GLIB_LIBS) -lpthread $(strip\ + ) $(LOADLIBES) $(LDLIBS) -o $@ + +.PHONY : all doc html clean distclean mostlyclean maintainer-clean \ + check run-client run-server install install-html \ + install-server install-client-nokey install-client uninstall \ + uninstall-server uninstall-client purge purge-server \ + purge-client + clean: - -rm -f $(PROGS) $(objects) core + -rm --force $(CPROGS) $(objects) $(htmldocs) $(DOCS) core + +distclean: clean +mostlyclean: clean +maintainer-clean: clean + -rm --force --recursive keydir confdir statedir + +check: all + ./mandos --check + ./mandos-ctl --check + ./mandos-keygen --version + ./plugin-runner --version + ./plugin-helpers/mandos-client-iprouteadddel --version + ./dracut-module/password-agent --test + +# Run the client with a local config and key +run-client: all keydir/seckey.txt keydir/pubkey.txt \ + keydir/tls-privkey.pem keydir/tls-pubkey.pem + @echo '######################################################' + @echo '# The following error messages are harmless and can #' + @echo '# be safely ignored: #' + @echo '## From plugin-runner: #' + @echo '# setgid: Operation not permitted #' + @echo '# setuid: Operation not permitted #' + @echo '## From askpass-fifo: #' + @echo '# mkfifo: Permission denied #' + @echo '## From mandos-client: #' + @echo '# Failed to raise privileges: Operation not permi... #' + @echo '# Warning: network hook "*" exited with status * #' + @echo '# ioctl SIOCSIFFLAGS +IFF_UP: Operation not permi... #' + @echo '# Failed to bring up interface "*": Operation not... #' + @echo '# #' + @echo '# (The messages are caused by not running as root, #' + @echo '# but you should NOT run "make run-client" as root #' + @echo '# unless you also unpacked and compiled Mandos as #' + @echo '# root, which is also NOT recommended.) #' + @echo '######################################################' +# We set GNOME_KEYRING_CONTROL to block pam_gnome_keyring + ./plugin-runner --plugin-dir=plugins.d \ + --plugin-helper-dir=plugin-helpers \ + --config-file=plugin-runner.conf \ + --options-for=mandos-client:--seckey=keydir/seckey.txt,--pubkey=keydir/pubkey.txt,--tls-privkey=keydir/tls-privkey.pem,--tls-pubkey=keydir/tls-pubkey.pem,--network-hook-dir=network-hooks.d \ + --env-for=mandos-client:GNOME_KEYRING_CONTROL= \ + $(CLIENTARGS) + +# Used by run-client +keydir/seckey.txt keydir/pubkey.txt keydir/tls-privkey.pem keydir/tls-pubkey.pem: mandos-keygen + install --directory keydir + ./mandos-keygen --dir keydir --force + +# Run the server with a local config +run-server: confdir/mandos.conf confdir/clients.conf statedir + ./mandos --debug --no-dbus --configdir=confdir \ + --statedir=statedir $(SERVERARGS) + +# Used by run-server +confdir/mandos.conf: mandos.conf + install --directory confdir + install --mode=u=rw,go=r $^ $@ +confdir/clients.conf: clients.conf keydir/seckey.txt keydir/tls-pubkey.pem + install --directory confdir + install --mode=u=rw $< $@ +# Add a client password + ./mandos-keygen --dir keydir --password --no-ssh >> $@ +statedir: + install --directory statedir + +install: install-server install-client-nokey + +install-html: html + install --directory $(htmldir) + install --mode=u=rw,go=r --target-directory=$(htmldir) \ + $(htmldocs) + +install-server: doc + install --directory $(CONFDIR) + if install --directory --mode=u=rwx --owner=$(USER) \ + --group=$(GROUP) $(STATEDIR); then \ + :; \ + elif install --directory --mode=u=rwx $(STATEDIR); then \ + chown -- $(USER):$(GROUP) $(STATEDIR) || :; \ + fi + if [ "$(TMPFILES)" != "$(DESTDIR)" \ + -a -d "$(TMPFILES)" ]; then \ + install --mode=u=rw,go=r tmpfiles.d-mandos.conf \ + $(TMPFILES)/mandos.conf; \ + fi + if [ "$(SYSUSERS)" != "$(DESTDIR)" \ + -a -d "$(SYSUSERS)" ]; then \ + install --mode=u=rw,go=r sysusers.d-mandos.conf \ + $(SYSUSERS)/mandos.conf; \ + fi + install --mode=u=rwx,go=rx mandos $(PREFIX)/sbin/mandos + install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ + mandos-ctl + install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ + mandos-monitor + install --mode=u=rw,go=r --target-directory=$(CONFDIR) \ + mandos.conf + install --mode=u=rw --target-directory=$(CONFDIR) \ + clients.conf + install --mode=u=rw,go=r dbus-mandos.conf \ + $(DESTDIR)/etc/dbus-1/system.d/mandos.conf + install --mode=u=rwx,go=rx init.d-mandos \ + $(DESTDIR)/etc/init.d/mandos + if [ "$(SYSTEMD)" != "$(DESTDIR)" -a -d "$(SYSTEMD)" ]; then \ + install --mode=u=rw,go=r mandos.service $(SYSTEMD); \ + fi + install --mode=u=rw,go=r default-mandos \ + $(DESTDIR)/etc/default/mandos + if [ -z $(DESTDIR) ]; then \ + update-rc.d mandos defaults 25 15;\ + fi + gzip --best --to-stdout mandos.8 \ + > $(MANDIR)/man8/mandos.8.gz + gzip --best --to-stdout mandos-monitor.8 \ + > $(MANDIR)/man8/mandos-monitor.8.gz + gzip --best --to-stdout mandos-ctl.8 \ + > $(MANDIR)/man8/mandos-ctl.8.gz + gzip --best --to-stdout mandos.conf.5 \ + > $(MANDIR)/man5/mandos.conf.5.gz + gzip --best --to-stdout mandos-clients.conf.5 \ + > $(MANDIR)/man5/mandos-clients.conf.5.gz + gzip --best --to-stdout intro.8mandos \ + > $(MANDIR)/man8/intro.8mandos.gz + +install-client-nokey: all doc + install --directory $(LIBDIR)/mandos $(CONFDIR) + install --directory --mode=u=rwx $(KEYDIR) \ + $(LIBDIR)/mandos/plugins.d \ + $(LIBDIR)/mandos/plugin-helpers + if [ "$(SYSUSERS)" != "$(DESTDIR)" \ + -a -d "$(SYSUSERS)" ]; then \ + install --mode=u=rw,go=r sysusers.d-mandos.conf \ + $(SYSUSERS)/mandos-client.conf; \ + fi + if [ "$(CONFDIR)" != "$(LIBDIR)/mandos" ]; then \ + install --mode=u=rwx \ + --directory "$(CONFDIR)/plugins.d" \ + "$(CONFDIR)/plugin-helpers"; \ + fi + install --mode=u=rwx,go=rx --directory \ + "$(CONFDIR)/network-hooks.d" + install --mode=u=rwx,go=rx \ + --target-directory=$(LIBDIR)/mandos plugin-runner + install --mode=u=rwx,go=rx \ + --target-directory=$(LIBDIR)/mandos \ + mandos-to-cryptroot-unlock + install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ + mandos-keygen + install --mode=u=rwx,go=rx \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ + plugins.d/password-prompt + install --mode=u=rwxs,go=rx \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ + plugins.d/mandos-client + install --mode=u=rwxs,go=rx \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ + plugins.d/usplash + install --mode=u=rwxs,go=rx \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ + plugins.d/splashy + install --mode=u=rwxs,go=rx \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ + plugins.d/askpass-fifo + install --mode=u=rwxs,go=rx \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ + plugins.d/plymouth + install --mode=u=rwx,go=rx \ + --target-directory=$(LIBDIR)/mandos/plugin-helpers \ + plugin-helpers/mandos-client-iprouteadddel + install initramfs-tools-hook \ + $(INITRAMFSTOOLS)/hooks/mandos + install --mode=u=rw,go=r initramfs-tools-conf \ + $(INITRAMFSTOOLS)/conf.d/mandos-conf + install --mode=u=rw,go=r initramfs-tools-conf-hook \ + $(INITRAMFSTOOLS)/conf-hooks.d/zz-mandos + install initramfs-tools-script \ + $(INITRAMFSTOOLS)/scripts/init-premount/mandos + install initramfs-tools-script-stop \ + $(INITRAMFSTOOLS)/scripts/local-premount/mandos + install --directory $(DRACUTMODULE) + install --mode=u=rw,go=r --target-directory=$(DRACUTMODULE) \ + dracut-module/ask-password-mandos.path \ + dracut-module/ask-password-mandos.service + install --mode=u=rwxs,go=rx \ + --target-directory=$(DRACUTMODULE) \ + dracut-module/module-setup.sh \ + dracut-module/cmdline-mandos.sh \ + dracut-module/password-agent + install --mode=u=rw,go=r plugin-runner.conf $(CONFDIR) + gzip --best --to-stdout mandos-keygen.8 \ + > $(MANDIR)/man8/mandos-keygen.8.gz + gzip --best --to-stdout plugin-runner.8mandos \ + > $(MANDIR)/man8/plugin-runner.8mandos.gz + gzip --best --to-stdout plugins.d/mandos-client.8mandos \ + > $(MANDIR)/man8/mandos-client.8mandos.gz + gzip --best --to-stdout plugins.d/password-prompt.8mandos \ + > $(MANDIR)/man8/password-prompt.8mandos.gz + gzip --best --to-stdout plugins.d/usplash.8mandos \ + > $(MANDIR)/man8/usplash.8mandos.gz + gzip --best --to-stdout plugins.d/splashy.8mandos \ + > $(MANDIR)/man8/splashy.8mandos.gz + gzip --best --to-stdout plugins.d/askpass-fifo.8mandos \ + > $(MANDIR)/man8/askpass-fifo.8mandos.gz + gzip --best --to-stdout plugins.d/plymouth.8mandos \ + > $(MANDIR)/man8/plymouth.8mandos.gz + gzip --best --to-stdout dracut-module/password-agent.8mandos \ + > $(MANDIR)/man8/password-agent.8mandos.gz + +install-client: install-client-nokey +# Post-installation stuff + -$(PREFIX)/sbin/mandos-keygen --dir "$(KEYDIR)" + if command -v update-initramfs >/dev/null; then \ + update-initramfs -k all -u; \ + elif command -v dracut >/dev/null; then \ + for initrd in $(DESTDIR)/boot/initr*-$(LINUXVERSION); do \ + if [ -w "$$initrd" ]; then \ + chmod go-r "$$initrd"; \ + dracut --force "$$initrd"; \ + fi; \ + done; \ + fi + echo "Now run mandos-keygen --password --dir $(KEYDIR)" + +uninstall: uninstall-server uninstall-client + +uninstall-server: + -rm --force $(PREFIX)/sbin/mandos \ + $(PREFIX)/sbin/mandos-ctl \ + $(PREFIX)/sbin/mandos-monitor \ + $(MANDIR)/man8/mandos.8.gz \ + $(MANDIR)/man8/mandos-monitor.8.gz \ + $(MANDIR)/man8/mandos-ctl.8.gz \ + $(MANDIR)/man5/mandos.conf.5.gz \ + $(MANDIR)/man5/mandos-clients.conf.5.gz + update-rc.d -f mandos remove + -rmdir $(CONFDIR) + +uninstall-client: +# Refuse to uninstall client if /etc/crypttab is explicitly configured +# to use it. + ! grep --regexp='^ *[^ #].*keyscript=[^,=]*/mandos/' \ + $(DESTDIR)/etc/crypttab + -rm --force $(PREFIX)/sbin/mandos-keygen \ + $(LIBDIR)/mandos/plugin-runner \ + $(LIBDIR)/mandos/plugins.d/password-prompt \ + $(LIBDIR)/mandos/plugins.d/mandos-client \ + $(LIBDIR)/mandos/plugins.d/usplash \ + $(LIBDIR)/mandos/plugins.d/splashy \ + $(LIBDIR)/mandos/plugins.d/askpass-fifo \ + $(LIBDIR)/mandos/plugins.d/plymouth \ + $(INITRAMFSTOOLS)/hooks/mandos \ + $(INITRAMFSTOOLS)/conf-hooks.d/mandos \ + $(INITRAMFSTOOLS)/scripts/init-premount/mandos \ + $(INITRAMFSTOOLS)/scripts/local-premount/mandos \ + $(DRACUTMODULE)/ask-password-mandos.path \ + $(DRACUTMODULE)/ask-password-mandos.service \ + $(DRACUTMODULE)/module-setup.sh \ + $(DRACUTMODULE)/cmdline-mandos.sh \ + $(DRACUTMODULE)/password-agent \ + $(MANDIR)/man8/mandos-keygen.8.gz \ + $(MANDIR)/man8/plugin-runner.8mandos.gz \ + $(MANDIR)/man8/mandos-client.8mandos.gz + $(MANDIR)/man8/password-prompt.8mandos.gz \ + $(MANDIR)/man8/usplash.8mandos.gz \ + $(MANDIR)/man8/splashy.8mandos.gz \ + $(MANDIR)/man8/askpass-fifo.8mandos.gz \ + $(MANDIR)/man8/plymouth.8mandos.gz \ + $(MANDIR)/man8/password-agent.8mandos.gz \ + -rmdir $(LIBDIR)/mandos/plugins.d $(CONFDIR)/plugins.d \ + $(LIBDIR)/mandos $(CONFDIR) $(KEYDIR) $(DRACUTMODULE) + if command -v update-initramfs >/dev/null; then \ + update-initramfs -k all -u; \ + elif command -v dracut >/dev/null; then \ + for initrd in $(DESTDIR)/boot/initr*-$(LINUXVERSION); do \ + test -w "$$initrd" && dracut --force "$$initrd"; \ + done; \ + fi + +purge: purge-server purge-client + +purge-server: uninstall-server + -rm --force $(CONFDIR)/mandos.conf $(CONFDIR)/clients.conf \ + $(DESTDIR)/etc/dbus-1/system.d/mandos.conf + $(DESTDIR)/etc/default/mandos \ + $(DESTDIR)/etc/init.d/mandos \ + $(SYSTEMD)/mandos.service \ + $(DESTDIR)/run/mandos.pid \ + $(DESTDIR)/var/run/mandos.pid + -rmdir $(CONFDIR) + +purge-client: uninstall-client + -shred --remove $(KEYDIR)/seckey.txt $(KEYDIR)/tls-privkey.pem + -rm --force $(CONFDIR)/plugin-runner.conf \ + $(KEYDIR)/pubkey.txt $(KEYDIR)/seckey.txt \ + $(KEYDIR)/tls-pubkey.txt $(KEYDIR)/tls-privkey.txt + -rmdir $(KEYDIR) $(CONFDIR)/plugins.d $(CONFDIR) === added file 'NEWS' --- NEWS 1970-01-01 00:00:00 +0000 +++ NEWS 2019-09-03 19:06:41 +0000 @@ -0,0 +1,570 @@ +This NEWS file records noteworthy changes, very tersely. +See the manual for detailed information. + +Version 1.8.9 (2019-09-03) +* No user-visible changes + +Version 1.8.8 (2019-08-18) +* No user-visible changes + +Version 1.8.7 (2019-08-05) +* Client: +** Always compile with LFS (Large File Support) enabled. +* Server +** Improve intro(8mandos) manual page to cover dracut(8) support. + +Version 1.8.6 (2019-08-03) +* Client: +** dracut support: In password-agent, properly ignore deleted and + renamed question files, and also fix memory alignment issue. + +Version 1.8.5 (2019-07-30) +* Client +** Support dracut(8) as well as initramfs-tools(7). +** Minor bug fix: Allow the mandos-keygen --passfile option to use + passfiles with names starting with "-". +** Document known limitation of mandos-keygen --password; it strips + white space from start and end of the password. +* Server +** Bug fix: The server used to fail to restart if the "port" setting + was used. This has been fixed. +** Minor bug fix: Reap zombies left over from checker runs. (Debian + bug #933387) + +Version 1.8.4 (2019-04-09) +* Client +** Fix minor memory leak in plugin-runner. +* Server +** mandos-ctl now has a --debug option to show D-Bus calls. + +Version 1.8.3 (2019-02-11) +* No user-visible changes. + +Version 1.8.2 (2019-02-10) +* Client +** In mandos-keygen, ignore failures to remove files in some cases. + +Version 1.8.1 (2019-02-10) +* Client +** Only generate TLS keys using GnuTLS' certtool, of sufficient + version. Key generation of TLS keys will not happen until a + version of GnuTLS is installed with support for raw public keys. +** Remove any bad keys created by 1.8.0 and openssl. +* Server +** On installation, edit clients.conf and remove the same bad key ID + which was erroneously reported by all 1.8.0 clients. Also do not + trust this key ID in the server. + +Version 1.8.0 (2019-02-10) +* Client +** Use new TLS keys for server communication and identification. + With GnuTLS 3.6 or later, OpenPGP keys are no longer supported. + The client can now use the new "raw public keys" (RFC 7250) API + instead, using GnuTLS 3.6.6. Please note: This *requires* new key + IDs to be added to server's client.conf file. +** New --tls-privkey and --tls-pubkey options to load TLS key files. + If GnuTLS is too old, these options do nothing. +* Server +** Supports either old or new GnuTLS. + The server now supports using GnuTLS 3.6.6 and clients connecting + with "raw public keys" as identification. The server will read + both fingerprints and key IDs from clients.conf file, and will use + either one or the other, depending on what is supported by GnuTLS + on the system. Please note: both are *not* supported at once; if + one type is supported by GnuTLS, all values of the other type from + clients.conf are ignored. + +Version 1.7.20 (2018-08-19) +* Client +** Fix: Adapt to the Debian cryptsetup package 2.0.3 or later. + Important: in that version or later, the plugins "askpass-fifo", + "password-prompt", and "plymouth" will no longer be run, since they + would conflict with what cryptsetup is doing. Other plugins, such + as mandos-client and any user-supplied plugins, will still run. +** Better error message if failing to decrypt secret data +** Check for (and report) any key import failure from GPGME +** Better error message if self-signature verification fails +** Set system clock if not set; required by GnuPG for key import +** When debugging plugin-runner, it will now show starting plugins + +Version 1.7.19 (2018-02-22) +* Client +** Do not print "unlink(...): No such file or directory". +** Bug fixes: Fix file descriptor leaks. +** Bug fix: Don't use leak sanitizer with leaking libraries. + +Version 1.7.18 (2018-02-12) +* Client +** Bug fix: Revert faulty fix for a nonexistent bug in the + plugin-runner + +Version 1.7.17 (2018-02-10) +* Client +** Bug fix: Fix a memory leak in the plugin-runner +** Bug fix: Fix memory leaks in the plymouth plugin + +Version 1.7.16 (2017-08-20) +* Client +** Bug fix: ignore "resumedev" entries in initramfs' cryptroot file +** Bug fix in plymouth plugin: fix memory leak, avoid warning output + +Version 1.7.15 (2017-02-23) +* Server +** Bug fix: Respect the mandos.conf "zeroconf" and "restore" options +* Client +** Bug fix in mandos-keygen: Handle backslashes in passphrases + +Version 1.7.14 (2017-01-25) +* Server +** Use "Requisite" instead of "RequisiteOverridable" in systemd + service file. + +Version 1.7.13 (2016-10-08) +* Client +** Minor bug fix: Don't ask for passphrase or fail when generating + keys using GnuPG 2.1 in a chrooted environment. + +Version 1.7.12 (2016-10-05) +* Client +** Bug fix: Don't crash after exit() when using DH parameters file + +Version 1.7.11 (2016-10-01) +* Client +** Security fix: Don't compile with AddressSanitizer +* Server +** Bug fix: Find GnuTLS library when gnutls28-dev is not installed +** Bug fix: Include "Expires" and "Last Checker Status" in mandos-ctl + verbose output +** New option for mandos-ctl: --dump-json + +Version 1.7.10 (2016-06-23) +* Client +** Security fix: restrict permissions of /etc/mandos/plugin-helpers +* Server +** Bug fix: Make the --interface flag work with Python 2.7 when "cc" + is not installed + +Version 1.7.9 (2016-06-22) +* Client +** Do not include intro(8mandos) man page + +Version 1.7.8 (2016-06-21) +* Client +** Include intro(8mandos) man page +** mandos-keygen: Use ECDSA SSH keys by default +** Bug fix: Work with GnuPG 2 when booting (Debian bug #819982) + by copying /usr/bin/gpg-agent into initramfs +* Server +** Bug fix: Work with GnuPG 2 (don't use --no-use-agent option) +** Bug fix: Make the --interface option work when using Python 2.7 + by trying harder to find SO_BINDTODEVICE + +Version 1.7.7 (2016-03-19) +* Client +** Fix bug in Plymouth client, broken since 1.7.2 + +Version 1.7.6 (2016-03-13) +* Server +** Fix bug where stopping server would time out +** Make server runnable with Python 3 + +Version 1.7.5 (2016-03-08) +* Server +** Fix security restrictions in systemd service file. +** Work around bug where stopping server would time out + +Version 1.7.4 (2016-03-05) +* Client +** Bug fix: Tolerate errors from configure_networking (Debian Bug + #816513) +** Compilation: Only use sanitizing options which work with the + compiler used when building. This should fix compilation with GCC + 4.9 on mips, mipsel, and s390x. +* Server +** Add extra security restrictions in systemd service file. + +Version 1.7.3 (2016-02-29) +* Client +** Bug fix: Remove new type of keyring directory user by GnuPG 2.1. +** Bug fix: Remove "nonnull" attribute from a function argument, which + would otherwise generate a spurious runtime warning. + +Version 1.7.2 (2016-02-28) +* Server +** Stop using python-gnutls library; it was not updated to GnuTLS 3.3. +** Bug fix: Only send D-Bus signal ClientRemoved if using D-Bus. +** Use GnuPG 2 if available. +* Client +** Compile with various sanitizing flags. + +Version 1.7.1 (2015-10-24) +* Client +** Bug fix: Can now really find Mandos server even if the server has + an IPv6 address on a network other than the one which the Mandos + server is on. + +Version 1.7.0 (2015-08-10) +* Server +** Bug fix: Handle local Zeroconf service name collisions better. +** Bug fix: Finally fix "ERROR: Child process vanished" bug. +** Bug fix: Fix systemd service file to start server correctly. +** Bug fix: Be compatible with old 2048-bit DSA keys. +** The D-Bus API now provides the standard D-Bus ObjectManager + interface, and deprecates older functionality. See the DBUS-API + file for the currently recommended API. Note: the original API + still works, but is deprecated. +* Client +** Can now find Mandos server even if the server has an IPv6 address + on a network without IPv6 Router Advertisment (like if the Mandos + client itself is the router, or there is an IPv6 router advertising + a network other than the one which the Mandos server is on.) +** Use a better value than 1024 for the default number of DH bits. + This better value is either provided by a DH parameters file (see + below) or an appropriate number of DH bits is determined based on + the PGP key. +** Bug fix: mandos-keygen now generates correct output for the + "Checker" variable even if the SSH server on the Mandos client has + multiple SSH key types. +** Can now use pre-generated Diffie-Hellman parameters from a file. + +Version 1.6.9 (2014-10-05) +* Server +** Changed to emit standard D-Bus signal when D-Bus properties change. + (The old signal is still emitted too, but marked as deprecated.) + +Version 1.6.8 (2014-08-06) +* Client +** Bug fix: mandos-keygen now generates working SSH checker commands. +* Server +** Bug fix: "mandos-monitor" now really redraws screen on Ctrl-L. +** Now requires Python 2.7. + +Version 1.6.7 (2014-07-17) +* Client +** Bug fix: Now compatible with GPGME 1.5.0. +** Bug fix: Fixed minor memory leaks. +* Server +** "mandos-monitor" now has verbose logging, toggleable with "v". + +Version 1.6.6 (2014-07-13) +* Client +** If client host has an SSH server, "mandos-keygen --password" now + outputs "checker" option which uses "ssh-keyscan"; this is more + secure than the default "fping" checker. +** Bug fix: allow "." in network hook names, to match documentation. +** Better error messages. +* Server +** New --no-zeroconf option. +** Bug fix: Fix --servicename option, broken since 1.6.4. +** Bug fix: Fix --socket option work for --socket=0. + +Version 1.6.5 (2014-05-11) +* Client +** Work around bug in GnuPG +** Give better error messages when run without sufficient privileges +** Only warn if workaround for Debian bug #633582 was necessary and + failed, not if it failed and was unnecessary. + +Version 1.6.4 (2014-02-16) +* Server +** Very minor fix to self-test code. + +Version 1.6.3 (2014-01-21) +* Server +** Add systemd support. +** For PID file, fall back to /var/run if /run does not exist. +* Client +** Moved files from /usr/lib/mandos to whatever the architecture + specifies, like /usr/lib/x86_64-linux-gnu/mandos or + /usr/lib64/mandos. + +Version 1.6.2 (2013-10-24) +* Server +** PID file moved from /var/run to /run. +** Bug fix: Handle long secrets when saving client state. +** Bug fix: Use more magic in the GnuTLS priority string to handle + both old DSA/ELG 2048-bit keys and new RSA/RSA 4096-bit keys. +* Client +** mandos-keygen: Bug fix: now generate RSA keys which GnuTLS can use. + Bug fix: Output passphrase prompts even when + redirecting standard output. + +Version 1.6.1 (2013-10-13) +* Server +** All client options for time intervals now also take an RFC 3339 + duration. The same for all options to mandos-ctl. +** Bug fix: Handle fast checkers (like ":") correctly. +** Bug fix: Don't print output from checkers when running in + foreground. +** Bug fix: Do not fail when client is removed from clients.conf but + saved settings remain. +** Bug fix: mandos-monitor now displays standout (reverse video) again + using new version of Urwid. +** Bug fix: Make boolean options work from the config file again. +** Bug fix: Make --no-ipv6 work again. +** New default priority string to be slightly more compatible with + older versions of GnuTLS. +* Client +** Bug fix: Fix bashism in mandos-keygen. +** Default key and subkey types are now RSA and RSA, respectively. + Also, new default key size is 4096 bits. + +Version 1.6.0 (2012-06-18) +* Server +** Takes new --foreground option +** Init script supports new "status" action. +* Client +** Now uses all interfaces by default; the --interface option can + still be used to restrict it, and the argument to --interface (as + well as the $DEVICE environment variable for the network hooks) is + now a comma-separated list of interfaces to use. + +Version 1.5.5 (2012-06-01) +* Server +** Server takes new --socket option + +Version 1.5.4 (2012-05-20) +* Server +** Bug fix: Regression fix: Make non-zero approval timeout values work. +** Bug fix: Regression fix: Allow changing the Timeout D-Bus property. +** Fall back to not bind to an interface if an invalid interface name + is given. +** Removed support for undocumented feature of using plain "%%s" in + "checker" client option. +** Old D-Bus interface are now marked as deprecated. +** mandos-monitor: Bug fix: show approval timers correctly. +** mandos-ctl: Show "Extended Timeout" correctly, not as milliseconds. + +Version 1.5.3 (2012-01-15) +* Server +** Add D-Bus property se.recompile.Client.LastCheckerStatus and use it + in mandos-monitor. +* Client +** Fix bugs in the example "bridge" network hook. + +Version 1.5.2 (2012-01-08) +* Server +** Removed D-Bus signal se.recompile.Mandos.NewRequest() added in + 1.5.0. It was buggy and was of questionable utility. + +Version 1.5.1 (2012-01-01) +* Server +** Include intro(8mandos) manual page, missing since migration from + README file in version 1.4.0. + +Version 1.5.0 (2012-01-01) +* Client +** Network hooks. The Mandos client can now run custom scripts to take + up a network interface before the client is run. Three example + scripts are provided: "wireless", "openvpn", and "bridge". + To facilitate this, the client now prefers network interfaces which + are up (if any) over all other interfaces. +* Server +** Persistent state. Client state is now saved between server + restarts. +** clients.conf file can now contain "enabled" setting for clients. +** Bug fix: Fix rare crash bug. +** Bug fix: Send corrent D-Bus type in PropertyChanged for + "ApprovalDelay", "ApprovalDuration", "Timeout", and + "ExtendedTimeout". +** mandos-ctl: Bare numbers as arguments are taken to be milliseconds. +** Bug fix: mandos-ctl --secret option now works. +** New D-Bus signal: se.recompile.Mandos.NewRequest(s). + +Version 1.4.1 (2011-10-15) +* Server +** Make D-Bus properties settable again, and handle checkers + for disabled clients correctly. +* Miscellaneous fixes to "pedantic" Lintian warnings + +Version 1.4.0 (2011-10-09) +* README file migrated to manual page intro(8mandos). +* Client: +** Fixed warning about "rmdir: Directory not empty". +* Server: +** Default values changed: timeout 5 minutes, interval 2 minutes. +** Clients gets an expiration extension when receiving a password, + controlled by new "extended_timeout" setting. +** New domain name: "fukt.bsnet.se" changes to "recompile.se". This + also affects the D-Bus bus and interface names (old names still + work). Users should start using the new names immediately. +** New D-Bus Client object properties "Expires" and "ExtendedTimeout"; + see DBUS-API for details. + +Version 1.3.1 (2011-07-27) +* Client: +** Client now retries all Mandos servers periodically. +** Work around Debian bug #633582 - fixes "Permission denied" problem. + +Version 1.3.0 (2011-03-08) +* Server: +** Updated for Python 2.6. +* Client: +** Bug fix: Make the password-prompt plugin not conflict with + Plymouth. +** Bug fix: Bug fix: update initramfs also when purging package. + +Version 1.2.3 (2010-10-11) +* Server: +** Bug fix: Expose D-Bus API also in non-debug mode. + +Version 1.2.2 (2010-10-07) +* Client: +** splashy: Minor fix to compile with non-Linux kernels. + +Version 1.2.1 (2010-10-02) +* Server: +** mandos-monitor(8): Documentation bug fix: Key for removing client + is "R", not "r". + +Version 1.2 (2010-09-28) +* Client: +** New "plymouth" plugin to ask for a password using the Plymouth + graphical boot system. +** The Mandos client now automatically chooses a network interface if + the DEVICE setting in /etc/initramfs-tools/initramfs.conf is set to + the empty string. This is also the new default instead of "eth0". +** The Mandos client --connect option now loops indefinitely until a + password is received from the specified server. +** Bug fix: Quote directory correctly in mandos-keygen with --password +** Bug fix: don't use "echo -e" in mandos-keygen; unsupported by dash. +* Server: +** Terminology change: clients are now "ENABLED" or "DISABLED", not + "valid" or "invalid". +** New D-Bus API; see the file "DBUS-API". +** New control utilities using the new D-Bus API: + + mandos-ctl A command-line based utility + + mandos-monitor A text-based GUI interface +** New feature: manual interactive approval or denying of clients on a + case-by-case basis. +** New --debuglevel option to control logging +** Will not write PID file if --debug is passed +** Bug fix: Avoid race conditions with short "interval" values or + fast checkers. +** Bug fix: Don't try to bind to a network interface when none is + specified + +Version 1.0.14 (2009-10-25) +Enable building without -pie and -fPIE if BROKEN_PIE is set. + +Version 1.0.13 (2009-10-22) +* Client +** Security bug fix: If Mandos server is also installed, do not copy + its config files (with encrypted passwords) into the initrd.img-* + files. + +Version 1.0.12 (2009-09-17) +* Client +** Bug fix: Allow network interface renaming by "udev" by taking down + the network interface after using it. +** Bug fix: User-supplied plugins are now installed correctly. +** Bug fix: If usplash was used but the password was instead provided + by the Mandos server, the usplash daemon used to ignore the first + command passed to it. This has been fixed. +** Bug fix: Make the "--userid" and "--groupid" options in + "plugin-runner.conf" work. +* Server +** Bug fix: Fix the LSB header in the init.d script to make dependency + based booting work. +** A client receiving its password now also counts as if a checker was + run successfully (i.e. the timeout timer is reset). + +Version 1.0.11 (2009-05-23) +* Client +** Bug fix: Use "pkg-config" instead of old "libgnutls-config". + +Version 1.0.10 (2009-05-17) +* Client +** Security bug fix: Fix permissions on initrd.img-*.bak files when + upgrading from older versions. + +Version 1.0.9 (2009-05-17) +* Client +** Security bug fix: Fix permissions on initrd.img file when + installing new linux-image-* packages calling mkinitramfs-kpkg (all + version lower than 2.6.28-1-* does this). + +Version 1.0.8 (2009-02-25) +* Client +** Bug fix: Fix missing quote characters in initramfs-tools-hook. + +Version 1.0.7 (2009-02-24) +* Client +** Bug fix: Do not depend on GNU awk. + +Version 1.0.6 (2009-02-13) +* Server +** Fix bug where server would stop responding, with a zombie checker +** Support for disabling IPv6 (only for advanced users) +** Fix bug which made server not change group ID + +* Client +** Bug fix: Fix permission for /lib64 (on relevant architechtures). +** Add support for IPv4 addresses. +** Add support in mandos-client for not bringing up a network + interface by specifying an empty string to "--interface". +** Make password prompt on boot not be mangled by kernel log messages + about network interface. +** Get network interface from initramfs.conf and/or from kernel + command line. +** If set by "ip=" kernel command line, configure network on boot. +** Support connecting directly using "mandos=connect" kernel command. + line option, provided network is configured using "ip=". +** Fix bug which made plugin-runner and mandos-client not change group + ID. +** Fix bug where the "--options-for" option of plugin-runner would + truncate the value at the first colon character. +** Fix bug where plugin-runner would not go to fallback if all plugins + failed. +** Fix bug where mandos-client would not clean temporary directory on + a signal or on certain file systems. +** Bug fix: remove bashism in /bin/sh script "mandos-keygen". + +Version 1.0.5 (2009-01-17) +* Client +** Fix small memory leak in plugin-runner. + +Version 1.0.4 (2009-01-15) +* Server +** Only find matched user/group pairs when searching for suitable + nonprivileged user/group to switch to. + +* Client +** New kernel parameter "mandos=off" makes client not run at boot. +** Fix linking errors and compilation warnings on AMD64. +** Parse numbers in command line options better. +** The splashy and usplash plugins are more robust while traversing + /proc, and will not abort if a process suddenly disappears. + +Version 1.0.3 (2009-01-06) +* Server +** Now tries to change to user and group "_mandos" before falling back + to trying the old values "mandos", "nobody:nogroup", and "65534". +** Now does not abort on startup even if no clients are defined in + clients.conf. + +* Client +** Plugins named "*.dpkg-bak" are now ignored. +** Hopefully fixed compilation failure on some architectures where the + C compiler does not recognize the "-z" option as a linker option. + +Version 1.0.2 (2008-10-17) +* mandos-keygen now signs the encrypted key blobs. This signature is + not currently verified by mandos-client, but this may change in the + future. + +Version 1.0.1 (2008-10-07) +* Server +** Expand environment variables and ~user in clients.conf's "secfile" + The "secfile" option in /etc/mandos/clients.conf now expands + "~user/foo" and "$ENVVAR" strings. + +* Client (plugin-runner, plugins, etc.) +** Manual pages for the usplash, splashy, and askpass-fifo plugins. + All plugins now have man pages. +** More secure compilation and linking flags. + All programs are now compiled with "-fstack-protector-all -fPIE + -pie", and linked using "-z relro -pie" for additional security. + +* There is now a "NEWS" file (this one), giving a history of + noteworthy changes. === added file 'README' --- README 1970-01-01 00:00:00 +0000 +++ README 2016-03-23 07:11:22 +0000 @@ -0,0 +1,11 @@ +Please see: https://www.recompile.se/mandos/man/intro.8mandos + +This information previously in this file has been moved to the +intro(8mandos) manual page. Go to the above URL, or install the +Mandos server and run this command: + + man 8mandos intro + +In short, this is the Mandos system; it allows computers to have +encrypted root file systems and at the same time be capable of remote +and/or unattended reboots. === modified file 'TODO' --- TODO 2008-07-22 06:23:29 +0000 +++ TODO 2019-04-09 19:41:53 +0000 @@ -1,25 +1,124 @@ -[Mandos client] -configuration for OpenPGP key -header files/symbols tally -check exit codes of all system calls -IPv4 support - -[Pluginbasedclient] -disable certain plugins -header files/symbols tally -check exit codes of all system calls -change uid to nobody:nogroup - other drop privs stuff? - -[Server] -config file for: - TXT record -run-time communication with server - -[Mandos-tools/utilities] - List clients - Enable client - Disable client - -[Installer] -... +-*- org -*- + +* Testing +** python-nemu + +* mandos-applet + +* mandos-client +** TODO A --server option which only adds to the server list. + (Unlike --connect, which implicitly disables zeroconf.) +** TODO [#B] Use capabilities instead of seteuid(). + https://forums.grsecurity.net/viewtopic.php?f=7&t=2522 +** TODO [#B] Use getaddrinfo(hints=AI_NUMERICHOST) instead of inet_pton() +** TODO [#C] Make start_mandos_communication() take "struct server". +** TODO [#C] --interfaces=regex,eth*,noregex (bridge-utils-interfaces(5)) +** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL +** TODO [#B] Use reallocarray() with GNU LibC 2.29 or later. + +* splashy +** TODO [#B] use scandir(3) instead of readdir(3) +** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL + +* usplash (Deprecated) +** TODO [#B] Make it work again +** TODO [#B] use scandir(3) instead of readdir(3) +** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL + +* askpass-fifo +** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL + +* password-prompt +** TODO [#B] lock stdin (with flock()?) +** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL + +* plymouth +** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL +** TODO [#B] Use reallocarray() with GNU LibC 2.29 or later. + +* TODO [#B] passdev + +* plugin-runner +** TODO handle printing for errors for plugins +*** Hook up stderr of plugins, buffer them, and prepend "Mandos Plugin [plugin name]" +** TODO [#C] use same file name rules as run-parts(8) +** kernel command line option for debug info +** TODO [#A] Restart plugins which exit with EX_TEMPFAIL + +* mandos (server) +** TODO [#B] --notify-command + This would allow the mandos.service to use + --notify-command="systemd-notify --pid --ready" +** TODO [#B] python-systemd +*** import systemd.daemon; systemd.daemon.notify() +** TODO [#B] Log level :BUGS: +*** TODO /etc/mandos/clients.d/*.conf + Watch this directory and add/remove/update clients? +** TODO [#C] config for TXT record +** TODO Log level dbus option + SetLogLevel D-Bus call +** TODO [#C] DBusServiceObjectUsingSuper +** TODO [#B] Global enable/disable flag +** TODO [#B] By-client countdown on number of secrets given +** D-Bus Client method NeedsPassword(50) - Timeout, default disapprove + + SetPass(u"gazonk", True) -> Approval, persistent + + Approve(False) -> Close client connection immediately +** TODO [#C] python-parsedatetime +** TODO Separate logging logic to own object +** TODO [#B] Limit approval_delay to max gnutls/tls timeout value +** TODO [#B] break the wait on approval_delay if connection dies +** TODO Generate Client.runtime_expansions from client options + extra +** TODO Allow %%(checker)s as a runtime expansion +** TODO D-Bus AddClient() method on server object +** TODO Use org.freedesktop.DBus.Method.NoReply annotation on async methods. :2: +** TODO Save state periodically to recover better from hard shutdowns +** TODO CheckerCompleted method, deprecate CheckedOK +** TODO Secret Service API? + https://standards.freedesktop.org/secret-service/ +** TODO Remove D-Bus interfaces with old domain name :2: +** TODO Remove old string_to_delta format :2: +** TODO http://0pointer.de/blog/projects/stateless.html +*** File in /usr/lib/sysusers.d to create user+group "_mandos" +** TODO Error handling on error parsing config files +** TODO init.d script error handling +** TODO D-Bus server properties; address, port, interface, etc. :2: +** Python 3 :2: +*** TODO [#C] In Python 3.3, use shlex.quote() instead of re.escape() + +* mandos-ctl +** TODO Remove old string_to_delta format :2: + +* TODO mandos-dispatch + Listens for specified D-Bus signals and spawns shell commands with + arguments. + +* mandos-monitor +** TODO --servicename :BUGS: +** TODO help should be toggleable +** Urwid client data displayer + Better view of client data in the listing +*** Properties popup +** Print a nice "We are sorry" message, save stack trace to log. + +* mandos-keygen +** TODO "--secfile" option + Using the "secfile" option instead of "secret" +** TODO [#B] "--test" option + For testing decryption before rebooting. + +* Package +** /usr/share/initramfs-tools/hooks/mandos +*** TODO [#C] use same file name rules as run-parts(8) +*** TODO [#C] Do not install in initrd.img if configured not to. + Use "/etc/initramfs-tools/hooksconf.d/mandos"? +** TODO [#C] $(pkg-config --variable=completionsdir bash-completion) + From XML sources directly? + +* Side Stuff +** TODO Locate which package moves the other bin/sh when busybox is deactivated +** TODO contact owner of package, and ask them to have that shell static in position regardless of busybox + +* [[http://www.undeadly.org/cgi?action=article&sid=20110530221728][OpenBSD]] + + +#+STARTUP: showall === added file 'bugs.xml' --- bugs.xml 1970-01-01 00:00:00 +0000 +++ bugs.xml 2016-03-05 21:42:56 +0000 @@ -0,0 +1,11 @@ + + + + Please report bugs to the Mandos development mailing list: + mandos-dev@recompile.se (subscription required). + Note that this list is public. The developers can be reached + privately at mandos@recompile.se (OpenPGP key + fingerprint 153A 37F1 0BBA 0435 987F 2C4A 7223 2973 CA34 + C2C4 for encrypted mail). + === removed file 'ca.pem' --- ca.pem 2007-10-20 21:38:25 +0000 +++ ca.pem 1970-01-01 00:00:00 +0000 @@ -1,40 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIHEzCCBPugAwIBAgIJAMCSQPxm3Tm7MA0GCSqGSIb3DQEBBQUAMIGMMQswCQYD -VQQGEwJTRTELMAkGA1UECBMCQkwxEDAOBgNVBAcTB1Jvbm5lYnkxITAfBgNVBAoT -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAxMNQnJheGVuIHVuaXRl -ZDEjMCEGCSqGSIb3DQEJARYUYmVsb3JuQGZ1a3QuYnNuZXQuc2UwHhcNMDcxMDE1 -MTczNDQ1WhcNMTcxMDEyMTczNDQ1WjCBjDELMAkGA1UEBhMCU0UxCzAJBgNVBAgT -AkJMMRAwDgYDVQQHEwdSb25uZWJ5MSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRz -IFB0eSBMdGQxFjAUBgNVBAMTDUJyYXhlbiB1bml0ZWQxIzAhBgkqhkiG9w0BCQEW -FGJlbG9ybkBmdWt0LmJzbmV0LnNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC -CgKCAgEAxrMpGUiDUI0X3XgYovgRGaKaSIoCL7RlNVRHxWLEaNIEmHuhQv40fksa -ndO7KcbxBRTMvF/XNYyEEyks+D3jQrBmWBjsOSdZkLhP64HLnx2eYLd1HEAXIhB8 -wip180m9tfT6XeXX6cOh5AowMOGHv364xcZXJvYJgxVuMW1vlh9F79N1bnnR2rJ6 -4nmebJ91QW7ecSwA6h0fnx6GF3d5PfEC24/Fys/1NbdYiy8EYsP+lIEHQC4oD0Za -kjclNWT3O1I8lnLdm4F1KdgDIva7aCyoAI4rLsxFxnnaqQcPo+8gGhusfnbOSymd -m2tdz91x3Z2rSoo/cgc2TaPYKYuiJScsw1FftYGOZLiXbAedCqmoEC9XdMlZrbPn -AWhjSvbFY8B2LCoy23OFpV2h+QrnKXOzsxyfxexwkjytIn9Msy5DlcCQz93vO/c9 -CGN/GpXJf5ebNk4oTE7pXNMM27shvubtXa6BL7A+S7yB8xmQ3NA+CABsQsKHcgVy -2+SLD65Ms6OpwXn9BPlT1aJxbTiCXlSIQ7OIDPE1okwjxLTHc9dygoYuqjLG9Dkx -WOqZWNxyj3kU40mX96qbOhJG5Zgq9J9fUT4ZJETO754YIFGKltlT6RSqeLrNOuLg -9cYNGOLqoCVIyUi5HiTR7iIukNDeZy4YN6BiQH+TnYjC5+FwZbsCAwEAAaOCAXQw -ggFwMB0GA1UdDgQWBBT4/xxxSVgFPLkDVNc8Ocdvodu3JTCBwQYDVR0jBIG5MIG2 -gBT4/xxxSVgFPLkDVNc8Ocdvodu3JaGBkqSBjzCBjDELMAkGA1UEBhMCU0UxCzAJ -BgNVBAgTAkJMMRAwDgYDVQQHEwdSb25uZWJ5MSEwHwYDVQQKExhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQxFjAUBgNVBAMTDUJyYXhlbiB1bml0ZWQxIzAhBgkqhkiG -9w0BCQEWFGJlbG9ybkBmdWt0LmJzbmV0LnNlggkAwJJA/GbdObswDwYDVR0TAQH/ -BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAQYwCQYDVR0SBAIwADArBglghkgBhvhC -AQ0EHhYcVGlueUNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAfBgNVHREEGDAWgRRi -ZWxvcm5AZnVrdC5ic25ldC5zZTAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEF -BQADggIBAD/AJL0dKb4CcVEBzYsKV5JAxZQBTFYXm4TlMzbNJnl6C3FH636xSfvE -qWWPrIplbmNn0tyXNf1dvb2Xy4QVFTo5lnp6PKwhqpV5JK7+4/LiDZj5R4VNl36B -RUleXSSGV/+PJ1hRC9J/s+oziK1025hPpDB+n13sFrBv9DyqP/3GDSVoi13Mldf1 -vTWRnvugi7XlMhFdemB1Vz0eyIde0FtFpPrd3HeFrXQX8617XfcEYJmCU2aDT1fy -77RHFG+3dL286UcK2D85bqNCntP9W3zP6rwerkO1pGmT2cBn9AO6Qt2MI7klTJmw -SRqbeBcF+gaBRGY8iZHcqow7vqTvo65p/ZH9AUqMQh41W40FbepUA3354xGmwAlB -LBXRuVjaDvKyargh936dOyzIDr+fZtr6BEthmt6BghgOZlezAV/P60P56DlFvpK8 -4wHyT5KI9LOqbg+W2ToB/GdRZz4p7K90JljrgVlnQhYIQ1YT/5OzPu6Uq5+4Q4hc -ORb5hRCWHwre95kgEzUPqrteqV/g99XaOgqpK6KhY/dpmb7Z5I9f6d61XYYFfxYm -iuNJiTLCQlLyfXFSRlJubWD8hNHo2HNRYdiWF3cttuGL9aC0uSIk4xq/Oi/guqIn -sZlrO3I4MaiUnUXntMLU4zh0o/RPqhoT3lY1VubwgmpkOw8p7ggE ------END CERTIFICATE----- === removed file 'cert.pem' --- cert.pem 2007-10-20 21:38:25 +0000 +++ cert.pem 1970-01-01 00:00:00 +0000 @@ -1,40 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIG8zCCBNugAwIBAgIBATANBgkqhkiG9w0BAQUFADCBjDELMAkGA1UEBhMCU0Ux -CzAJBgNVBAgTAkJMMRAwDgYDVQQHEwdSb25uZWJ5MSEwHwYDVQQKExhJbnRlcm5l -dCBXaWRnaXRzIFB0eSBMdGQxFjAUBgNVBAMTDUJyYXhlbiB1bml0ZWQxIzAhBgkq -hkiG9w0BCQEWFGJlbG9ybkBmdWt0LmJzbmV0LnNlMB4XDTA3MTAxNTE3MzczMVoX -DTA4MTAxNDE3MzczMVowdTELMAkGA1UEBhMCU0UxCzAJBgNVBAgTAkJMMRAwDgYD -VQQHEwdSb25uZWJ5MREwDwYDVQQKEwhHbnVzdHVmZjEPMA0GA1UEAxMGQnJheGVu -MSMwIQYJKoZIhvcNAQkBFhRiZWxvcm5AZnVrdC5ic25ldC5zZTCCAiIwDQYJKoZI -hvcNAQEBBQADggIPADCCAgoCggIBAK4j59iJGfir5jxl4gCeLyhXED4YGd42o/ll -XdWtYO9IHtrCZknnRIjjviQ0PJk6JQpIGkNrdW5uRSQcTKshcEOL8cprUHe+Mpi8 -34i7bOaPlDdJYoMatkHEvq92weXFfKa8yG5UtmAbiuo388JbnVgU7HAqq1ipi+dS -MoJU0FR3qxKC3eioQQdq+QB257BeZNDq3SrWTqZXw7pu4DmiOZJhpMAV5CmjQKt4 -ZC0EdByGct0tp3X3swpBY+0a+wlFMKVe1WGy9dfhkNRVhUCXNKOCuaaIMsn8iuCc -NlkIw4GVhvJj9ALfRZS5VzRaKIhOnFub4+PxYClt1ghfTBhk1aQjiSl1JT+qtgpv -Sqd34OnYtLRp+L5EcLQSPoykG2TjsUxnYXtBiPoZuIk7v5alQlbT20DmuWhpCMm4 -fw1W0Q6eXL2APtrjAt7HS79qDopySL4st8nTkY3fzmjW8Vpg1Y4HyN6aY8tR9OIj -SwZhCv+NoixbZToUnLXmoRtQnO6rSp7qLuJCPC1gbDc1TR4mclr5Vumb8ag1iQTi -i0SvhqY8u1sDMGlL9aZW0WaykfoC64cUdBKm9R6dt08Y0xAevSaxdcsmK9kCfCJO -o6nJ2pQ9+WdSjYEhq5DLBGhpfhzs+U1ZL0HYmTfyVjmbWuDZaQ1Yht4vHf1TYwAp -FjGJGt/7AgMBAAGjggF0MIIBcDAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIG -QDArBglghkgBhvhCAQ0EHhYcVGlueUNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAd -BgNVHQ4EFgQUoq4JMCRt6ToFRTrLNUFd4xQ0hGUwgcEGA1UdIwSBuTCBtoAU+P8c -cUlYBTy5A1TXPDnHb6HbtyWhgZKkgY8wgYwxCzAJBgNVBAYTAlNFMQswCQYDVQQI -EwJCTDEQMA4GA1UEBxMHUm9ubmVieTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRYwFAYDVQQDEw1CcmF4ZW4gdW5pdGVkMSMwIQYJKoZIhvcNAQkB -FhRiZWxvcm5AZnVrdC5ic25ldC5zZYIJAMCSQPxm3Tm7MB8GA1UdEgQYMBaBFGJl -bG9ybkBmdWt0LmJzbmV0LnNlMB8GA1UdEQQYMBaBFGJlbG9ybkBmdWt0LmJzbmV0 -LnNlMA0GCSqGSIb3DQEBBQUAA4ICAQBC5Hz334l9o0RnjjjzZZyfYpEuAK7MJr6g -r7hPqZzAAXL7i4w5AEutthIxo+JGGL0P4xv2Swc6uwodU0GE4/6DUEBQrjaDhr/F -Uw3GaXYVopdDa/kYWAXF6lP+hkNhb1hI5onLMtRpoICLpfePNALZn3lMaBPI2efC -EvViu3dpjJKKNm/HlXOl/wgrrcLwHKlbPSozaiXe2qxik4fr15cXtm4/nXetZq1g -8NAOcZpdurZNEieMhtFSvLbQ3X/yNYKLgukz/zVCzL0IXLQXA4B2URBbzQJtrjOD -Agq5mAXhrlGuEEvaMJeQP+VrfsIZU2fFKvr8LVQO37GprQizPFBC8FDkrE4XrpET -A6ztDijyWRxT5x+4MbZrfL3GE9ZO57HZaAdXil1cjxXMlFqtJ6yoLNil3v+mgp9G -yJeEz6L3jYRxVZqXYy7VMh+rJ7Mdr31g9pT8TT0/LGoCBorCTcUYIBP3BE3F85Nk -JCPR5GG7Mfk3s8VTxTFAMZGKgst+i+YiMQSLkXIHNph5Fi18qIacpD9kkCXVj+JU -UW/fYcTd4vQdR2HhyJdPddtHkprMN3ZlICEug3junMOJb0PWByqfuaihkPYIZ2xw -puEkrP8cMdTXiUXep2RHTwByqy3/EQMC3ZWRiMIHZRIkl+o5bklOHwadUmzrJ1vL -QjmtPc7y1g== ------END CERTIFICATE----- === removed file 'client-cert.pem' --- client-cert.pem 2007-10-20 21:38:25 +0000 +++ client-cert.pem 1970-01-01 00:00:00 +0000 @@ -1,40 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIHBzCCBO+gAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBjDELMAkGA1UEBhMCU0Ux -CzAJBgNVBAgTAkJMMRAwDgYDVQQHEwdSb25uZWJ5MSEwHwYDVQQKExhJbnRlcm5l -dCBXaWRnaXRzIFB0eSBMdGQxFjAUBgNVBAMTDUJyYXhlbiB1bml0ZWQxIzAhBgkq -hkiG9w0BCQEWFGJlbG9ybkBmdWt0LmJzbmV0LnNlMB4XDTA3MTAxNTIwNDkwNloX -DTA4MTAxNDIwNDkwNlowfDELMAkGA1UEBhMCU0UxCzAJBgNVBAgTAkJMMRAwDgYD -VQQHEwdSb25uZWJ5MREwDwYDVQQKEwhnbnVzdHVmZjEWMBQGA1UEAxQNYnJheGVu -X2NsaWVudDEjMCEGCSqGSIb3DQEJARYUYmVsb3JuQGZ1a3QuYnNuZXQuc2UwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDWAwsXT4jeKCKg7/4g8KTmwEZ6 -88zfaJeqpzpkpc25J47Qmr5Mf6NiKUIAqjD8KycApG1cwVoLEvxvQOip2lSzL4ex -ipTpZO45O6N5blKG4XKqJRE2VE5N1j2Z+jJmfFEaHqMDYb9+dlnSZ2c0c1d2qcgC -HgOR5iZHWQCGtEK3y9HfWsFpULuzfYMIpYZjPic+kAuisT5cl/wIDVI/MbpB3/vk -9vMpRCFRjvbOcN7rEW4UihuVSFmzFNk99QR8e3Li1HtqbXmmtJps9u16ugGQO4ED -4gk65i5TU15sZJgFLXBuFuZFYH8WF2CIu6o4oylCLVjFRZHl8jKwZdPtNUZ01cJk -oj+M9Yc9m6F1dLafLThhvSLMuRfUvHY9bFAkF6QkkgIjY1mN1VTm6XxLN991DJd7 -67IfpFrBaKi47C4abBc4lZYfBlmXdkpOt3UcYxh3YT+KTUK/7S/arj2iEypTic4x -PVDsA4DbnHJ1+6ZzyMAM7ACmZnWhq7gXhjT8z3eEyaqaZVOracG1DFln781Mm6b9 -Eb3qJfczKR/QXiMsF4gwwH+Dclkd/THjjv/SPQYCiWm2Fo8IXG/hdZzBx4dWoSMZ -kgQM5GDLQU80/jAvDni6n97I48fcxjL6YnEgQeoasKoayGaOmnhEs/eytpcGocNi -3pJUpD0coYyAemeL5wIDAQABo4IBgTCCAX0wCQYDVR0TBAIwADARBglghkgBhvhC -AQEEBAMCBLAwKwYJYIZIAYb4QgENBB4WHFRpbnlDQSBHZW5lcmF0ZWQgQ2VydGlm -aWNhdGUwHQYDVR0OBBYEFK9FlbSWMi9o063eYNprvQ4Msg/FMIHBBgNVHSMEgbkw -gbaAFPj/HHFJWAU8uQNU1zw5x2+h27cloYGSpIGPMIGMMQswCQYDVQQGEwJTRTEL -MAkGA1UECBMCQkwxEDAOBgNVBAcTB1Jvbm5lYnkxITAfBgNVBAoTGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAxMNQnJheGVuIHVuaXRlZDEjMCEGCSqG -SIb3DQEJARYUYmVsb3JuQGZ1a3QuYnNuZXQuc2WCCQDAkkD8Zt05uzAfBgNVHRIE -GDAWgRRiZWxvcm5AZnVrdC5ic25ldC5zZTAfBgNVHREEGDAWgRRiZWxvcm5AZnVr -dC5ic25ldC5zZTALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQEFBQADggIBAINB3d3z -FDRV5/+vov3KhyKhhDYdhO2IqkSXARRQffrd94/F/8YcrOgJJrvnz6THbQIzuSM7 -sSE2Y2JqyWfaA0J8CQm9JrOgVTE9I74/IZONC3jSBrt/Q9dG74otaeaHNOTaH4lU -nAH5Cv6CgMFi58n+yQLxrYBAimWx8x1u40OxUXM+AbpAgDo3uXNt62SdrWAldwh3 -9Eqog3qUcN7R1VGx535VXJGg3e8yZFRPnFLiYuO1afjO/tlOyYBG0wvCfmNMSpcx -+/ycIXzTH5kvIcPoa8oCaAuCMgtsMZ2SoBJ37xgwb1qKjfVAOfju1qVYgI5jqQLp -sNsS4FqyS7jpKoUxbcShvOui5GUunXgxZ/VMd7rqSqbC8ozKXvOEYaJWf9lY7DCR -RdFR03CmWZ8EDO2JM1sKRni9gVz7wflSPjI8vDw9EbQzrW0Du64i4aBC2/kuJq9g -7rEQrjMRh8/kQigCP1BUk1l+r/gPO8UeIBc7tTls0FsPIkKQq7cPNQgNiNIIV8v2 -iLW1bZmT2vswpiyqbz7LtGzYj+x1Ce+7U9iXgY08QgbiqiXl/eNw/xNVkFaMMDc2 -4+APQMF790OaZvWtme9XzePsy16wLgF4G/Xh1M8RfG3z7vXXm4Vl38iErZkXTmMl -63Z87xA2LHvcoMEpP/nJHnvts3X2o3u7FFG9 ------END CERTIFICATE----- === removed file 'client-key.pem' --- client-key.pem 2007-10-20 21:38:25 +0000 +++ client-key.pem 1970-01-01 00:00:00 +0000 @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIJKgIBAAKCAgEA1gMLF0+I3igioO/+IPCk5sBGevPM32iXqqc6ZKXNuSeO0Jq+ -TH+jYilCAKow/CsnAKRtXMFaCxL8b0DoqdpUsy+HsYqU6WTuOTujeW5ShuFyqiUR -NlROTdY9mfoyZnxRGh6jA2G/fnZZ0mdnNHNXdqnIAh4DkeYmR1kAhrRCt8vR31rB -aVC7s32DCKWGYz4nPpALorE+XJf8CA1SPzG6Qd/75PbzKUQhUY72znDe6xFuFIob -lUhZsxTZPfUEfHty4tR7am15prSabPbteroBkDuBA+IJOuYuU1NebGSYBS1wbhbm -RWB/FhdgiLuqOKMpQi1YxUWR5fIysGXT7TVGdNXCZKI/jPWHPZuhdXS2ny04Yb0i -zLkX1Lx2PWxQJBekJJICI2NZjdVU5ul8SzffdQyXe+uyH6RawWiouOwuGmwXOJWW -HwZZl3ZKTrd1HGMYd2E/ik1Cv+0v2q49ohMqU4nOMT1Q7AOA25xydfumc8jADOwA -pmZ1oau4F4Y0/M93hMmqmmVTq2nBtQxZZ+/NTJum/RG96iX3Mykf0F4jLBeIMMB/ -g3JZHf0x447/0j0GAolpthaPCFxv4XWcwceHVqEjGZIEDORgy0FPNP4wLw54up/e -yOPH3MYy+mJxIEHqGrCqGshmjpp4RLP3sraXBqHDYt6SVKQ9HKGMgHpni+cCAwEA -AQKCAgBha4c7+EecoXaJ/lWXlxPpurMauyqStGD+HRvWvycz1s8LJLXlyuCMCa3y -8YZU9CvP/gmOhLHBgsYIuupuj2WpH8TMTAJXcEuFICHdYBwPLEdvLmp0adIvWow2 -MI+K2aJtmm6oVnG+Vo+y2MFBPhQdf1H9rL4BR1w7dEdqClqoog6Kdxy+HTMklMj2 -Qas4OA3TS+0QBVEXA1SGMdIz1CYuYJCg/M1aBpqILuUounavWQLcNLYzsXirrZzq -uENvix6UJRd9LhKHkYUOfyVBjbSyfHPRWa7L8gY6hiPggbY1/SZF5wSxpiiT3NZj -x9HH8HYSmuPjATVWEHeElwXu4CaOqax6OCMUwrIHWXUq/ydWjDXk/vK6PU1d2Fvh -Y274QkBHwrDRZ7HiDLZDO6VVJx9O0WKajkLqidVMFzSZem+rxlIW8KwJUto8ObEi -DK6VfkD0f1VfhWbVyb4dzgmloseHc9S/31ZBY4MfESpgVOh3rg12eXl4BVTRzDlf -Qq0Z9hKuBaURt/CNv+ALpAp+u6mvzmNuSnjuXOkD3bmYwPZljykf1A3Cr8eyPK1E -4PbLZN6Te7pbbjDvj+k23dzk1HUZ+I+5ykJi1v7tsQIiUOxFVJRMEmsfG+rMmKp6 -+9fsgcqK0wpnZUJ325ghjaaGR4i4AUMYVHn+mTmbhCDis97Q2QKCAQEA71FMbFGt -pBzYOCn9hMrBDIAU2tQNkmukeg7HremIyh5zm3Lt9YdSNGM/D1WqN3XdsdVczLoy -l89jUTyoqoFQkackTkPxftUd6655CuOqSGFhr8xVk7SSvC9ZbwV69WgVJkYfrYv5 -3gXPAmBgc3S8S400ik5AtKh3XIQMsWwKPdpHKQZvbpyw851IoRQEzUhiT/Bi3Uug -PzZHyFNmJMrPqSaot73zwkmkPK3KOCgAxStB/Ab2JNx8yjfL2UxIqunU5TaO65Wp -2Be2mSPwYe0FZ8oE4HPKmOWJC7jHgT88OzdaeT9jBTOecGbvm/Wz2Rr83wUkMZlf -+/sF2XOrIsCSjQKCAQEA5O4oDf+t0vvEonTC9YE5bQ/rkSz4SKYI23nnI+u19C4E -ntsHzrU9VMrnZV/Zc+j+z5hObgN179ePTvHLSjhvfo1zONGznl05YZRUE0dLf9bw -ns3tNjxYc86BtYGGJfX8WqgsTLCTotrRY8hk2D2NupzB21WfaalWi2pZEsEq5vk4 -gqh7IwraS7x56GQTQIPfdQmv9XNfDh7VO4bcaqAI1n+KIpJTFVcEHnu6RpoQyE12 -F2oEQOo/S4nY2N+MZioavO46o9r4p/3V8CyuXQghkal2GHZeCkTpMto1BayHYOKc -ZwxxLV/F/Nhr2BqJwiUojvbDcjcDeZVysjHQfNI1QwKCAQEAwwbd4OgvOa7IBf0y -PSV+bVFzrWFiLhDK2S1yTKgkcZKfY+8lPRIqS8cVfMmzDb9gC6x1E+IpmM3JgkqM -qWb44bn0PFPiWhoTaB2nRtiBzLqPgVRj6Rse+X0cxP0SVyubELXU2vlXhzf0m1sv -PufDC07nok5jLNadbyetsGj7b6ySkTxNUzcefWmP5rUJtMFoXPzplK4syVbS6M4O -T613zcFTfWmvXIXm8gwu39S5y/SUsW566U9F4wXVeiBQl/g3JxRvJE2zPAcXJ3XC -UAt4fDyF/ORgFnn4VTUgYJPH4foaIPUnHPYUCEXavp5dEnCL4rOt6z2ymwbWnX9F -1+xXgQKCAQEAr1XAE8ipRxhRJ+OplgKdCuzQjOYWWv3fdslMwHQ9bYD5RPmYZzbk -fFbTFw9sKpxe3HxYRWYdI87DEcGa44OJ0TFg+DmUCkx4MEY8hm9qYcUrkVVCwvFB -BaE6Mtu69MQLvRtkom/zAx34lSXcJsouDKkWyHgxmel6QVj4U6bixvhF0bxcYyBi -xPLbo6NSI21c7fS3cZQlT1lKE0dc9cUQP8h68bOEMqnsm7RDnONOyzqYoaIvE9DV -HXO8Q44jp/PDesQy4WBKZc/B1StDeDlMDJXvvxiZOeBBgxMg3PGg1hF5nGspG4lo -yBixsFfS/oEbKTPRyV5dKPfPWq9QcOlGpQKCAQEAwe2qtx7tzI7cdx1JLSHNZyPp -h9/WTjYKVg3Sl6dXfDsNTqIUkRrwpB1tEZXs1PIq5641ev4LmRxGDPLJWIwNZ1LN -slR12towc6LTY/05OIBHrTIAW8fGs2E4YcqYcLWsWuzU9BsJLQeBdUth9zF6qZoK -USN6a3326dx8lhUcQ4zJ9c/DtK+99WAXtt0Cki7aICvLbcsmRbKPHpJyzRpDI7uH -Ex/UgjSR1w5L2Seou3e6ljxYGAecqa86HIkHVleDIrIYl6GlKDBKSVzfmLh/97mi -o3oTNqRal934xYS6ZTId/ZkRL5c78U/NJlK1H5gYzfL65mJ1+c2N3cR3sy4jtQ== ------END RSA PRIVATE KEY----- === renamed file 'mandos-clients.conf' => 'clients.conf' --- mandos-clients.conf 2008-07-22 06:23:29 +0000 +++ clients.conf 2019-02-09 23:34:15 +0000 @@ -1,33 +1,93 @@ +# Default settings for all clients. These values are the default +# values, so uncomment and change them if you want different ones. [DEFAULT] -timeout = 1h -interval = 5m -checker = fping -q -- %%(fqdn)s - -# Example -[foo] -fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 -secret = Base+64+encoded+OpenPGP+encrypted+data/= -# secfile = /etc/mandos/foo-secret.txt.asc -fqdn = foo.example.org -checker = fping -q -- %%(fqdn)s -timeout = 10m - -[braxen_client] -fingerprint = 7788 2722 5BA7 DE53 9C5A 7CFA 59CF F7CD BD9A 5920 -secret = - hQIOA6QdEjBs2L/HEAf/TCyrDe5Xnm9esa+Pb/vWF9CUqfn4srzVgSu234REJMVv - 7lBSrPE2132Lmd2gqF1HeLKDJRSVxJpt6xoWOChGHg+TMyXDxK+NXl89vGvdU1Xf - hKkVm9MDLOgT5ECDPysDGHFPDhqHOSu3Kaw2DWMV/iH9vz3Z20erVNbdcvyBnuoj - coWO/6yfB5EQO0BXp7kcyy00USA3CjD5FGZdoQGITb8A/ar0tVA5crSQmaSotm6K - mNLhrFnZ5BxX+TiE+eTUTqSloWRY6VAvqWQHC7OASxK5E6RXPBuFH5IohUA2Qbk5 - AHt99pYvsIPX88j2rWauOokoiKZot/9leJ8VxO5l3wf/U64IH8bkPIoWmWZfd/nq - h4uwGNbCgKMyT+AnvH7kMJ3i7DivfWl2mKLV0PyPHUNva0VQxX6yYjcOhj1R6fCr - /at8/NSLe2OhLchzdC+Ls9h+kvJXgF8Sisv+Wk/1RadPLFmraRlqvJwt6Ww21Lpi - XqXHV2mIgqWnR98YgSvUi3TJHrUQiNc9YyBzuRo0AjgG2C9qiE3FM+Y28+iQ/sR3 - +bFszYuZKVTObqiIslwXu7imO0cvvFRgJF/6u3HNFQ4LUTGhiM3FQmC6NNlF3/vJ - M2hwRDMcJqDd54Twx90Wh+tYz0z7QMsK4ANXWHHWHR0JchnLWmenzbtW5MHdW9AY - sNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm4T2zw4dxS5NswX - WU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2OQlnHIvPzEArRQLo= - =iHhv -fqdn = localhost -interval = 5m + +# How long until a client is disabled and not be allowed to get the +# data this server holds. +;timeout = PT5M + +# How often to run the checker to confirm that a client is still up. +# Note: a new checker will not be started if an old one is still +# running. The server will wait for a checker to complete until the +# above "timeout" occurs, at which time the client will be disabled, +# and any running checker killed. +;interval = PT2M + +# Extended timeout is an added timeout that is given once after a +# password has been sent sucessfully to a client. This allows for +# additional delays caused by file system checks and quota checks. +;extended_timeout = PT15M + +# What command to run as "the checker". +;checker = fping -q -- %%(host)s + +# Whether to approve a client by default after the approval delay. +;approved_by_default = True + +# How long to wait for approval. +;approval_delay = PT0S + +# How long one approval will last. +;approval_duration = PT1S + +# Whether this client is enabled by default +;enabled = True + + +;#### +;# Example client +;[foo] +; +;# TLS public key ID +;key_id = f33fcbed11ed5e03073f6a55b86ffe92af0e24c045fb6e3b40547b3dc0c030ed +; +;# OpenPGP key fingerprint +;fingerprint = 7788 2722 5BA7 DE53 9C5A 7CFA 59CF F7CD BD9A 5920 +; +;# This is base64-encoded binary data. It will be decoded and sent to +;# the client matching the above fingerprint. This should, of course, +;# be OpenPGP encrypted data, decryptable only by the client. +;secret = +; hQIOA6QdEjBs2L/HEAf/TCyrDe5Xnm9esa+Pb/vWF9CUqfn4srzVgSu234 +; REJMVv7lBSrPE2132Lmd2gqF1HeLKDJRSVxJpt6xoWOChGHg+TMyXDxK+N +; Xl89vGvdU1XfhKkVm9MDLOgT5ECDPysDGHFPDhqHOSu3Kaw2DWMV/iH9vz +; 3Z20erVNbdcvyBnuojcoWO/6yfB5EQO0BXp7kcyy00USA3CjD5FGZdoQGI +; Tb8A/ar0tVA5crSQmaSotm6KmNLhrFnZ5BxX+TiE+eTUTqSloWRY6VAvqW +; QHC7OASxK5E6RXPBuFH5IohUA2Qbk5AHt99pYvsIPX88j2rWauOokoiKZo +; t/9leJ8VxO5l3wf/U64IH8bkPIoWmWZfd/nqh4uwGNbCgKMyT+AnvH7kMJ +; 3i7DivfWl2mKLV0PyPHUNva0VQxX6yYjcOhj1R6fCr/at8/NSLe2OhLchz +; dC+Ls9h+kvJXgF8Sisv+Wk/1RadPLFmraRlqvJwt6Ww21LpiXqXHV2mIgq +; WnR98YgSvUi3TJHrUQiNc9YyBzuRo0AjgG2C9qiE3FM+Y28+iQ/sR3+bFs +; zYuZKVTObqiIslwXu7imO0cvvFRgJF/6u3HNFQ4LUTGhiM3FQmC6NNlF3/ +; vJM2hwRDMcJqDd54Twx90Wh+tYz0z7QMsK4ANXWHHWHR0JchnLWmenzbtW +; 5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm +; 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O +; QlnHIvPzEArRQLo= +; +;# Host name; used only by the checker, not used by the server itself. +;host = foo.example.org +;#### + +;#### +;# Another example client, named "bar". +;[bar] +;# The key ID is not space or case sensitive +;key_id = F33FCBED11ED5E03073F6A55B86FFE92 AF0E24C045FB6E3B40547B3DC0C030ED +; +;# The fingerprint is not space or case sensitive +;fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 +; +;# If "secret" is not specified, a file can be read for the data. +;secfile = /etc/keys/mandos/bar-secret.bin +; +;# An IP address for host is also fine, if the checker accepts it. +;host = 192.0.2.3 +; +;# Parameters from the [DEFAULT] section can be overridden per client. +;interval = PT1M +; +;# This client requires manual approval before it receives its secret. +;approved_by_default = False +;# Require approval within 30 seconds. +;approval_delay = PT30S +;#### === added file 'common.ent' --- common.ent 1970-01-01 00:00:00 +0000 +++ common.ent 2019-09-03 19:06:41 +0000 @@ -0,0 +1,3 @@ + + + === removed file 'crl.pem' --- crl.pem 2007-10-20 21:38:25 +0000 +++ crl.pem 1970-01-01 00:00:00 +0000 @@ -1,18 +0,0 @@ ------BEGIN X509 CRL----- -MIIC0zCBvDANBgkqhkiG9w0BAQUFADCBjDELMAkGA1UEBhMCU0UxCzAJBgNVBAgT -AkJMMRAwDgYDVQQHEwdSb25uZWJ5MSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRz -IFB0eSBMdGQxFjAUBgNVBAMTDUJyYXhlbiB1bml0ZWQxIzAhBgkqhkiG9w0BCQEW -FGJlbG9ybkBmdWt0LmJzbmV0LnNlFw0wNzEwMTUxNzQ2MTlaFw0wNzExMTQxNzQ2 -MTlaMA0GCSqGSIb3DQEBBQUAA4ICAQCYTCUYRfx8+lTNXsVMhLT/890agPGj7BQw -NhwHTZuEudPTxBtOLPf0za4z7eGTD9ggu7SayQWEOV4bfjv1yiOLzf6vEHdzv1Ee -mlLhYgDMIhACrQmfKAmjoabsaK+VccBJW/R1oNW5Z9sWjFP91+3T7lfp8pPvWAlf -+9mJaaysd1yguY0OITAIWEL2lLlGtd85RYLvJe2nWZ6GrH5mIEYA7IQrnPgcU3ij -eAEn88I7EofUHfn1TMpMDJgMKm/edvEerLKb62AblcGLfo4gOBQWcvCAWLPzqxhE -wKag1xL8ucG6250yfkYBf3KEMLZAU8py5MwaIqMVOQzz3gsQ7dE87xR5GndLvPvr -149RbKDSZDdPDOEmCqlmb/Sxppm7jsNwqAphrZlNsBgLTrxih7Ex1cFph4jA5AZ5 -Hgqpftb94CdauOPz/AVu5aeXIwG0dpxhN0dtemwhHIxslFwDtuFWwcmP3upDZUOM -Q4ZZBp2A1lhDW86w3law8E2TCuDFmwNtqp5zOMtOwAJF2RK2DquaaxKQG0JRhdTi -f3mzjTTRPXomBgf7U1W25RFMO91uslCijAr/ELAa9SkFYWArnbfGabDsoy3OGL8D -fjuaz2eIdwvASyPwUkGlfeFBRDNIRCZ3szsvtThMFWUxKvKTRBTJf4AFLwx3XWOS -R6IxSk3t0A== ------END X509 CRL----- === added file 'dbus-mandos.conf' --- dbus-mandos.conf 1970-01-01 00:00:00 +0000 +++ dbus-mandos.conf 2011-10-02 19:18:24 +0000 @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + === added directory 'debian' === added file 'debian/changelog' --- debian/changelog 1970-01-01 00:00:00 +0000 +++ debian/changelog 2019-09-04 21:17:42 +0000 @@ -0,0 +1,928 @@ +mandos (1.8.9-2) unstable; urgency=medium + + * Fix failing autopkgtest. + * debian/tests/control (mandos-check/Restrictions): Add "allow-stderr". + + -- Teddy Hogeborn Wed, 04 Sep 2019 23:14:06 +0200 + +mandos (1.8.9-1) unstable; urgency=medium + + * New upstream release. + * Fix "Python2 removal in sid/bullseye" by using Python 3 instead + (Closes: #936987) + * debian/control (Build-Depends, Build-Depends-Indep): Move "systemd" + from indep to regular build-depends. + (Build-Depends-Indep, Package: mandos/Depends): Depend on Python 3 and + Python 3 modules instead of Python 2. + + -- Teddy Hogeborn Tue, 03 Sep 2019 20:58:27 +0200 + +mandos (1.8.8-1) unstable; urgency=medium + + * New upstream release. + * debian/po/de.po: New; Fix "[INTL:de] Initial German debconf + translation" by including the contributed translation (Closes: + #934373) + * debian/po/fr.po: New; Fix "[INTL:fr] French debconf templates + translation" by including the contributed translation (Closes: + #934888) + * debian/po/sv.po: New Swedish translation. + * debian/mandos.postinst: Only reload D-Bus daemon if new user was + created. + * debian/mandos.dirs (usr/lib/sysusers.d): New. + * debian/mandos-client.dirs (usr/lib/sysusers.d): - '' - + + -- Teddy Hogeborn Sun, 18 Aug 2019 22:01:13 +0200 + +mandos (1.8.7-1) unstable; urgency=medium + + * New upstream release. + * debian/upstream/metadata: New. + * debian/mandos-client.postrm: Use the same logic as the + update_initramfs function in debian/mandos-client.postinst. + * debian/mandos-client.templates (mandos-client/key_id): Line which + should not be wrapped should be prefixed by a space. + * debian/mandos.templates (mandos/key_id): - '' - + * debian/po/en_US.po: New "translation" from ASCII to UTF-8. + * debian/po/templates.pot: Updated. + * debian/source/lintian-overrides + (package-uses-old-debhelper-compat-version): New; set to "10". + * debian/mandos-client.lintian-overrides + (maintainer-script-supports-ancient-package-version): New. + debian/mandos.lintian-overrides + (maintainer-script-supports-ancient-package-version): - '' - + + -- Teddy Hogeborn Mon, 05 Aug 2019 23:22:00 +0200 + +mandos (1.8.6-1) unstable; urgency=medium + + * New upstream release. + * Fix "mandos FTCBFS: hard codes build architecture pkg-config" + by making pkg-config overridable (Closes: #933701) + * debian/mandos.postinst (configure): After creating (or renaming) user + & group, reload D-Bus daemon (if present). + + -- Teddy Hogeborn Sat, 03 Aug 2019 14:51:01 +0200 + +mandos (1.8.5-1) unstable; urgency=medium + + * New upstream release. + * Fix "does not reap children" by reaping children (Closes: #933387) + * debian/mandos-client.README.Debian: Use new-style interface name. + * debian/tests/control: New file; implements autopkgtest support. + * debian/mandos-client.lintian-overrides + (manpage-has-errors-from-man): Remove; unnecessary. + * debian/mandos.lintian-overrides + (init.d-script-needs-depends-on-lsb-base): - '' - + * debian/mandos-client.postinst (update_initramfs): Upstream now + supports dracut(8), so update commands here to and run the correct + command to update initramfs. + * debian/control (Build-Depends): Add GLib -dev package. + (mandos-client/Depends): Add dracut(8) as an alternative dependency to + initramfs-tools. + (mandos-client/Conflicts): New; set to "dracut-config-generic". + (debian/mandos-client.README.Debian): Update for dracut(8) support. + * debian/mandos-client.templates: Reflowed by debconf-gettextize(1). + * debian/mandos.templates: - '' - + * debian/po/POTFILES.in: New. + * debian/po/templates.pot: - '' - + * debian/source/lintian-overrides: New. + * debian/control (Standards-Version): Update to "4.4.0". + + -- Teddy Hogeborn Tue, 30 Jul 2019 20:41:29 +0200 + +mandos (1.8.4-1) unstable; urgency=medium + + * Fix "dirs in initrd are not accessible by mandos plugin-runner" by + making sure UMASK is set, no matter what other packages have installed + in "/usr/share/initramfs-tools/conf-hooks.d". (Closes: #926641) + * Fix "LeakSanitizer: detected memory leaks, fails to decrypt" + by fixing memory leak in plugin-runner. (Closes: #926643) + * debian/mandos-client.dirs: Add + "usr/share/initramfs-tools/conf-hooks.d", needed by fix for #926641. + + -- Teddy Hogeborn Tue, 09 Apr 2019 22:05:39 +0200 + +mandos (1.8.3-3) unstable; urgency=medium + + * Fix "src:mandos: modifies d/control during build" by not doing that + anymore. (Closes: #922202) + * debian/rules (override_dh_shlibdeps-arch): Commented out. + + -- Teddy Hogeborn Wed, 13 Feb 2019 09:52:39 +0100 + +mandos (1.8.3-2) unstable; urgency=medium + + * debian/rules (override_dh_shlibdeps-arch): New; conditionally edit + debian/control before running dh_shlibdeps. + + -- Teddy Hogeborn Mon, 11 Feb 2019 12:49:57 +0100 + +mandos (1.8.3-1) unstable; urgency=medium + + * New upstream release. + * debian/watch: Make the ".orig" file name suffix non-optional; + otherwise uscan thinks that ".orig" is part of the version number. + * debian/control (Build-Depends): Changed GnuTLS dependencies; move + 3.6.6 alternative to first in list, and remove dependencies on the + virtual package "gnutls-dev", since we need the version restrictions. + (Package: mandos/Depends): Remove dependency on libgnutls28-dev + package. + (Package: mandos/Suggests): New; set to "libc6-dev, c-compiler". (Used + to find value of "SO_BINDTODEVICE"). + (Package: mandos-client/Depends): Don't depend on openssl anymore; + instead depend on either a gnutls-bin (>= 3.6.6) (in which case TLS + key generation will work), or on libgnutls30 (<< 3.6.0) (in which case + TLS key generation will not be needed). + + -- Teddy Hogeborn Mon, 11 Feb 2019 07:30:32 +0100 + +mandos (1.8.2-1) unstable; urgency=medium + + * New upstream release. + * debian/mandos-client.postinst (create_keys): Ignore failure to remove + bad keys. + + -- Teddy Hogeborn Sun, 10 Feb 2019 11:44:56 +0100 + +mandos (1.8.1-1) unstable; urgency=high + + * New upstream release. + * debian/mandos-client.postinst (create_keys): Remove any bad keys + created by 1.8.0-1. Only create TLS keys if certtool succeeds. + * debian/mandos.postinst (configure): Remove any bad keys from + clients.conf, and inform the user if any were found. + * debian/mandos.templates (mandos/removed_bad_key_ids): New message. + + -- Teddy Hogeborn Sun, 10 Feb 2019 10:00:21 +0100 + +mandos (1.8.0-1) unstable; urgency=medium + + * New upstream release. + * Fix "(tries to) use GnuTLS OpenPGP support" by using raw public keys + when available (Closes: #879538) + * Fix "mandos : Depends: libgnutls30 (< 3.6.0) but 3.6.5-2 is to be + installed" by now also allowing GnuTLS >= 3.6.6 (Closes: #916673) + * debian/control (Standards-Version): Update to "4.3.0". + (Package: mandos-client/Depends): Change from "cryptsetup" to + "cryptsetup (<< 2:2.0.3-1) | cryptsetup-initramfs". Add "debconf (>= + 1.5.5) | debconf-2.0". + (Source: mandos/Build-Depends): Also allow libgnutls30 (>= 3.6.6). + (Package: mandos/Depends): - '' - and add debconf (>= 1.5.5) | + debconf-2.0". + (Package: mandos/Description): Alter description to match new design. + (Package: mandos-client/Description): - '' - + (Package: mandos-client/Depends): Move "gnutls-bin | openssl" to here + from "Recommends". + * debian/mandos-client.README.Debian: Add --tls-privkey and --tls-pubkey + options to test command. + * debian/mandos-client.postinst (create_key): Renamed to "create_keys" + - all callers changed - and also create TLS key files. Show notice if + new TLS key files were created. + * debian/mandos-client.postrm (purge): Also remove TLS key files. + * debian/mandos-client.lintian-overrides: Override warnings. + * debian/mandos-client.templates: New. + * debian/mandos.lintian-overrides: Override warnings. + * debian/mandos.postinst (configure): If GnuTLS 3.6.6 or later is + detected, show an important notice (once) about the new key_id option + required in clients.conf. + * debian/mandos.templates: New. + * debian/copyright: Update copyright year to 2019. + + -- Teddy Hogeborn Sun, 10 Feb 2019 05:52:49 +0100 + +mandos (1.7.20-1) unstable; urgency=medium + + * New upstream release. + * Fix "[tethys] mandos-client: Mandos client fails while booting but + works from chroot into unpacked initramfs" by setting system clock if + necessary (Closes: #894495) + * Fix "initramfs boot script assumes internal cryptsetup implementation + details and is now broken" by only using documented + interfaces (Closes: #904899) + * debian/mandos-client.dirs: Add + "usr/share/initramfs-tools/scripts/local-premount" and + "usr/share/initramfs-tools/conf.d", and remove + "usr/share/initramfs-tools/conf-hooks.d". + * debian/control (mandos-client/Depends): Add "(>= 0.99)" to dependency + on "initramfs-tools". + * debian/control (Source: mandos/Rules-Requires-Root): New; set to + "binary-targets". + (Standards-Version): Update to "4.2.0". + + -- Teddy Hogeborn Sun, 19 Aug 2018 22:14:04 +0200 + +mandos (1.7.19-1) unstable; urgency=medium + + * New upstream release. + * Fix "fails with "LeakSanitizer has encountered a fatal error"" by not + using LeakSanitizer in affected binary (Closes: #886595) + + -- Teddy Hogeborn Thu, 22 Feb 2018 19:47:59 +0100 + +mandos (1.7.18-1) unstable; urgency=medium + + * New upstream release. + + -- Teddy Hogeborn Mon, 12 Feb 2018 16:00:11 +0100 + +mandos (1.7.17-1) unstable; urgency=medium + + * New upstream release. + * Fix "fails with "LeakSanitizer has encountered a fatal error"" + by fixing memory leak in plugin-runner (Closes: #886595) + * debian/control (Build-Depends): Also depend on "libgnutls28-dev (<< + 3.6.0) | libgnutls30 (<< 3.6.0)". + (Package: mandos/Depends): - '' - + * debian/compat: Change to "10". + * debian/watch (version): Change to "4". + (opts/pgpsigurlmangle): Remove. + (opts/pgpmode): New; set to "auto". + (URL): Change to "https://ftp.recompile.se/pub/@PACKAGE@/@PACKAGE@ + @ANY_VERSION@(?:\.orig)?@ARCHIVE_EXT@". + * debian/copyright: Update copyright year to 2018. + * debian/rules: Support the "noopt" and "parallel" DEB_BUILD_OPTIONS. + (override_dh_fixperms-arch): Use the DEB_HOST_MULTIARCH + variable directly instead of shelling out to "dpkg-architecture". + * debian/control (Standards-Version): Update to "4.1.3". + (Build-Depends): Change version of debhelper dependency to ">= 10". + * debian/mandos.lintian-overrides + (init.d-script-needs-depends-on-lsb-base): Change line number to "46". + + -- Teddy Hogeborn Sat, 10 Feb 2018 19:09:50 +0100 + +mandos (1.7.16-1) unstable; urgency=medium + + * New upstream release. + * debian/copyright (License): Use program name explicitly. + (Format): Use https in URL. + * debian/control (Priority): Change from "extra" to "optional". + (Standards-Version): Update to "4.0.1". + + -- Teddy Hogeborn Sun, 20 Aug 2017 21:05:26 +0200 + +mandos (1.7.15-1) unstable; urgency=medium + + * New upstream release. + * Upstream release fixes "Seems not to be honoring zeroconf option at + mandos.conf" (Closes: #855589) + * debian/mandos.lintian-overrides (mandos): Add new line + "init.d-script-needs-depends-on-lsb-base etc/init.d/mandos (line 49)". + * debian/copyright: Update copyright year to 2017. + + -- Teddy Hogeborn Thu, 23 Feb 2017 21:29:36 +0100 + +mandos (1.7.14-1) unstable; urgency=medium + + * New upstream release. + * debian/mandos-client.postinst (create_key): Stop GPG agent after + running mandos-keygen. + * debian/control (Package: mandos/Depends): Add "systemd-sysv | lsb-base + (>= 3.0-6)", change "gnupg" to "gnupg2 | gnupg", and change + "libgpgme11-dev" to "libgpgme-dev | libgpgme11-dev". + + -- Teddy Hogeborn Wed, 25 Jan 2017 20:36:03 +0100 + +mandos (1.7.13-1) unstable; urgency=medium + + * New upstream release. + * Fix "fails to install noninteractively" by using the "%no-protection" + statement in the GnuPG batch parameter file. (Closes: #840001) + + -- Teddy Hogeborn Sat, 08 Oct 2016 06:31:07 +0200 + +mandos (1.7.12-1) unstable; urgency=medium + + * New upstream release. + + -- Teddy Hogeborn Wed, 05 Oct 2016 22:06:55 +0200 + +mandos (1.7.11-1) unstable; urgency=high + + * New upstream release. + * debian/control (Source: mandos/Vcs-Bzr): Change to use HTTPS. + (Vcs-Browser): - '' - + + -- Teddy Hogeborn Sat, 01 Oct 2016 16:20:48 +0200 + +mandos (1.7.10-1) unstable; urgency=high + + * New upstream release. + * debian/rules (override_dh_fixperms-arch): Also exclude + "etc/mandos/plugin-helpers" from changes by dh_fixperms. + * debian/mandos-client.postinst: Fix the permissions of + "/etc/mandos/plugin-helpers" for those systems which had a fresh + install of an older version. + + -- Teddy Hogeborn Thu, 23 Jun 2016 22:00:29 +0200 + +mandos (1.7.9-1) unstable; urgency=medium + + * New upstream release. + + -- Teddy Hogeborn Wed, 22 Jun 2016 07:30:12 +0200 + +mandos (1.7.8-1) unstable; urgency=medium + + * New upstream release. + * Fix "bad gpgme_op_decrypt: GPGME: Decryption failed." by copying + /usr/bin/gpg-agent into initramfs (Closes: #819982) + * debian/control (Homepage): Change URL to use HTTPS. + (Standards-Version): Update to 3.9.8. + * debian/copyright (Source): Change URL to HTTPS. + * debian/mandos-client.README.Debian: Change wording to match updated + capabilities. + + -- Teddy Hogeborn Tue, 21 Jun 2016 21:36:10 +0200 + +mandos (1.7.7-1) unstable; urgency=medium + + * New upstream release. + * debian/mandos-client.postinst (configure): If older version, fix + permissions on plugin helper directory. Also fix permissions on + plugin helper local override directory (/etc/mandos/plugin-helpers), + but only if not listed by "dpkg-statoverride". + * debian/rules (override_dh_fixperms-arch): Exclude plugin helper + directory from dh_fixperms. + * debian/mandos.postinst (configure): Fix state directory permissions, + but only if not listed by "dpkg-statoverride". + * debian/mandos-client.lintian-overrides: Do not warn about permissions + on plugin helper directory. + * debian/mandos.dirs (usr/lib/tmpfiles.d): Added. + + -- Teddy Hogeborn Sat, 19 Mar 2016 22:58:49 +0100 + +mandos (1.7.6-1) unstable; urgency=medium + + * New upstream release. + * debian/control (Source: mandos/Build-Depends-Indep): Remove + "python-avahi". + (Source: mandos/Build-Depends-Indep): Change "python-gi | + python-gobject" to "python-gi"; i.e. remove "python-gobject". + + -- Teddy Hogeborn Sun, 13 Mar 2016 22:58:23 +0100 + +mandos (1.7.5-1) unstable; urgency=high + + * New upstream release. + * debian/mandos.postinst (configure): If old version was 1.7.4-1 or + 1.7.4-1~bpo8+1, fix situation where clients.pickle file is owned by + root. + + -- Teddy Hogeborn Tue, 08 Mar 2016 01:09:55 +0100 + +mandos (1.7.4-1) unstable; urgency=medium + + * New upstream release. + * initramfs-tools-script: Fix "Call to configure_network in initramfs + script broken due to set -e" by surrounding call by "set +x" and "set + -e" (Closes: #816513) + * debian/control: (Source: mandos/Build-Depends-Indep): Change + "python-gobject | python-gi" to "python-gi | python-gobject" + (Package: mandos/Depends): - '' - + + -- Teddy Hogeborn Sat, 05 Mar 2016 23:10:07 +0100 + +mandos (1.7.3-1) unstable; urgency=medium + + * New upstream release. + + -- Teddy Hogeborn Mon, 29 Feb 2016 22:26:38 +0100 + +mandos (1.7.2-1) unstable; urgency=medium + + * New upstream release. + * Fix "Uses unneeded and obsolete version specific python packages" + by removing version-specific dependencies (Closes: #811159) + * debian/control (Source: mandos/Build-Depends): Add (>= 3.3.0) to + "libgnutls28-dev" and "gnutls-dev". + (Source: mandos/Build-Depends-Indep): Remove "python2.7-gnutls", + "python2.7", "python2.7-dbus", "python2.7-avahi", and + "python2.7-gobject"; replace with "python (>= 2.7), python (<< 3)", + "python-dbus", "python-avahi", "python-gobject | python-gi". + (Package: mandos/Depends): Remove "python-gnutls" and + "python2.7-gnutls", add "libgnutls28-dev (>= 3.3.0) | libgnutls30 (>= + 3.3.0)". Add "python (<< 3)". Remove "python2.7-dbus", + "python2.7-avahi", "python2.7-gobject", and "python2.7-urwid". + Replace "python-gobject" with "python-gobject | python-gi" and "gnupg + (<< 2)" with "gnupg". + (Package: mandos-client/Depends): Replace + "gnupg (<< 2)" with "gnupg". + (Source: mandos/Standards-Version): Change to 3.9.7. + * debian/copyright (Copyright): Update copyright year. + + -- Teddy Hogeborn Sun, 28 Feb 2016 16:09:01 +0100 + +mandos (1.7.1-2) unstable; urgency=medium + + * debian/control (Package: mandos/Depends): Fix "Please drop versioned + dependency on initscripts package" by removing initscripts dependency + (Closes: #804967) + * debian/rules (override_dh_fixperms) Fix "FTBFS when built with + dpkg-buildpackage -A (No such file or directory)" by splitting into + "override_dh_fixperms-arch" and "override_dh_fixperms-indep". + (Closes: #806073) + + -- Teddy Hogeborn Sat, 05 Dec 2015 02:27:40 +0100 + +mandos (1.7.1-1) unstable; urgency=medium + + * New upstream release. + + -- Teddy Hogeborn Sat, 24 Oct 2015 19:43:40 +0200 + +mandos (1.7.0-1) unstable; urgency=medium + + * New upstream release. + * debian/control (Standards-Version): Updated to "3.9.6". + (Build-Depends): Add "libnl-route-3-dev". + (Package: mandos-client/Recommends): Added "gnutls-bin | openssl" for + the generating of DH parameters. + * debian/mandos-client.README.Debian: Update example command line to use + new MANDOSPLUGINHELPERDIR environment variable. Also document the new + dhparams.pem file. + * debian/mandos-client.postinst: Create DH parameters file. + * debian/mandos.prerm: Don't run init script, use only invoke-rc.d. + * debian/mandos-client.postinst: Don't use absolute paths to commands. + * debian/mandos-client.postrm: Don't use absolute paths to commands. + Also remove dhparams.pem file. + * debian/copyright (Copyright): Update copyright year. + * Upstream changed systemd service file to implicitly be of + "Type=dbus". (Closes: #786845) + + -- Teddy Hogeborn Mon, 10 Aug 2015 22:00:29 +0200 + +mandos (1.6.9-1) unstable; urgency=medium + + * New upstream release. + * debian/control (Build-Depends): Fix "still uses GnutLS 2.x" by + changing from "libgnutls-dev" to "libgnutls28-dev | gnutls-dev" + (Closes: #762349) + + -- Teddy Hogeborn Sun, 05 Oct 2014 22:05:06 +0200 + +mandos (1.6.8-1) unstable; urgency=medium + + * New upstream release. + * debian/control (Source: mandos/Build-Depends-Indep): Since upstream + now requires Python 2.7, depend on exactly the python2.7 package and + all the Python 2.7 versions of the python modules. + (Package: mandos/Depends): - '' - but still depend on python (>=2.7) + and the generic versions of the Python modules; this is for mandos-ctl + and mandos-monitor, both of which are compatible with Python 3, and + use #!/usr/bin/python. + + -- Teddy Hogeborn Wed, 06 Aug 2014 22:55:24 +0200 + +mandos (1.6.7-1) unstable; urgency=medium + + * New upstream release. + + -- Teddy Hogeborn Thu, 17 Jul 2014 05:22:45 +0200 + +mandos (1.6.6-1) unstable; urgency=medium + + * New upstream release. + * debian/mandos.postinst: Fix typo in comment. + * debian/control (mandos/Recommends): Changed to "ssh-client | fping". + (mandos-client/Recommends): New; set to "ssh". + + -- Teddy Hogeborn Sun, 13 Jul 2014 22:49:21 +0200 + +mandos (1.6.5-3) unstable; urgency=medium + + * debian/control (mandos-client/Depends): Add "dpkg-dev (>=1.16.0)"; + initramfs-tools-hook runs "dpkg-architecture -qDEB_HOST_MULTIARCH". + (Closes: #750221) + + -- Teddy Hogeborn Fri, 06 Jun 2014 04:27:15 +0200 + +mandos (1.6.5-2) unstable; urgency=medium + + * debian/rules (override_dh_auto_test-arch): New; does nothing. Fixes + FTBFS for build-indep. + + -- Teddy Hogeborn Tue, 13 May 2014 08:08:31 +0200 + +mandos (1.6.5-1) unstable; urgency=medium + + * New upstream release. + * debian/copyright: Change year to "2014". + * debian/control (Build-Depends, Build-Depends-Indep): Moved build + dependencies of "mandos" package to "Build-Depends-Indep". + * debian/upstream/signing-key.asc: New; upstream source public key. + * debian/control (Standards-Version): Updated to "3.9.5". + * debian/control (mandos/Depends): Remove the dependency on + "avahi-daemon (>= 0.6.31-3) | systemd-sysv". It is unnecessary + since we have a workaround in debian/mandos.postinst anyway. + + -- Teddy Hogeborn Sun, 11 May 2014 22:16:33 +0200 + +mandos (1.6.4-1) unstable; urgency=medium + + * New upstream release. + * debian/control (Build-Depends): Add Python dependencies to + successfully run self-tests. + * debian/copyright: GPLv3 now has its own license file - use it. + * debian/watch: Set PGP signature URL. + + -- Teddy Hogeborn Sun, 16 Feb 2014 14:09:25 +0100 + +mandos (1.6.3-1) unstable; urgency=low + + * New upstream release. + * debian/control (Build-Depends): Added "systemd". + * debian/mandos.dirs (lib/systemd/system): New. + * debian/mandos-client.README.Debian: Refer to architecture libdir. + * debian/control (mandos/Depends): Add "avahi-daemon (>= 0.6.31-3) | + systemd-sysv". + * debian/mandos.postinst: If avahi-daemon is version 0.6.31-2 or older, + edit /etc/init.d script headers Required-Start + and Required-Stop to have "avahi" instead of + "avahi-daemon", before insserv(8) sees it. + * debian/mandos-client.lintian-overrides: Libdir changes. + * debian/rules (override_dh_fixperms): - '' - + + -- Teddy Hogeborn Tue, 21 Jan 2014 22:01:30 +0100 + +mandos (1.6.2-1) unstable; urgency=low + + * New upstream release. + * debian/compat: Changed to "9". + * debian/control (Build-Depends): Changed debhelper version to (>= 9). + (Standards-Version): Updated to "3.9.4". + (DM-Upload-Allowed): Removed. + (mandos/Depends): Add "initscripts (>= 2.88dsf-13.3)" to be able to + use the "/run" directory (for mandos.pid). + * debian/copyright (Copyright): Update year. + * Fix "Mandos/gnutls fails to establish connection, "an algorithm that + is not enabled was negotiated"" fixed by upstream. (Closes: #702120) + + -- Teddy Hogeborn Thu, 24 Oct 2013 22:33:40 +0200 + +mandos (1.6.1-1) unstable; urgency=low + + * New upstream release. + * debian/control (mandos/Depends): No longer depends on + python-gnupginterface, but does + depend on gnupg (<< 2). + (Build-Depends): Depend on debhelper 8.9.7 for using "override-*-arch" + and "override-*-indep" targets in debian/rules. + * debian/mandos-client.README: Update Linux documentation link. + * debian/rules: Completely rewritten to use debhelper v7. + * initramfs-tools-hook: Bug fix: Make sure the right version of GnuPG is + copied into the initramfs image. Always assume that GPGME is used to + avoid searching for it since the path might not be /usr/lib. Thanks + to Félix Sipma for the initial bug report, + and also thanks to Dick Middleton for some more + debugging. (Closes: #721903) + * Fix "bashism in /bin/sh script" fixed by upstream. (Closes: #690639) + + -- Teddy Hogeborn Sun, 13 Oct 2013 19:03:23 +0200 + +mandos (1.6.0-1) unstable; urgency=low + + * New upstream release. + * debian/copyright (Copyright): Join the two lines to a single line. + * debian/mandos-client.README.Debian: Update to refer to the new + location of the example network hooks, and the new feature of using + all network interfaces. + * debian/mandos-client.docs (network-hooks.d): Removed. + * debian/mandos-client.examples (network-hooks.d): New. + * debian/rules (binary-common): Added "dh_installexamples". + (binary-common/dh_fixperms): Exclude new location of + "network-hooks.d". + + -- Teddy Hogeborn Mon, 18 Jun 2012 00:15:23 +0200 + +mandos (1.5.5-1) unstable; urgency=low + + * New upstream release. + * debian/copyright (Format): Updated to + "http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/". + * debian/control (Build-Depends): Removed "man, locales-all". + + -- Teddy Hogeborn Fri, 01 Jun 2012 20:30:41 +0200 + +mandos (1.5.4-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 20 May 2012 15:38:34 +0200 + +mandos (1.5.3-1.2) unstable; urgency=low + + * Non-maintainer upload. + * Set Architecture to linux-any. (Closes: #647670) + + -- Robert Millan Sun, 22 Apr 2012 16:22:01 +0200 + +mandos (1.5.3-1.1) unstable; urgency=low + + * Non-maintainer upload. + * Fix "mandos FTBFS on buildds": add build-dependency on locales-all and + pass LC_ALL to dh_auto_build to make sure we have and use the en_US.UTF-8 + locale for manpage creation. + (Closes: #656178) + + -- gregor herrmann Tue, 31 Jan 2012 17:56:05 +0100 + +mandos (1.5.3-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 15 Jan 2012 22:05:54 +0100 + +mandos (1.5.2-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 08 Jan 2012 11:17:20 +0100 + +mandos (1.5.1-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 01 Jan 2012 21:53:31 +0100 + +mandos (1.5.0-1) unstable; urgency=low + + * New upstream release. + * debian/control (mandos-client/Depends): Added "initramfs-tools". + * debian/mandos-client.README.Debian: Corrected mail address and adjust + wording. + * debian/rules (binary-common): Exclude new nework-hooks.d directory + from dh_fixperms. + * debian/mandos-client.README.Debian: Document network hook facility. + * debian/mandos-client.docs (network-hooks.d): Added. + * debian/mandos.dirs (var/lib/mandos): Added. + * debian/mandos.postinst: Fix ownership of /var/lib/mandos. + * debian/control (mandos/Depends): Added "python-gnupginterface". + + -- Teddy Hogeborn Sun, 01 Jan 2012 05:58:11 +0100 + +mandos (1.4.1-1) unstable; urgency=low + + * New upstream release. + * debian/control (Build-Depends): Added "man". + * debian/control (Conflicts): Changed to "Breaks:". + * debian/copyright: Updated format. + * debian/mandos-client.postinst: Use "set -e" instead of "#!/bin/sh -e". + * debian/mandos-client.postrm: - '' - + * debian/mandos.postinst: - '' - + * debian/mandos.prerm: Consistent magic. + + -- Björn Påhlsson Sat, 15 Oct 2011 18:18:52 +0200 + +mandos (1.4.0-1) unstable; urgency=low + + * New upstream release. + * Fix "FTBFS with binutils-gold": Added "-Xlinker --as-needed" to + LDFLAGS in Makefile. (Closes: #632145) + * Fix "/run transition: uses obsolete /dev/.initramfs": Try both old and + new PID file locations. (Closes: #643554) + * debian/source/local-options: New; contains "--single-debian-patch". + * debian/control (Standards-Version): Upgraded to "3.9.2". + (DM-Upload-Allowed): New; set to "yes". + * debian/control: Changed domain from "fukt.bsnet.se" to "recompile.se". + * debian/copyright: - '' - + * debian/mandos-client.README.Debian: - '' - + * debian/mandos.README.Debian: - '' - + * debian/watch: - '' - + * debian/control (mandos/Description): Fix language to placate lintian. + + -- Teddy Hogeborn Sun, 09 Oct 2011 19:15:08 +0200 + +mandos (1.3.1-1) unstable; urgency=low + + * New upstream release. + * Conflict with correct version of dropbear. + * New version uses argparse; depend on python (<=2.7) | python-argparse. + + -- Teddy Hogeborn Wed, 27 Jul 2011 19:47:17 +0200 + +mandos (1.3.0-1) unstable; urgency=low + + * New upstream release. + * debian/control (mandos): Depend on Python 2.6, remove dependency on + python-multiprocessing. + (mandos-client): Conflict with dropbear (<< 0.52-5). + * debian/mandos-client.postrm (purge): Bug fix: update initramfs also on + purge. + * debian/mandos-client.lintian-overrides: Added plugins.d/plymouth. + + -- Teddy Hogeborn Tue, 08 Mar 2011 20:22:57 +0100 + +mandos (1.2.3-1) experimental; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Mon, 11 Oct 2010 19:37:31 +0200 + +mandos (1.2.2-1) experimental; urgency=low + + * New upstream release. + * plugins.d/splashy.c: Only use ELIBBAD if defined. (Closes: #599256) + + -- Teddy Hogeborn Thu, 07 Oct 2010 20:27:54 +0200 + +mandos (1.2.1-3) experimental; urgency=low + + * debian/changelog: Include entry for NMU of version 1.0.14-1.1. + + -- Teddy Hogeborn Tue, 05 Oct 2010 20:58:38 +0200 + +mandos (1.2.1-2) unstable; urgency=low + + * debian/source/format: New; contains "3.0 (quilt)". Really. + + -- Björn Påhlsson Sat, 02 Oct 2010 19:46:59 +0200 + +mandos (1.2.1-1) unstable; urgency=low + + * New upstream release. + * debian/source/format: New; contains "3.0 (quilt)". + + -- Björn Påhlsson Sat, 02 Oct 2010 19:03:58 +0200 + +mandos (1.2-1) unstable; urgency=low + + * New upstream release. + * Makefile (LINK_FORTIFY_LD): Remove "-fPIE". (Closes: #557076) + * debian/control: Add gnupg dependency to "mandos-client" and removed it + from "mandos". Added dependency on "python-urwid" "mandos" since the + new "mandos-monitor" utility needs it, and on "python (>=2.6) | + python-multiprocessing" since the Mandos server now uses it. + * debian/rules: Set BROKEN_PIE on mips and mipsel if a known buggy + version of binutils is used. + * debian/mandos.docs: Also install "/usr/share/doc/mandos/DBUS-API". + * debian/mandos.dirs: Added "etc/dbus-1/system.d". + * debian/mandos-client.README.Debian: Update info about DEVICE setting + of initramfs.conf. + * debian/mandos-client.README.Debian: Remove warning about --connect not + looping, since it now does. + + -- Teddy Hogeborn Tue, 28 Sep 2010 20:46:11 +0200 + +mandos (1.0.14-1.1) unstable; urgency=low + + * Non-maintainer upload. + * Rebuild against libavahi-core-dev (>= 0.6.26-1). + + -- Michael Biebl Mon, 12 Jul 2010 16:34:34 +0200 + +mandos (1.0.14-1) unstable; urgency=low (HIGH on mips and mipsel) + + * New upstream release. + * debian/rules: Build with BROKEN_PIE set on mips and mipsel + architectures - fixes FTBFS there. + + -- Teddy Hogeborn Sun, 25 Oct 2009 20:10:09 +0100 + +mandos (1.0.13-1) unstable; urgency=high + + * New upstream release. + * Do not copy unnecessary files to initrd (Closes: #551907) + + -- Teddy Hogeborn Thu, 22 Oct 2009 00:53:21 +0200 + +mandos (1.0.12-1) unstable; urgency=low + + * New upstream release. + * init.d-mandos: Correct dependencies (Closes: #546928) + * debian/control (Standards-Version): Changed to "3.8.3". + * debian/mandos-client.README.Debian: Improved wording and formatting. + Updated location of nfsroot.txt. + * debian/mandos.README.Debian: Improved wording and formatting. + * debian/mandos-client.postinst (configure): Don't look for user and + group with the old name if upgrading from a new enough version. + * debian/mandos.postinst (configure): - '' - + * debian/mandos-client.README.Debian: Added text about non-usability of + pseudo-network interfaces. + + -- Teddy Hogeborn Thu, 17 Sep 2009 15:03:59 +0200 + +mandos (1.0.11-1) unstable; urgency=low + + * debian/control (Standards-Version): Changed to "3.8.1". + * Makefile (GNUTLS_CFLAGS, GNUTLS_CFLAGS): Use "pkg-config" instead of + the old "libgnutls-config" script. (Closes: #529836) + + -- Teddy Hogeborn Sat, 23 May 2009 07:12:20 +0200 + +mandos (1.0.10-1) unstable; urgency=low + + * New upstream release. + * debian/mandos-client.postinst (update_initramfs): Fix permissions of + old initrd.img-*.bak files. + + -- Teddy Hogeborn Sun, 17 May 2009 04:56:35 +0200 + +mandos (1.0.9-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 17 May 2009 02:59:45 +0200 + +mandos (1.0.8-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Wed, 25 Feb 2009 02:26:57 +0100 + +mandos (1.0.7-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Tue, 24 Feb 2009 12:58:06 +0100 + +mandos (1.0.6-1) unstable; urgency=low + + * New upstream release. + * debian/mandos-client.postinst: Converted to Bourne shell. Also + minor message change. + * debian/mandos-client.postrm: Minor message change. + * debian/mandos.postinst: Converted to Bourne shell. Also minor + message change. + * debian/mandos.prerm: Minor message change. + * debian/rules (install-indep): Removed "--no-start" from + dh_installinit. + * debian/mandos-client.lintian-overrides: Remove obsolete override for + unbreakable line in plugin-runner manual page. + * debian/control (mandos/Depends): Added "python-gobject". + * debian/mandos-client.dirs: Change + "usr/share/initramfs-tools/scripts/local-top" to + "usr/share/initramfs-tools/scripts/init-premount". + * debian/mandos-client.README.Debian: Add reference to initramfs.conf + and nfsroot.txt. New section about the new non-local connection + feature. + + -- Teddy Hogeborn Fri, 13 Feb 2009 09:27:25 +0100 + +mandos (1.0.5-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sat, 17 Jan 2009 02:26:00 +0100 + +mandos (1.0.4-1) unstable; urgency=low + + * New upstream release. + * debian/watch: New file. + * debian/mandos-client.README.Debian: Document new "mandos=off" kernel + parameter. + + -- Teddy Hogeborn Thu, 15 Jan 2009 05:49:22 +0100 + +mandos (1.0.3-2) unstable; urgency=low + + * Removed some now-unused debconf files. + * Changed postinst scripts to not source debconf/confmodule. + * Removed po-debconf from build-depends. + + -- Teddy Hogeborn Tue, 06 Jan 2009 21:28:20 +0100 + +mandos (1.0.3-1) unstable; urgency=low + + * New upstream release. + * Add -Xlinker to linker flags to fix FTBFS for some architectures. + Thanks to Thiemo Seufer for the report and + fix. (Closes: #509398) + * Remove debconf use altogether, thereby stopping debconf abuse. Thanks + to Christian Perrier . (Closes: #509653) + * Add NEWS file to /usr/share/doc directories. + * Use and create "_mandos" user+group. Rename old user+group created by + older versions of this package. + * Fix manual pages by adding build-depend on "docbook-xml". + + -- Teddy Hogeborn Tue, 06 Jan 2009 01:21:20 +0100 + +mandos (1.0.2-1) unstable; urgency=low + + * New upstream release. + * debian/copyright: Rewritten to conform to + . + + -- Teddy Hogeborn Fri, 17 Oct 2008 20:42:12 +0200 + +mandos (1.0.1-1) unstable; urgency=low + + * New upstream release. + * Separate /usr/share/doc/mandos-client/README.Debian into sections with + headlines. Add instructions on how to test the server and verify the + password. + + -- Teddy Hogeborn Tue, 07 Oct 2008 23:07:23 +0200 + +mandos (1.0-2) unstable; urgency=low + + * Added comments in debian/*.lintian-overrides files. Added Debian + revison number to version number. + + -- Teddy Hogeborn Wed, 01 Oct 2008 17:23:35 +0200 + +mandos (1.0-1) unstable; urgency=low + + * Initial Release. (Closes: #500727). + + -- Teddy Hogeborn Tue, 30 Sep 2008 21:58:43 +0200 === added file 'debian/compat' --- debian/compat 1970-01-01 00:00:00 +0000 +++ debian/compat 2018-02-06 20:03:50 +0000 @@ -0,0 +1,1 @@ +10 === added file 'debian/control' --- debian/control 1970-01-01 00:00:00 +0000 +++ debian/control 2019-10-20 03:39:15 +0000 @@ -0,0 +1,70 @@ +Source: mandos +Section: admin +Priority: optional +Maintainer: Mandos Maintainers +Uploaders: Teddy Hogeborn , + Björn Påhlsson +Build-Depends: debhelper (>= 10), docbook-xml, docbook-xsl, + libavahi-core-dev, libgpgme-dev | libgpgme11-dev, + libglib2.0-dev (>=2.40), libgnutls28-dev (>= 3.3.0), + libgnutls28-dev (>= 3.6.6) | libgnutls28-dev (<< 3.6.0), + xsltproc, pkg-config, libnl-route-3-dev, systemd +Build-Depends-Indep: python3 (>= 3), python3-dbus, python3-gi, + po-debconf +Standards-Version: 4.4.1 +Vcs-Bzr: https://ftp.recompile.se/pub/mandos/trunk +Vcs-Browser: https://bzr.recompile.se/loggerhead/mandos/trunk/files +Homepage: https://www.recompile.se/mandos +Rules-Requires-Root: binary-targets + +Package: mandos +Architecture: all +Depends: ${misc:Depends}, python3 (>= 3), libgnutls30 (>= 3.3.0), + libgnutls30 (>= 3.6.6) | libgnutls30 (<< 3.6.0), + python3-dbus, python3-gi, avahi-daemon, adduser, + python3-urwid, gnupg2 | gnupg, + systemd-sysv | lsb-base (>= 3.0-6), + debconf (>= 1.5.5) | debconf-2.0 +Recommends: ssh-client | fping +Suggests: libc6-dev | libc-dev, c-compiler +Description: server giving encrypted passwords to Mandos clients + This is the server part of the Mandos system, which allows + computers to have encrypted root file systems and at the + same time be capable of remote and/or unattended reboots. + . + The computers run a small client program in the initial RAM + disk environment which will communicate with a server over a + network. All network communication is encrypted using TLS. + The clients are identified by the server using a TLS public + key; each client has one unique to it. The server sends the + clients an encrypted password. The encrypted password is + decrypted by the clients using an OpenPGP key, and the + password is then used to unlock the root file system, + whereupon the computers can continue booting normally. + +Package: mandos-client +Architecture: linux-any +Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, + cryptsetup (<< 2:2.0.3-1) | cryptsetup-initramfs, + initramfs-tools (>= 0.99) | dracut (>= 044+241-3), + dpkg-dev (>=1.16.0), + gnutls-bin (>= 3.6.6) | libgnutls30 (<< 3.6.0), + debconf (>= 1.5.5) | debconf-2.0 +Recommends: ssh +Breaks: dropbear (<= 0.53.1-1) +Enhances: cryptsetup +Conflicts: dracut-config-generic +Description: do unattended reboots with an encrypted root file system + This is the client part of the Mandos system, which allows + computers to have encrypted root file systems and at the + same time be capable of remote and/or unattended reboots. + . + The computers run a small client program in the initial RAM + disk environment which will communicate with a server over a + network. All network communication is encrypted using TLS. + The clients are identified by the server using a TLS public + key; each client has one unique to it. The server sends the + clients an encrypted password. The encrypted password is + decrypted by the clients using an OpenPGP key, and the + password is then used to unlock the root file system, + whereupon the computers can continue booting normally. === added file 'debian/copyright' --- debian/copyright 1970-01-01 00:00:00 +0000 +++ debian/copyright 2019-02-10 04:20:26 +0000 @@ -0,0 +1,26 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: Mandos +Upstream-Contact: Mandos +Source: + +Files: * +Copyright: Copyright © 2008-2019 Teddy Hogeborn + Copyright © 2008-2019 Björn Påhlsson +License: GPL-3+ + This file is part of Mandos. + . + Mandos 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. + . + Mandos 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 Mandos. If not, see . + . + On Debian systems, the complete text of the GNU General Public + License can be found in "/usr/share/common-licenses/GPL-3". === added file 'debian/mandos-client.README.Debian' --- debian/mandos-client.README.Debian 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.README.Debian 2019-07-27 10:11:45 +0000 @@ -0,0 +1,119 @@ +This file documents the next steps to take after installation of the +Debian package, and also contain some notes specific to the Debian +packaging which are not also in the manual. + +* Adding a Client Password to the Server + + The server must be given a password to give back to the client on + boot time. This password must be a one which can be used to unlock + the root file system device. On the *client*, run this command: + + mandos-keygen --password + + It will prompt for a password and output a config file section. + This output should be copied to the Mandos server and added to the + file "/etc/mandos/clients.conf" there. + +* Testing that it Works (Without Rebooting) + + After the server has been started with this client's key added, it + is possible to verify that the correct password will be received by + this client by running the command, on the client: + + MANDOSPLUGINHELPERDIR=/usr/lib/$(dpkg-architecture \ + -qDEB_HOST_MULTIARCH)/mandos/plugin-helpers \ + /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH \ + )/mandos/plugins.d/mandos-client \ + --pubkey=/etc/keys/mandos/pubkey.txt \ + --seckey=/etc/keys/mandos/seckey.txt \ + --tls-privkey=/etc/keys/mandos/tls-privkey.pem \ + --tls-pubkey=/etc/keys/mandos/tls-pubkey.pem; echo + + This command should retrieve the password from the server, decrypt + it, and output it to standard output. There it can be verified to + be the correct password, before rebooting. + +* Emergency Escape + + If it ever should be necessary, the Mandos client can be temporarily + prevented from running at startup by passing the parameter + "mandos=off" to the kernel. + +* Specifying a Client Network Interface + + At boot time the network interfaces to use will by default be + automatically detected. If this should result in incorrect + interfaces, edit the DEVICE setting in the + "/etc/initramfs-tools/initramfs.conf" file. (The default setting is + empty, meaning it will autodetect the interfaces.) *If* the DEVICE + setting is changed, it will be necessary to update the initrd image + by running this command: + + (For initramfs-tools:) + update-initramfs -k all -u + + (For dracut:) + dpkg-reconfigure dracut + + The device can also be overridden at boot time on the Linux kernel + command line using the sixth colon-separated field of the "ip=" + option; for exact syntax, read the documentation in the file + "/usr/share/doc/linux-doc-*/Documentation/filesystems/nfs/nfsroot.txt", + available in the "linux-doc-*" package. + + Note that since the network interfaces are used in the initial RAM + disk environment, the network interfaces *must* exist at that stage. + Thus, an interface can *not* be a pseudo-interface such as "br0" or + "tun0"; instead, only real interfaces (such as "enp1s0" or "eth0") + can be used. This can be overcome by writing a "network hook" + program to create an interface (see mandos-client(8mandos)) and + placing it in "/etc/mandos/network-hooks.d", from where it will be + copied into the initial RAM disk. Example network hook scripts can + be found in "/usr/share/doc/mandos-client/examples/network-hooks.d". + +* User-Supplied Plugins + + Any plugins found in "/etc/mandos/plugins.d" will override and add + to the normal Mandos plugins. When adding or changing plugins, do + not forget to update the initital RAM disk image: + + (For initramfs-tools:) + update-initramfs -k all -u + + (For dracut:) + dpkg-reconfigure dracut + +* Do *NOT* Edit "/etc/crypttab" + + It is NOT necessary to edit "/etc/crypttab" to specify + "/usr/lib/mandos/plugin-runner" as a keyscript for the root file + system; if no keyscript is given for the root file system, the + Mandos client will be the new default way for getting a password for + the root file system when booting. + +* Non-local Connection (Not Using ZeroConf) + + If the "ip=" kernel command line option is used to specify a + complete IP address and device name, as noted above, it then becomes + possible to specify a specific IP address and port to connect to, + instead of using ZeroConf. The syntax for doing this is + "mandos=connect::" on the kernel command + line. + + For very advanced users, it is possible to specify simply + "mandos=connect" on the kernel command line to make the system only + set up the network (using the data in the "ip=" option) and not pass + any extra "--connect" options to mandos-client at boot. For this to + work, "--options-for=mandos-client:--connect=
:" needs + to be manually added to the file "/etc/mandos/plugin-runner.conf". + +* Diffie-Hellman Parameters + + On installation, a file with Diffie-Hellman parameters, + /etc/keys/mandos/dhparams.pem, will be generated and automatically + installed into the initital RAM disk image and also used by the + Mandos Client on boot. If different parameters are needed for + policy or other reasons, simply replace the existing dhparams.pem + file and update the initital RAM disk image. + + -- Teddy Hogeborn , Mon, 15 Jul 2019 16:47:02 +0200 === added file 'debian/mandos-client.dirs' --- debian/mandos-client.dirs 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.dirs 2019-08-18 04:14:31 +0000 @@ -0,0 +1,8 @@ +usr/share/man/man8 +usr/sbin +usr/share/initramfs-tools/hooks +usr/share/initramfs-tools/conf.d +usr/share/initramfs-tools/conf-hooks.d +usr/share/initramfs-tools/scripts/init-premount +usr/share/initramfs-tools/scripts/local-premount +usr/lib/sysusers.d === added file 'debian/mandos-client.docs' --- debian/mandos-client.docs 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.docs 2012-06-01 21:48:12 +0000 @@ -0,0 +1,3 @@ +NEWS +README +TODO === added file 'debian/mandos-client.examples' --- debian/mandos-client.examples 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.examples 2012-06-01 21:48:12 +0000 @@ -0,0 +1,1 @@ +network-hooks.d === added file 'debian/mandos-client.links' --- debian/mandos-client.links 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.links 2008-09-19 13:50:22 +0000 @@ -0,0 +1,1 @@ +usr/share/man/man8/plugin-runner.8mandos.gz usr/share/man/man5/plugin-runner.conf.5mandos.gz === added file 'debian/mandos-client.lintian-overrides' --- debian/mandos-client.lintian-overrides 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.lintian-overrides 2019-08-05 21:14:05 +0000 @@ -0,0 +1,44 @@ +# This directory contains secret client key files. +# +mandos-client binary: non-standard-dir-perm etc/keys/mandos/ 0700 != 0755 + +# The directory /usr/lib//mandos/plugins.d contains setuid +# binaries which are not meant to be run outside an initial RAM disk +# environment (except for test purposes). It would be insecure to +# allow anyone to run them. +# +mandos-client binary: non-standard-dir-perm usr/lib/*/mandos/plugins.d/ 0700 != 0755 +# Likewise for helper executables for plugins +mandos-client binary: non-standard-dir-perm usr/lib/*/mandos/plugin-helpers/ 0700 != 0755 + +# These binaries must be setuid root, since they need root powers, but +# are started by plugin-runner(8mandos), which runs all plugins as +# user/group "_mandos". These binaries are not run in a running +# system, but in an initial RAM disk environment. Here they are +# protected from non-root access by the directory permissions, above. +# +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/mandos-client 4755 root/root +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/askpass-fifo 4755 root/root +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/splashy 4755 root/root +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/usplash 4755 root/root +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/plymouth 4755 root/root + +# The directory /etc/mandos/plugins.d can be used by local system +# administrators to place plugins in, overriding and complementing +# /usr/lib//mandos/plugins.d, and must be likewise protected. +# +mandos-client binary: non-standard-dir-perm etc/mandos/plugins.d/ 0700 != 0755 +# Likewise for plugin-helpers directory +mandos-client binary: non-standard-dir-perm etc/mandos/plugin-helpers/ 0700 != 0755 + +# The debconf templates is only used for displaying information +# detected in the postinst, not for saving answers to questions, so we +# don't need a .config file. +mandos-client binary: no-debconf-config + +# The notice displayed from the postinst script really is critical +mandos-client binary: postinst-uses-db-input + +# These are very important to work around bugs or changes in the old +# versions, and there is no pressing need to remove them. +mandos-client binary: maintainer-script-supports-ancient-package-version * === added file 'debian/mandos-client.postinst' --- debian/mandos-client.postinst 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.postinst 2019-07-27 10:11:45 +0000 @@ -0,0 +1,204 @@ +#!/bin/sh +# This script can be called in the following ways: +# +# After the package was installed: +# configure +# +# +# If prerm fails during upgrade or fails on failed upgrade: +# abort-upgrade +# +# If prerm fails during deconfiguration of a package: +# abort-deconfigure in-favour +# removing +# +# If prerm fails during replacement due to conflict: +# abort-remove in-favour + +. /usr/share/debconf/confmodule + +set -e + +# Update the initial RAM file system image +update_initramfs() +{ + if command -v update-initramfs >/dev/null; then + update-initramfs -k all -u + elif command -v dracut >/dev/null; then + dracut_version="`dpkg-query --showformat='${Version}' --show dracut`" + if dpkg --compare-versions "$dracut_version" lt 043-1 \ + && bash -c '. /etc/dracut.conf; . /etc/dracut.conf.d/*; [ "$hostonly" != yes ]'; then + echo 'Dracut is not configured to use hostonly mode!' >&2 + return 1 + fi + # Logic taken from dracut.postinst + for kernel in /boot/vmlinu[xz]-*; do + kversion="${kernel#/boot/vmlinu[xz]-}" + # Dracut preserves old permissions of initramfs image + # files, so we adjust permissions before creating new + # initramfs image containing secret keys. + chmod go-r /boot/initrd.img-"$kversion" + if [ "$kversion" != "*" ]; then + /etc/kernel/postinst.d/dracut "$kversion" + fi + done + fi + + if dpkg --compare-versions "$2" lt-nl "1.0.10-1"; then + # Make old initrd.img files unreadable too, in case they were + # created with mandos-client 1.0.8 or older. + find /boot -maxdepth 1 -type f -name "initrd.img-*.bak" \ + -print0 | xargs --null --no-run-if-empty chmod o-r + fi +} + +# Add user and group +add_mandos_user(){ + # Rename old "mandos" user and group + if dpkg --compare-versions "$2" lt "1.0.3-1"; then + case "`getent passwd mandos`" in + *:Mandos\ password\ system,,,:/nonexistent:/bin/false) + usermod --login _mandos mandos + groupmod --new-name _mandos mandos + return + ;; + esac + fi + # Create new user and group + if ! getent passwd _mandos >/dev/null; then + adduser --system --force-badname --quiet --home /nonexistent \ + --no-create-home --group --disabled-password \ + --gecos "Mandos password system" _mandos + fi +} + +# Create client key pairs +create_keys(){ + # If the OpenPGP key files do not exist, generate all keys using + # mandos-keygen + if ! [ -r /etc/keys/mandos/pubkey.txt \ + -a -r /etc/keys/mandos/seckey.txt ]; then + mandos-keygen + gpg-connect-agent KILLAGENT /bye || : + return 0 + fi + + # Remove any bad TLS keys by 1.8.0-1 + if dpkg --compare-versions "$2" eq "1.8.0-1" \ + || dpkg --compare-versions "$2" eq "1.8.0-1~bpo9+1"; then + # Is the key bad? + if ! certtool --password='' \ + --load-privkey=/etc/keys/mandos/tls-privkey.pem \ + --outfile=/dev/null --pubkey-info --no-text \ + 2>/dev/null; then + shred --remove -- /etc/keys/mandos/tls-privkey.pem \ + 2>/dev/null || : + rm --force -- /etc/keys/mandos/tls-pubkey.pem + fi + fi + + # If the TLS keys already exists, do nothing + if [ -r /etc/keys/mandos/tls-privkey.pem \ + -a -r /etc/keys/mandos/tls-pubkey.pem ]; then + return 0 + fi + + # Try to create the TLS keys + + TLS_PRIVKEYTMP="`mktemp -t mandos-client-privkey.XXXXXXXXXX`" + + if certtool --generate-privkey --password='' \ + --outfile "$TLS_PRIVKEYTMP" --sec-param ultra \ + --key-type=ed25519 --pkcs8 --no-text 2>/dev/null; then + + local umask=$(umask) + umask 077 + cp --archive "$TLS_PRIVKEYTMP" /etc/keys/mandos/tls-privkey.pem + shred --remove -- "$TLS_PRIVKEYTMP" 2>/dev/null || : + + # First try certtool from GnuTLS + if ! certtool --password='' \ + --load-privkey=/etc/keys/mandos/tls-privkey.pem \ + --outfile=/etc/keys/mandos/tls-pubkey.pem --pubkey-info \ + --no-text 2>/dev/null; then + # Otherwise try OpenSSL + if ! openssl pkey -in /etc/keys/mandos/tls-privkey.pem \ + -out /etc/keys/mandos/tls-pubkey.pem -pubout; then + rm --force /etc/keys/mandos/tls-pubkey.pem + # None of the commands succeded; give up + umask $umask + return 1 + fi + fi + umask $umask + + key_id=$(mandos-keygen --passfile=/dev/null \ + | grep --regexp="^key_id[ =]") + + db_version 2.0 + db_fset mandos-client/key_id seen false + db_reset mandos-client/key_id + db_subst mandos-client/key_id key_id $key_id + db_input critical mandos-client/key_id || true + db_go + db_stop + else + shred --remove -- "$TLS_PRIVKEYTMP" 2>/dev/null || : + fi +} + +create_dh_params(){ + if [ -r /etc/keys/mandos/dhparams.pem ]; then + return 0 + fi + # Create a Diffe-Hellman parameters file + DHFILE="`mktemp -t mandos-client-dh-parameters.XXXXXXXXXX.pem`" + # First try certtool from GnuTLS + if ! certtool --generate-dh-params --sec-param high \ + --outfile "$DHFILE"; then + # Otherwise try OpenSSL + if ! openssl genpkey -genparam -algorithm DH -out "$DHFILE" \ + -pkeyopt dh_paramgen_prime_len:3072; then + # None of the commands succeded; give up + rm -- "$DHFILE" + return 1 + fi + fi + sed --in-place --expression='0,/^-----BEGIN DH PARAMETERS-----$/d' \ + "$DHFILE" + sed --in-place --expression='1i-----BEGIN DH PARAMETERS-----' \ + "$DHFILE" + cp --archive "$DHFILE" /etc/keys/mandos/dhparams.pem + rm -- "$DHFILE" +} + +case "$1" in + configure) + add_mandos_user "$@" + create_keys "$@" + create_dh_params "$@" || : + update_initramfs "$@" + if dpkg --compare-versions "$2" lt-nl "1.7.10-1"; then + PLUGINHELPERDIR=/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)/mandos/plugin-helpers + if ! dpkg-statoverride --list "$PLUGINHELPERDIR" \ + >/dev/null 2>&1; then + chmod u=rwx,go= -- "$PLUGINHELPERDIR" + fi + if ! dpkg-statoverride --list /etc/mandos/plugin-helpers \ + >/dev/null 2>&1; then + chmod u=rwx,go= -- /etc/mandos/plugin-helpers + fi + fi + ;; + abort-upgrade|abort-deconfigure|abort-remove) + ;; + + *) + echo "$0 called with unknown argument '$1'" 1>&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 === added file 'debian/mandos-client.postrm' --- debian/mandos-client.postrm 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.postrm 2019-08-05 14:31:51 +0000 @@ -0,0 +1,73 @@ +#!/bin/sh +# This script can be called in the following ways: +# +# After the package was removed: +# remove +# +# After the package was purged: +# purge +# +# After the package was upgraded: +# upgrade +# if that fails: +# failed-upgrade +# +# +# After all of the packages files have been replaced: +# disappear +# +# +# If preinst fails during install: +# abort-install +# +# If preinst fails during upgrade of removed package: +# abort-install +# +# If preinst fails during upgrade: +# abort-upgrade + +set -e + +# Update the initial RAM file system image +update_initramfs() +{ + if command -v update-initramfs >/dev/null; then + update-initramfs -k all -u + elif command -v dracut >/dev/null; then + # Logic taken from dracut.postinst + for kernel in /boot/vmlinu[xz]-*; do + kversion="${kernel#/boot/vmlinu[xz]-}" + if [ "$kversion" != "*" ]; then + /etc/kernel/postinst.d/dracut "$kversion" + fi + done + fi +} + +case "$1" in + remove) + update_initramfs + ;; + + purge) + shred --remove /etc/keys/mandos/seckey.txt 2>/dev/null || : + rm --force /etc/mandos/plugin-runner.conf \ + /etc/keys/mandos/pubkey.txt \ + /etc/keys/mandos/seckey.txt \ + /etc/keys/mandos/tls-privkey.pem \ + /etc/keys/mandos/tls-pubkey.pem \ + /etc/keys/mandos/dhparams.pem 2>/dev/null + update_initramfs + ;; + upgrade|failed-upgrade|disappear|abort-install|abort-upgrade) + ;; + + *) + echo "$0 called with unknown argument '$1'" 1>&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 === added file 'debian/mandos-client.templates' --- debian/mandos-client.templates 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.templates 2019-08-05 21:00:35 +0000 @@ -0,0 +1,19 @@ +Template: mandos-client/key_id +Type: note +_description: New client option "${key_id}" is REQUIRED on server + A new "key_id" client option is REQUIRED in the server's clients.conf + file, otherwise this computer most likely will not reboot unattended. + This option: + . + ${key_id} + . + must be added (all on one line!) on the Mandos server host, in the file + /etc/mandos/clients.conf, right before the "fingerprint" option for this + Mandos client. You must edit that file on that server and add this + option. + . + With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as + TLS session keys. A new TLS key pair has been generated and will be used + as identification, but the key ID of the public key needs to be added to + the server, since this will now be used to identify the client to the + server. === added file 'debian/mandos.README.Debian' --- debian/mandos.README.Debian 1970-01-01 00:00:00 +0000 +++ debian/mandos.README.Debian 2011-10-05 16:00:56 +0000 @@ -0,0 +1,10 @@ +The Mandos server is useless without at least one configured client in +/etc/mandos/clients.conf. To create one, install the "mandos-client" +package on a client computer, and, on the client, run the command + + # mandos-keygen --password + +to get a config file stanza. Append the output of that command to the +file "/etc/mandos/clients.conf" on the Mandos server computer. + + -- Teddy Hogeborn , Wed, 5 Oct 2011 17:51:22 +0200 === added file 'debian/mandos.dirs' --- debian/mandos.dirs 1970-01-01 00:00:00 +0000 +++ debian/mandos.dirs 2019-08-18 00:23:21 +0000 @@ -0,0 +1,10 @@ +usr/share/man/man5 +usr/share/man/man8 +etc/init.d +etc/default +etc/dbus-1/system.d +usr/sbin +var/lib/mandos +lib/systemd/system +usr/lib/tmpfiles.d +usr/lib/sysusers.d === added file 'debian/mandos.docs' --- debian/mandos.docs 1970-01-01 00:00:00 +0000 +++ debian/mandos.docs 2010-09-12 03:00:40 +0000 @@ -0,0 +1,4 @@ +NEWS +README +TODO +DBUS-API === added file 'debian/mandos.lintian-overrides' --- debian/mandos.lintian-overrides 1970-01-01 00:00:00 +0000 +++ debian/mandos.lintian-overrides 2019-08-05 21:14:05 +0000 @@ -0,0 +1,16 @@ +# This config file will normally have encrypted secret client keys in +# it, so it must be kept unreadable for non-root users. +# +mandos binary: non-standard-file-perm etc/mandos/clients.conf 0600 != 0644 + +# The debconf templates is only used for displaying information +# detected in the postinst, not for saving answers to questions, so we +# don't need a .config file. +mandos binary: no-debconf-config + +# The notice displayed from the postinst script really is critical +mandos binary: postinst-uses-db-input + +# These are very important to work around bugs or changes in the old +# versions, and there is no pressing need to remove them. +mandos binary: maintainer-script-supports-ancient-package-version * === added file 'debian/mandos.postinst' --- debian/mandos.postinst 1970-01-01 00:00:00 +0000 +++ debian/mandos.postinst 2019-08-18 00:05:36 +0000 @@ -0,0 +1,118 @@ +#!/bin/sh +# This script can be called in the following ways: +# +# After the package was installed: +# configure +# +# +# If prerm fails during upgrade or fails on failed upgrade: +# abort-upgrade +# +# If prerm fails during deconfiguration of a package: +# abort-deconfigure in-favour +# removing +# +# If prerm fails during replacement due to conflict: +# abort-remove in-favour + +. /usr/share/debconf/confmodule + +set -e + +case "$1" in + configure) + # Rename old "mandos" user and group + if dpkg --compare-versions "$2" lt "1.0.3-1"; then + case "`getent passwd mandos`" in + *:Mandos\ password\ system,,,:/nonexistent:/bin/false) + usermod --login _mandos mandos + groupmod --new-name _mandos mandos + # Reload D-Bus daemon to be aware of the _mandos + # user & group + if [ -x /etc/init.d/dbus ]; then + invoke-rc.d dbus force-reload || : + fi + ;; + esac + fi + # Create new user and group + if ! getent passwd _mandos >/dev/null; then + adduser --system --force-badname --quiet \ + --home /nonexistent --no-create-home --group \ + --disabled-password --gecos "Mandos password system" \ + _mandos + # Reload D-Bus daemon to be aware of the _mandos user & + # group + if [ -x /etc/init.d/dbus ]; then + invoke-rc.d dbus force-reload || : + fi + elif dpkg --compare-versions "$2" eq 1.7.4-1 \ + || dpkg --compare-versions "$2" eq "1.7.4-1~bpo8+1" + then + start=no + if ! [ -f /var/lib/mandos/clients.pickle ]; then + invoke-rc.d mandos stop + start=yes + fi + chown _mandos:_mandos /var/lib/mandos/clients.pickle \ + 2>/dev/null || : + if [ "$start" = yes ]; then + invoke-rc.d mandos start + fi + fi + if ! dpkg-statoverride --list "/var/lib/mandos" >/dev/null \ + 2>&1; then + chown _mandos:_mandos /var/lib/mandos + chmod u=rwx,go= /var/lib/mandos + fi + + if dpkg --compare-versions "$2" eq "1.8.0-1" \ + || dpkg --compare-versions "$2" eq "1.8.0-1~bpo9+1"; then + if grep --quiet --regexp='^[[:space:]]*key_id[[:space:]]*=[[:space:]]*[Ee]3[Bb]0[Cc]44298[Ff][Cc]1[Cc]149[Aa][Ff][Bb][Ff]4[Cc]8996[Ff][Bb]92427[Aa][Ee]41[Ee]4649[Bb]934[Cc][Aa]495991[Bb]7852[Bb]855[[:space:]]*$' /etc/mandos/clients.conf; then + sed --in-place \ + --expression='/^[[:space:]]*key_id[[:space:]]*=[[:space:]]*[Ee]3[Bb]0[Cc]44298[Ff][Cc]1[Cc]149[Aa][Ff][Bb][Ff]4[Cc]8996[Ff][Bb]92427[Aa][Ee]41[Ee]4649[Bb]934[Cc][Aa]495991[Bb]7852[Bb]855[[:space:]]*$/d' \ + /etc/mandos/clients.conf + invoke-rc.d mandos restart + db_version 2.0 + db_fset mandos/removed_bad_key_ids seen false + db_reset mandos/removed_bad_key_ids + db_input critical mandos/removed_bad_key_ids || true + db_go + db_stop + fi + fi + + gnutls_version=$(dpkg-query --showformat='${Version}' \ + --show libgnutls30 \ + 2>/dev/null || :) + if [ -n "$gnutls_version" ] \ + && dpkg --compare-versions $gnutls_version ge 3.6.6; then + db_version 2.0 + db_input critical mandos/key_id || true + db_go + db_stop + fi + ;; + + abort-upgrade|abort-deconfigure|abort-remove) + ;; + + *) + echo "$0 called with unknown argument '$1'" 1>&2 + exit 1 + ;; +esac + +# Avahi version 0.6.31-2 and older provides "avahi" (instead of +# "avahi-daemon") in its /etc/init.d script header. To make +# insserv(8) happy, we edit our /etc/init.d script header to contain +# the correct string before the code added by dh_installinit calls +# update.rc-d, which calls insserv. +avahi_version="`dpkg-query --showformat='${Version}' --show avahi-daemon`" +if dpkg --compare-versions "$avahi_version" le 0.6.31-2; then + sed --in-place --expression='/^### BEGIN INIT INFO$/,/^### END INIT INFO$/s/^\(# Required-\(Stop\|Start\):.*avahi\)-daemon\>/\1/g' /etc/init.d/mandos +fi + +#DEBHELPER# + +exit 0 === added file 'debian/mandos.prerm' --- debian/mandos.prerm 1970-01-01 00:00:00 +0000 +++ debian/mandos.prerm 2015-07-12 01:57:54 +0000 @@ -0,0 +1,32 @@ +#!/bin/sh +# prerm script for mandos +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * 'remove' +# * 'upgrade' +# * 'failed-upgrade' +# * 'remove' 'in-favour' +# * 'deconfigure' 'in-favour' +# 'removing' +# +# for details, see /usr/share/doc/packaging-manual/ + +case "$1" in + remove|deconfigure) + invoke-rc.d mandos stop || : + ;; + upgrade|failed-upgrade) + ;; + *) + echo "prerm called with unknown argument '$1'" >&2 + exit 0 + ;; +esac + +#DEBHELPER# + +exit 0 === added file 'debian/mandos.templates' --- debian/mandos.templates 1970-01-01 00:00:00 +0000 +++ debian/mandos.templates 2019-08-05 21:00:35 +0000 @@ -0,0 +1,29 @@ +Template: mandos/key_id +Type: note +_Description: New client option "key_id" is REQUIRED on server + A new "key_id" client option is REQUIRED in the clients.conf file, + otherwise the client most likely will not reboot unattended. This option: + . + key_id = + . + must be added in the file /etc/mandos/clients.conf, right before the + "fingerprint" option, for each Mandos client. You must edit that file and + add this option for all clients. To see the correct key ID for each + client, run this command (on each client): + . + mandos-keygen -F/dev/null|grep ^key_id + . + Note: the clients must all also be using GnuTLS 3.6.6 or later; the server + cannot serve passwords for both old and new clients! + . + Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP + keys as TLS session keys. A new TLS key pair will be generated on each + client and will be used as identification, but the key ID of the public + key needs to be added to this server, since this will now be used to + identify the client to the server. + +Template: mandos/removed_bad_key_ids +Type: note +_Description: Bad key IDs have been removed from clients.conf + Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been + removed from /etc/mandos/clients.conf === added directory 'debian/po' === added file 'debian/po/POTFILES.in' --- debian/po/POTFILES.in 1970-01-01 00:00:00 +0000 +++ debian/po/POTFILES.in 2019-07-27 19:28:14 +0000 @@ -0,0 +1,2 @@ +[type: gettext/rfc822deb] mandos.templates +[type: gettext/rfc822deb] mandos-client.templates === added file 'debian/po/de.po' --- debian/po/de.po 1970-01-01 00:00:00 +0000 +++ debian/po/de.po 2019-08-16 19:28:16 +0000 @@ -0,0 +1,155 @@ +# German debconf translation of mandos. +# This file is distributed under the same license as the mandos package. +# Copyright (C) 2008-2019 Teddy Hogeborn and Björn Påhlsson +# Copyright (C) of this file 2019 Chris Leick . +# +msgid "" +msgstr "" +"Project-Id-Version: mandos 1.8.7-1\n" +"Report-Msgid-Bugs-To: mandos@packages.debian.org\n" +"POT-Creation-Date: 2019-08-05 22:57+0200\n" +"PO-Revision-Date: 2019-08-10 12:06+0100\n" +"Last-Translator: Chris Leick \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "New client option \"key_id\" is REQUIRED on server" +msgstr "Auf diesem Server ist die Client-Option »key_id« ERFORDERLICH" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the clients.conf file, " +"otherwise the client most likely will not reboot unattended. This option:" +msgstr "" +"In der Datei clients.conf ist eine neue Client-Option »key_id« ERFORDERLICH, " +"andernfalls werden die Clients höchstwahrscheinlich nicht unbeaufsichtigt neu " +"starten. Die Option" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " key_id = " +msgstr " key_id = " + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"must be added in the file /etc/mandos/clients.conf, right before the " +"\"fingerprint\" option, for each Mandos client. You must edit that file and " +"add this option for all clients. To see the correct key ID for each client, " +"run this command (on each client):" +msgstr "" +"muss der Datei /etc/mandos/clients.conf kurz vor der Option »fingerprint« " +"auf jedem Mandos-Client hinzugefügt werden. Sie müssen diese Datei bearbeiten " +"und diese Option auf allen Clients hinzufügen. Um die korrekte " +"Schlüsselkennung für jeden Client anzusehen, führen Sie (auf jedem Client) " +"diesen Befehl aus:" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " mandos-keygen -F/dev/null|grep ^key_id" +msgstr " mandos-keygen -F/dev/null|grep ^key_id" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server " +"cannot serve passwords for both old and new clients!" +msgstr "" +"Hinweis: Die Clients müssen außerdem alle GnuTLS 3.6.6 oder neuer nutzen; der " +"Server kann keine Passwörter für sowohl alte als auch neue Clients anbieten!" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP " +"keys as TLS session keys. A new TLS key pair will be generated on each " +"client and will be used as identification, but the key ID of the public key " +"needs to be added to this server, since this will now be used to identify " +"the client to the server." +msgstr "" +"Begründung: Mit GnuTLS 3.6.6 wurde erzwungen, dass Mandos die Benutzung von " +"OpenPGP als TLS-Sitzungsschlüssel stoppt. Auf jedem Client wird ein neues " +"TLS-Schlüsselpaar erzeugt und zur Identifizierung benutzt, aber der " +"öffentliche Schlüssel muss auf diesem Server hinzugefügt werden, da dies nun " +"zur Identifizierung des Clients auf dem Server verwendet wird." + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "Bad key IDs have been removed from clients.conf" +msgstr "Falsche Schlüsselkennungen wurden aus der clients.conf entfernt." + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "" +"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been " +"removed from /etc/mandos/clients.conf" +msgstr "" +"Falsche Schlüsselkennungen, die durch einen Fehler im Mandos-Client 1.8.0 " +"erzeugt wurden, wurden aus /etc/mandos/clients.conf entfernt." + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "New client option \"${key_id}\" is REQUIRED on server" +msgstr "Auf dem Server ist die neue Client-Option »${key_id}« ERFORDERLICH." + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the server's clients.conf " +"file, otherwise this computer most likely will not reboot unattended. This " +"option:" +msgstr "" +"In der Datei clients.conf des Servers ist eine neue Client-Option »key_id« " +"ERFORDERLICH, andernfalls wird dieser Rechner höchstwahrscheinlich nicht " +"unbeaufsichtigt neu starten. Die Option " + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid " ${key_id}" +msgstr " ${key_id}" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"must be added (all on one line!) on the Mandos server host, in the file /etc/" +"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos " +"client. You must edit that file on that server and add this option." +msgstr "" +"muss (in einer einzigen Zeile!) der Datei /etc/mandos/clients.conf auf dem " +"Mandos-Server kurz vor der Option »fingerprint« für diesen Mandos-Client " +"hinzugefügt werden. Sie müssen diese Datei auf diesem Server bearbeiten und " +"diese Option hinzufügen." + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS " +"session keys. A new TLS key pair has been generated and will be used as " +"identification, but the key ID of the public key needs to be added to the " +"server, since this will now be used to identify the client to the server." +msgstr "" +"Mit GnuTLS 3.6.6 wurde erzwungen, dass Mandos die Benutzung von OpenPGP als " +"TLS-Sitzungsschlüssel stoppt. Ein neues TLS-Schlüsselpaar wurde erzeugt und " +"wird zur Identifizierung benutzt, aber die Schlüsselkennung des öffentlichen " +"Schlüssels muss auf diesem Server hinzugefügt werden, da dies nun " +"zur Identifizierung des Clients auf dem Server verwendet wird." === added file 'debian/po/en_US.po' --- debian/po/en_US.po 1970-01-01 00:00:00 +0000 +++ debian/po/en_US.po 2019-08-05 21:00:35 +0000 @@ -0,0 +1,150 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the mandos package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: mandos\n" +"Report-Msgid-Bugs-To: mandos@packages.debian.org\n" +"POT-Creation-Date: 2019-08-05 22:57+0200\n" +"PO-Revision-Date: 2019-08-05 22:59+0200\n" +"Last-Translator: Teddy Hogeborn \n" +"Language-Team: English\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "New client option \"key_id\" is REQUIRED on server" +msgstr "New client option “key_id” is REQUIRED on server" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the clients.conf file, " +"otherwise the client most likely will not reboot unattended. This option:" +msgstr "" +"A new “key_id” client option is REQUIRED in the clients.conf file, otherwise " +"the client most likely will not reboot unattended. This option:" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " key_id = " +msgstr " key_id = " + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"must be added in the file /etc/mandos/clients.conf, right before the " +"\"fingerprint\" option, for each Mandos client. You must edit that file and " +"add this option for all clients. To see the correct key ID for each client, " +"run this command (on each client):" +msgstr "" +"must be added in the file /etc/mandos/clients.conf, right before the " +"“fingerprint” option, for each Mandos client. You must edit that file and " +"add this option for all clients. To see the correct key ID for each client, " +"run this command (on each client):" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " mandos-keygen -F/dev/null|grep ^key_id" +msgstr " mandos-keygen -F/dev/null|grep ^key id" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server " +"cannot serve passwords for both old and new clients!" +msgstr "" +"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server " +"cannot serve passwords for both old and new clients!" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP " +"keys as TLS session keys. A new TLS key pair will be generated on each " +"client and will be used as identification, but the key ID of the public key " +"needs to be added to this server, since this will now be used to identify " +"the client to the server." +msgstr "" +"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP " +"keys as TLS session keys. A new TLS key pair will be generated on each " +"client and will be used as identification, but the key ID of the public key " +"needs to be added to this server, since this will now be used to identify " +"the client to the server. " + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "Bad key IDs have been removed from clients.conf" +msgstr "Bad key IDs have been removed from clients.conf" + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "" +"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been " +"removed from /etc/mandos/clients.conf" +msgstr "" +"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been " +"removed from /etc/mandos/clients.conf" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "New client option \"${key_id}\" is REQUIRED on server" +msgstr "New client option “${key_id}” is REQUIRED on server" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the server's clients.conf " +"file, otherwise this computer most likely will not reboot unattended. This " +"option:" +msgstr "" +"A new “key_id” client option is REQUIRED in the server’s clients.conf file, " +"otherwise this computer most likely will not reboot unattended. This option:" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid " ${key_id}" +msgstr " ${key_id}" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"must be added (all on one line!) on the Mandos server host, in the file /etc/" +"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos " +"client. You must edit that file on that server and add this option." +msgstr "" +"must be added (all on one line!) on the Mandos server host, in the file /" +"etc/ mandos/clients.conf, right before the “fingerprint” option for this " +"Mandos client. You must edit that file on that server and add this option." + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS " +"session keys. A new TLS key pair has been generated and will be used as " +"identification, but the key ID of the public key needs to be added to the " +"server, since this will now be used to identify the client to the server." +msgstr "" +"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS " +"session keys. A new TLS key pair has been generated and will be used as " +"identification, but the key ID of the public key needs to be added to the " +"server, since this will now be used to identify the client to the server." === added file 'debian/po/fr.po' --- debian/po/fr.po 1970-01-01 00:00:00 +0000 +++ debian/po/fr.po 2019-08-16 19:32:47 +0000 @@ -0,0 +1,156 @@ +# Translation of mandos debconf templates to French +# Copyright (C) 2019, French l10n team +# This file is distributed under the same license as the mandos package. +# Grégoire Scano , 2019. +msgid "" +msgstr "" +"Project-Id-Version: mandos\n" +"Report-Msgid-Bugs-To: mandos@packages.debian.org\n" +"POT-Creation-Date: 2019-07-27 21:06+0200\n" +"PO-Revision-Date: 2019-08-11 15:58+0800\n" +"Last-Translator: Grégoire Scano \n" +"Language-Team: French \n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "New client option \"key_id\" is REQUIRED on server" +msgstr "La nouvelle option de client « key_id » est NÉCESSAIRE sur le serveur" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the clients.conf file, " +"otherwise the client most likely will not reboot unattended. This option:" +msgstr "" +"Une nouvelle option de client « key_id » est NÉCESSAIRE dans le fichier " +"clients.conf, autrement le client ne redémarrera probablement pas de lui-" +"même. Cette option :" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "key_id = " +msgstr "key_id = " + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"must be added in the file /etc/mandos/clients.conf, right before the " +"\"fingerprint\" option, for each Mandos client. You must edit that file and " +"add this option for all clients. To see the correct key ID for each client, " +"run this command (on each client):" +msgstr "" +"doit être ajoutée dans le fichier /etc/mandos/clients.conf, juste avant " +"l'option « fingerprint », pour chaque client Mandos. Vous devez éditer ce " +"fichier et ajouter cette option pour tous les clients. Pour voir " +"l'identifiant de clef correct pour chaque client, exécutez la commande (sur " +"chaque client) :" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "mandos-keygen -F/dev/null|grep ^key_id" +msgstr "mandos-keygen -F/dev/null|grep ^key_id" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server " +"cannot serve passwords for both old and new clients!" +msgstr "" +"Note : les clients doivent également tous utiliser GnuTLS 3.6.6 ou " +"ultérieur ; le serveur ne peut pas servir des mots de passe pour des clients " +"anciens et récents en même temps !" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP " +"keys as TLS session keys. A new TLS key pair will be generated on each " +"client and will be used as identification, but the key ID of the public key " +"needs to be added to this server, since this will now be used to identify " +"the client to the server." +msgstr "" +"Explication : avec GnuTLS 3.6.6, Mandos a été contraint d'arrêter d'utiliser " +"des clefs OpenPGP comme clefs de session TLS. Une nouvelle paire de clefs " +"TLS sera générée pour chaque client et sera utilisée pour l'identification, " +"mais l'identifiant de la clef publique doit être ajouté à ce serveur, " +"puisqu'il sera utilisé pour identifier le client auprès du serveur." + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "Bad key IDs have been removed from clients.conf" +msgstr "Les identifiants de clef incorrects ont été supprimés de clients.conf" + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "" +"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been " +"removed from /etc/mandos/clients.conf" +msgstr "" +"Les identifiants de clef incorrects, créés par un bogue dans le client " +"Mandos 1.8.0, ont été supprimés de /etc/mandos/clients.conf" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "New client option \"${key_id}\" is REQUIRED on server" +msgstr "" +"La nouvelle option de client « ${key_id} » est NÉCESSAIRE sur le serveur" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the server's clients.conf " +"file, otherwise this computer most likely will not reboot unattended. This " +"option:" +msgstr "" +"Une nouvelle option de client « key_id » est NÉCESSAIRE dans le fichier " +"clients.conf du serveur, autrement cette machine ne pourra pas redémarrer " +"d'elle-même. Cette option :" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "${key_id}" +msgstr "${key_id}" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"must be added (all on one line!) on the Mandos server host, in the file /etc/" +"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos " +"client. You must edit that file on that server and add this option." +msgstr "" +"doit être ajoutée (tout sur une seule ligne !) sur le serveur Mandos hôte, " +"dans le fichier /etc/mandos/clients.conf, juste avant l'option " +"« fingerprint » de ce client Mandos. Vous devez éditer ce fichier sur ce " +"serveur et ajouter cette option." + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS " +"session keys. A new TLS key pair has been generated and will be used as " +"identification, but the key ID of the public key needs to be added to the " +"server, since this will now be used to identify the client to the server." +msgstr "" +"Avec GnuTLS 3.6.6, Mandos a été contraint d'arrêter d'utiliser des clefs " +"OpenPGP comme clefs de session TLS. Une nouvelle paire de clefs TLS a été " +"générée et sera utilisée pour l'identification, mais l'identifiant de la " +"clef publique doit être ajouté au serveur, puisqu'il sera utilisé pour " +"identifier le client auprès du serveur." === added file 'debian/po/pt.po' --- debian/po/pt.po 1970-01-01 00:00:00 +0000 +++ debian/po/pt.po 2019-10-19 17:37:00 +0000 @@ -0,0 +1,158 @@ +# Translation of mandos debconf messages to European Portuguese +# Copyright (C) 2019 THE mandos'S COPYRIGHT HOLDER +# This file is distributed under the same license as the mandos package. +# +# Américo Monteiro , 2019. +msgid "" +msgstr "" +"Project-Id-Version: mandos 1.8.9-2\n" +"Report-Msgid-Bugs-To: mandos@packages.debian.org\n" +"POT-Creation-Date: 2019-08-05 22:57+0200\n" +"PO-Revision-Date: 2019-10-18 18:45+0000\n" +"Last-Translator: Américo Monteiro \n" +"Language-Team: Portuguese <>\n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 2.0\n" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "New client option \"key_id\" is REQUIRED on server" +msgstr "Nova opção \"key_id\" de cliente é NECESSÁRIA no servidor" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the clients.conf file, " +"otherwise the client most likely will not reboot unattended. This option:" +msgstr "" +"Uma nova opção de cliente \"key_id\" é NECESSÁRIA no ficheiro clients.conf, " +"caso contrário o mais provável é o cliente não conseguir reinicicar sozinho. " +"Esta opção:" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " key_id = " +msgstr " key_id = " + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"must be added in the file /etc/mandos/clients.conf, right before the " +"\"fingerprint\" option, for each Mandos client. You must edit that file and " +"add this option for all clients. To see the correct key ID for each client, " +"run this command (on each client):" +msgstr "" +"tem de ser adicionada ao ficheiro /etc/mandos/clients.conf, logo antes " +"da opção \"fingerprint\", para cada cliente Mandos. Você tem de editar esse " +"ficheiro e adicionar esta opção para todos os clientes. Para ver a key ID " +"para cada cliente, corra este comando (em cada cliente):" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " mandos-keygen -F/dev/null|grep ^key_id" +msgstr " mandos-keygen -F/dev/null|grep ^key_id" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server " +"cannot serve passwords for both old and new clients!" +msgstr "" +"Note: os clientes têm de também usar GnuTLS 3.6.6 ou posterior; o servidor " +"não consegue servir palavras passe para ambos clientes antigos e novos!" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP " +"keys as TLS session keys. A new TLS key pair will be generated on each " +"client and will be used as identification, but the key ID of the public key " +"needs to be added to this server, since this will now be used to identify " +"the client to the server." +msgstr "" +"Razão: Com GnuTLS 3.6.6, o Mandos foi forçado a parar de usar chaves OpenPGP " +"como chaves de sessão TLS. Será gerado um novo par de chaves TLS em cada " +"cliente e será usado como identificação, mas o ID de chave da chave pública " +"precisa de ser adicionada a este servidor, pois esta irá agora ser usada " +"para identificar o cliente no servidor." + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "Bad key IDs have been removed from clients.conf" +msgstr "IDs de chave errados foram removidos de clients.conf" + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "" +"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been " +"removed from /etc/mandos/clients.conf" +msgstr "" +"IDs de chave errados, que foram criados por um bug no cliente Mandos 1.8.0, " +"foram removidos de /etc/mandos/clients.conf" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "New client option \"${key_id}\" is REQUIRED on server" +msgstr "Nova opção \"${key_id}\" de cliente é NECESSÁRIA no servidor" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the server's clients.conf " +"file, otherwise this computer most likely will not reboot unattended. This " +"option:" +msgstr "" +"Uma nova opção \"key_id\" de cliente é NECESSÁRIA no ficheiro clients.conf " +"do servidor, caso contrário, é bem provável que este computador não consiga " +"reiniciar sozinho. Esta opção:" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid " ${key_id}" +msgstr " ${key_id}" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"must be added (all on one line!) on the Mandos server host, in the file /etc/" +"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos " +"client. You must edit that file on that server and add this option." +msgstr "" +"tem de ser adicionada (toda numa linha) na máquina servidor do Mandos, no " +"ficheiro /etc/mandos/clients.conf, logo antes da opção \"fingerprint\" para " +"este cliente Mandos. Você tem de editar esse ficheiro nesse servidor e " +"adicionar esta opção." + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS " +"session keys. A new TLS key pair has been generated and will be used as " +"identification, but the key ID of the public key needs to be added to the " +"server, since this will now be used to identify the client to the server." +msgstr "" +"Com GnuTLS 3.6.6, o Mandos foi forçado a parar de usar chaves OpenPGP " +"como chaves de sessão TLS. Foi gerado um novo par de chaves TLS e será " +"usado como identificação, mas o ID de chave da chave pública precisa de ser " +"adicionada ao servidor, pois esta irá agora ser usada para identificar o " +"cliente no servidor." + + === added file 'debian/po/sv.po' --- debian/po/sv.po 1970-01-01 00:00:00 +0000 +++ debian/po/sv.po 2019-08-16 20:47:52 +0000 @@ -0,0 +1,156 @@ +# Translation of mandos debconf templates to Swedish +# Copyright (C) 2019, Mandos Maintainers +# This file is distributed under the same license as the mandos package. +# Teddy Hogeborn , 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: mandos\n" +"Report-Msgid-Bugs-To: mandos@packages.debian.org\n" +"POT-Creation-Date: 2019-08-05 22:57+0200\n" +"PO-Revision-Date: 2019-08-16 22:45+0200\n" +"Last-Translator: Teddy Hogeborn \n" +"Language-Team: Swedish \n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "New client option \"key_id\" is REQUIRED on server" +msgstr "Ny klientinställning ”key_id” KRÄVS på servern" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the clients.conf file, " +"otherwise the client most likely will not reboot unattended. This option:" +msgstr "" +"En ny klientinställning, ”key_id”, KRÄVS i filen clients.conf, annars\n" +"kommer klienten antagligen inte att starta upp av sig själv. Denna\n" +"inställning:" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " key_id = " +msgstr " key_id = " + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"must be added in the file /etc/mandos/clients.conf, right before the " +"\"fingerprint\" option, for each Mandos client. You must edit that file and " +"add this option for all clients. To see the correct key ID for each client, " +"run this command (on each client):" +msgstr "" +"måste läggas till i filen /etc/mandos/clients.conf, precis ovanför\n" +"inställningen ”fingerprint”, för varje Mandosklient. Du måste ändra i\n" +"den filen och lägga till den inställningen för alla klienter. För att\n" +"se det korrekta nyckel-IDt för varje klient, kör följande kommando (på\n" +"varje klient):" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " mandos-keygen -F/dev/null|grep ^key_id" +msgstr " mandos-keygen -F/dev/null|grep ^key_id" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server " +"cannot serve passwords for both old and new clients!" +msgstr "" +"Observera: Alla klienter måste också använda GnuTLS 3.6.6 eller nyare;\n" +"servern kan inte ge lösenord till både nya och gamla klienter!" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP " +"keys as TLS session keys. A new TLS key pair will be generated on each " +"client and will be used as identification, but the key ID of the public key " +"needs to be added to this server, since this will now be used to identify " +"the client to the server." +msgstr "" +"Förklaring: Med GnuTLS 3.6.6 så har Mandos nödgats att sluta använda\n" +"OpenPGP-nycklar som TLS-sessionsnycklar. Ett nytt TLS-nyckelpar\n" +"kommer att genereras på varje klient och kommer att användas för\n" +"identifiering, men nyckel-IDt för den publika nyckeln måste läggas\n" +"till på denna server, då denna numera kommer att användas för att\n" +"identifiera klienten för servern." + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "Bad key IDs have been removed from clients.conf" +msgstr "Dåliga nyckel-IDn har tagits bort från clients.conf" + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "" +"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been " +"removed from /etc/mandos/clients.conf" +msgstr "" +"Dåliga nyckel-IDn, som skapats av en bugg i Mandosklienten 1.8.0, har\n" +"tagits bort från /etc/mandos/clients.conf" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "New client option \"${key_id}\" is REQUIRED on server" +msgstr "Ny klientinställning ”${key_id}” KRÄVS på servern" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the server's clients.conf " +"file, otherwise this computer most likely will not reboot unattended. This " +"option:" +msgstr "" +"En ny klientinställning, ”key_id”, KRÄVS i serverns clients.conf-fil,\n" +"annars kommer denna dator antagligen inte att starta upp av sig själv.\n" +"Denna inställning:" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid " ${key_id}" +msgstr " ${key_id}" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"must be added (all on one line!) on the Mandos server host, in the file /etc/" +"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos " +"client. You must edit that file on that server and add this option." +msgstr "" +"måste läggas till (allt på en rad!) på Mandosservervärddatorn, i filen\n" +"/etc/mandos/clients.conf, precis ovanför inställningen ”fingerprint”,\n" +"för denna Mandosklient. Du måste ändra i den filen och lägga till den\n" +"inställningen." + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS " +"session keys. A new TLS key pair has been generated and will be used as " +"identification, but the key ID of the public key needs to be added to the " +"server, since this will now be used to identify the client to the server." +msgstr "" +"Med GnuTLS 3.6.6 så har Mandos nödgats att sluta använda\n" +"OpenPGP-nycklar som TLS-sessionsnycklar. Ett nytt TLS-nyckelpar har\n" +"genererats och kommer att användas för identifiering, men nyckel-IDt\n" +"för den publika nyckeln måste läggas till på servern, då detta numera\n" +"kommer att användas för att identifiera klienten för servern." === added file 'debian/po/templates.pot' --- debian/po/templates.pot 1970-01-01 00:00:00 +0000 +++ debian/po/templates.pot 2019-08-05 21:00:35 +0000 @@ -0,0 +1,127 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the mandos package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: mandos\n" +"Report-Msgid-Bugs-To: mandos@packages.debian.org\n" +"POT-Creation-Date: 2019-08-05 22:57+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "New client option \"key_id\" is REQUIRED on server" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the clients.conf file, " +"otherwise the client most likely will not reboot unattended. This option:" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " key_id = " +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"must be added in the file /etc/mandos/clients.conf, right before the " +"\"fingerprint\" option, for each Mandos client. You must edit that file and " +"add this option for all clients. To see the correct key ID for each client, " +"run this command (on each client):" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid " mandos-keygen -F/dev/null|grep ^key_id" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server " +"cannot serve passwords for both old and new clients!" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:1001 +msgid "" +"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP " +"keys as TLS session keys. A new TLS key pair will be generated on each " +"client and will be used as identification, but the key ID of the public key " +"needs to be added to this server, since this will now be used to identify " +"the client to the server." +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "Bad key IDs have been removed from clients.conf" +msgstr "" + +#. Type: note +#. Description +#: ../mandos.templates:2001 +msgid "" +"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been " +"removed from /etc/mandos/clients.conf" +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "New client option \"${key_id}\" is REQUIRED on server" +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"A new \"key_id\" client option is REQUIRED in the server's clients.conf " +"file, otherwise this computer most likely will not reboot unattended. This " +"option:" +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid " ${key_id}" +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"must be added (all on one line!) on the Mandos server host, in the file /etc/" +"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos " +"client. You must edit that file on that server and add this option." +msgstr "" + +#. Type: note +#. description +#: ../mandos-client.templates:1001 +msgid "" +"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS " +"session keys. A new TLS key pair has been generated and will be used as " +"identification, but the key ID of the public key needs to be added to the " +"server, since this will now be used to identify the client to the server." +msgstr "" === added file 'debian/rules' --- debian/rules 1970-01-01 00:00:00 +0000 +++ debian/rules 2019-04-09 22:31:23 +0000 @@ -0,0 +1,61 @@ +#!/usr/bin/make -f + +ifeq (,$(filter noopt,$(DEB_BUILD_OPTIONS))) + MAKEFLAGS += OPTIMIZE=-O0 +endif + +ifneq (,$(filter parallel=%,$(DEB_BUILD_OPTIONS))) + NUMJOBS = $(patsubst parallel=%,%,$(filter parallel=%,$(DEB_BUILD_OPTIONS))) + MAKEFLAGS += -j$(NUMJOBS) +endif + +%: + dh $@ + +override_dh_auto_build-arch: + LC_ALL=en_US.utf8 dh_auto_build -- all doc + +override_dh_auto_build-indep: + LC_ALL=en_US.utf8 dh_auto_build -- doc + +override_dh_installinit-indep: + dh_installinit --onlyscripts \ + --update-rcd-params="defaults 25 15" + +override_dh_auto_install-indep: + $(MAKE) DESTDIR=$(CURDIR)/debian/mandos install-server + +override_dh_auto_install-arch: + $(MAKE) DESTDIR=$(CURDIR)/debian/mandos-client \ + install-client-nokey + +override_dh_fixperms-arch: + dh_fixperms --exclude etc/keys/mandos \ + --exclude etc/mandos/plugins.d \ + --exclude etc/mandos/plugin-helpers \ + --exclude usr/lib/$(DEB_HOST_MULTIARCH)/mandos/plugins.d \ + --exclude usr/lib/$(DEB_HOST_MULTIARCH)/mandos/plugin-helpers \ + --exclude usr/share/doc/mandos-client/examples/network-hooks.d + chmod --recursive g-w -- \ + "$(CURDIR)/debian/mandos-client/usr/share/doc/mandos-client/examples/network-hooks.d" + +override_dh_fixperms-indep: + dh_fixperms --exclude etc/mandos/clients.conf + +override_dh_auto_test-arch: ; + +#bpo## dpkg-shlibdeps sees the "libgnutls28-dev (>= 3.6.6) | +#bpo## libgnutls28-dev (<< 3.6.0)," in the build-dependencies not as two +#bpo## alternatives, but as an absolute dependency on libgnutls30 >= 3.6.6. +#bpo## So we have to do this ugly hack to hide this build dependency if we +#bpo## compiled with libgnutls30 << 3.6.0. +#bpo#override_dh_shlibdeps-arch: +#bpo# -gnutls_version=$$(dpkg-query --showformat='$${Version}' \ +#bpo# --show libgnutls30); \ +#bpo# dpkg --compare-versions $$gnutls_version lt 3.6.0 \ +#bpo# && { cp --archive debian/control debian/control.orig; sed --in-place --expression='s/libgnutls28-dev (>= 3\.6\.6) |//' debian/control; } +#bpo# dh_shlibdeps +#bpo# -gnutls_version=$$(dpkg-query --showformat='$${Version}' \ +#bpo# --show libgnutls30); \ +#bpo# dpkg --compare-versions $$gnutls_version lt 3.6.0 \ +#bpo# && mv debian/control.orig debian/control === added directory 'debian/source' === added file 'debian/source/format' --- debian/source/format 1970-01-01 00:00:00 +0000 +++ debian/source/format 2010-10-02 17:41:05 +0000 @@ -0,0 +1,1 @@ +3.0 (quilt) === added file 'debian/source/lintian-overrides' --- debian/source/lintian-overrides 1970-01-01 00:00:00 +0000 +++ debian/source/lintian-overrides 2019-08-05 21:03:31 +0000 @@ -0,0 +1,7 @@ +# We are both upstream and Debian maintainer for this package, so the +# .asc signature can not exist until after the orig.tar.gz has been +# built as part of the Debian package build. +mandos source: orig-tarball-missing-upstream-signature mandos_*.tar.gz + +# We want to backport to stretch for as long as reasonably practical +mandos source: package-uses-old-debhelper-compat-version 10 === added file 'debian/source/local-options' --- debian/source/local-options 1970-01-01 00:00:00 +0000 +++ debian/source/local-options 2011-10-08 21:13:46 +0000 @@ -0,0 +1,1 @@ +--single-debian-patch === added directory 'debian/tests' === added file 'debian/tests/control' --- debian/tests/control 1970-01-01 00:00:00 +0000 +++ debian/tests/control 2019-09-04 05:31:20 +0000 @@ -0,0 +1,33 @@ +Test-Command: /usr/sbin/mandos --check +Restrictions: superficial, allow-stderr +Features: test-name=mandos-check +Depends: mandos + +Test-Command: /usr/sbin/mandos-ctl --check --verbose +Restrictions: allow-stderr +Features: test-name=mandos-ctl +Depends: mandos + +Test-Command: /usr/sbin/mandos-keygen --version +Restrictions: superficial +Features: test-name=mandos-keygen-version +Depends: mandos-client + +Test-Command: /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)/mandos/plugin-runner --version +Restrictions: needs-root, superficial +Features: test-name=plugin-runner-version +Depends: mandos-client + +Test-Command: /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)/mandos/plugin-helpers/mandos-client-iprouteadddel --version +Restrictions: needs-root, superficial +Features: test-name=mandos-client-iprouteadddel-version +Depends: mandos-client + +Test-Command: /usr/lib/dracut/modules.d/90mandos/password-agent --test --verbose +Features: test-name=password-agent +Depends: mandos-client + +Test-Command: /usr/lib/dracut/modules.d/90mandos/password-agent --test --verbose -p /task-creators/start_mandos_client/suid +Restrictions: needs-root +Features: test-name=password-agent-suid +Depends: mandos-client === added directory 'debian/upstream' === added file 'debian/upstream/metadata' --- debian/upstream/metadata 1970-01-01 00:00:00 +0000 +++ debian/upstream/metadata 2019-08-04 12:39:39 +0000 @@ -0,0 +1,13 @@ +# -*- yaml -*- +--- +Bug-Submit: mailto:mandos-dev@recompile.se +Changelog: https://bzr.recompile.se/loggerhead/mandos/trunk/view/head:/NEWS +Contact: mandos@recompile.se +Documentation: https://www.recompile.se/mandos/man/intro.8mandos +FAQ: https://www.recompile.se/mandos/man/intro.8mandos#faq +Name: Mandos +Other-References: https://www.recompile.se/mandos +Registration: https://mail.recompile.se/cgi-bin/mailman/listinfo/mandos-dev +Repository: https://ftp.recompile.se/pub/mandos/trunk +Repository-Browse: https://bzr.recompile.se/loggerhead/mandos/trunk/files +Security-Contact: mandos@recompile.se === added file 'debian/upstream/signing-key.asc' --- debian/upstream/signing-key.asc 1970-01-01 00:00:00 +0000 +++ debian/upstream/signing-key.asc 2014-03-28 22:32:21 +0000 @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +mQINBFJQOFYBEACoWsEGlOxVWFUAxOxdd3GDLaqEKkKihJwLp102Ks7JKMd9friR +7+OZuo3U0gdqLU9q1jPJn36J1QbaUTOvcaKtZp+QpUoYJ2OaGtlOY5ML8LSoC0rZ +MIzGYTtvriwpU/YplLNGPl/90KsB2VqjrY1l1he5M8zziWDlPdJxwg8GFvmPWoif +6oo+1iCswL5IdQ6c5MVO53zYu0cgyUSazLsVD5Xzy59lefgtaDydahJpPycf5aEQ +DAoC9fZt2mgG3FLIUCZdXIhZdOJGCMdjLThBnJXYgGbG4rbGLNlI4W/uA5aqa4ME +WYSAcCyX3ucKY/LkXRtC+z5s05e7tZ3Z+uAJy1eDsbhDXgZERye7a/zPWx1tAlzQ +E80Oltjh1uXWjQORyx99a0jK87zjm49YjhYw1ZN6Z0HfSaws4Yj2QOzp9t4B3l7f +DIUYoWBfHW7mseQeQ+t3TwQU5gjFCNu7oDeATqi5A5MksXN0+BcksterbGRBhEyp +CybIEyrZE033jIs407Ool4Kv10cnjc8oy609BXex/dxwcvVr2vQHle4NPUZd+Xhg +zC+9Z4jFwE0M/EPvtyieA/DWQse+TZ5itDGMYDub/GJfv1U61ANOgPIbTEF7iSa9 +5nWmq7zyUy/txmABka842Kt0Vp6ayoKcF8EIXCaDrVfPnXj+JlKf3c2u6wARAQAB +tCxNYW5kb3MgTWFpbnRhaW5lciBUZWFtIDxtYW5kb3NAcmVjb21waWxlLnNlPokC +PQQTAQgAJwUCUlA4VgIbAwUJCWYBgAULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAK +CRByIylzyjTCxETXEACi56jCV9lJNSBbTp0Iet4X/i7Mx0Z8UkFFa3l7o0i4jFQj +CIBrWECDlcxqZziii2dgh7L0ma93vB3rfjfCWeYLcEQw43MFBsd4dHuobrLXTcqU +7n0Zmc8BsXwk5B25CnEYgvlbWX9BCYtxHGRcZzQrqOFjCMKatq0EIIVuWaz5yuCU +V2rEgnr+veTd/rBOE9ez6Ju6xH11Teob5G7pMM0YPKHtZG/J3rvWPw4BDM6Tc60B +G0sTDZNgkrGWxuB8YaLIwVWzliQK/17Jv/0alajyA3cWLWMkcK9Yhi/einzdMRoD +IjnbtoHcSC9g6i0VGelwnMpHlFTwXriXEBSttULarK3iKE4tOv9nxMAEwicqlw14 +X96PPgz6ogtJG7FiwZfy1CQ81Uby+YuIhHZx3ZEyR1TFq70e98EgCuvjMZWMIhSy +gB5Vssfq7c2lXQjltV3ujhK1PD+7/iHlL7t4QRxPDN8fbMS2VPfAdtnWS1K48d5J +D/jP1LrGWS81HIaX8GFVLVw+jSQEu9cn3TFiZxK/4MMsITmlouJdtZWmQ/otMSMl +wiCCZp3dGpRXMmaqR1N8V0nMKshM8mci7bD92ubd/t6cR/G6l+VIp45WyFONvtde +F3ccfmfrKJuroMDfHxPxMf58EroAuWwzJKCPRH4JmwDvSSQoIIAGNL1lOKp6wrkC +DQRSUDhWARAAzN7pbpAu7XLNPODotV/N+JaCFvNAIqTcr9PrbhxiKFCDs9/IExwP +sGENL9GZd1DfoGEgxQ8j3l8VGw9VSeUoN7uMY2NwwbXTilAFkn/S8xnr2zQDRZ+n +EeFSq9MMFxj4Kt1TqVYDbO8vmFfOT3gRCRHeJ+pn4yJeSPau/ndNrmbQ1/Z6vaUG +yfo931ottx7SXZwkHA6jJVFT9rbHTyx9tzOqMKDJiMrx8qKaHpE9B45oHNR0WJJJ +75zoDVuOZ6wAxXZuqBFu2lKPqDTZeawfzcu5qplrm1RPgSOjz6w1A41HBLqGe+v9 +7Twx5wfNMgnKC0V2wUe0xR6hQHQlyZoCwcGyrasiu+v/joZ1p66SSWKjs+LGjoe4 +Lwh6VjZU2x+irVBjcgIoRWf3k4JAef0nGYsm0cFjAnwXac+/CYxVt+7Y4+HG8wPB +oUNZkW+bvdHQouxEClxnccIEgX/AkriJTYDQ4q3tkl2HVE/51R4pdQVLer2a5ov+ +Jwk44DdqzYstMsvqu+iD48hXzADg6HFvofkpct15h463pMaJf99uVVM9ZDNQ6B34 +l3tPX9ZDkIDl6n9dE2Jkaxx1yNVhPXpeMf4EzL+CEdUErVStB66lUkw+tNkuZyVT +ZTQel9h0196+CNSiqAaL8+ZZdbjKKfzlcB4Qnd897XzMfsFQ7mzJ9QsAEQEAAYkC +JQQYAQgADwUCUlA4VgIbDAUJCWYBgAAKCRByIylzyjTCxFmlEACBTOg5NqX63d8D +mwk4smlFPppQBIduxZaMG9HsLcPi3VKTG9Zg6WI6rEdr/4MnoINsudLsEbrQLgRH +2q1Zs+HqIIP5H2/sYHmswyokYB10zKB6gNUUg/GSlcAcrelsHVKx5B8kccWGT5gk +Wo/X0BGMUTOvQ6lJ6YNo1idcQ2ZjsyfZoz3G8JS7/EXN//jAZf+017yj8WsAS7hw +JRFMy7VET4g00JcBoNOAMP7PkozimZ2OwwsggJSYWkR1RaU2tKR1VmDF8R6UxuEd +BJzwFmz+wNC1Kq+FoSaRNsrKEmzLnfV9unDnF2z7Lc4LqOysXdzOk9zTBPur0gd2 +Lh5H/g5rTAMQBARqXfvIwiTtrBGgil8JW8e4Bc0LQUuHAE7x9gMRil+OtkQrCRk9 +0LWXVS+K0tvvruE4EDtCGiS5046+BEI3aYsp4hNzjHADq0TJeCYjNg9kY0CjxcEq +cfuMoUbQ0MkARGuBbykCdlylfTrkxrj/dPhr49lctY3H+Pj6F4fMDM4TP6UTGA8k +993RRNYhkDWSxIp6G7RJpBZobHN+eHQ3r8A4tWdYb4Fvd2lvwEDjUFT9uD6WAff4 +8A1hM2uSy91UYBOPrIjqYdRFKJc9rThYdXH2T6SiRMYtZMrEKhqPffB/i9mqVBlD +6vKRsaQikZujRdP9Dkf0mLmJ7LANWw== +=9Noe +-----END PGP PUBLIC KEY BLOCK----- === added file 'debian/watch' --- debian/watch 1970-01-01 00:00:00 +0000 +++ debian/watch 2019-02-11 05:15:24 +0000 @@ -0,0 +1,3 @@ +version=4 +opts=pgpmode=auto \ + https://ftp.recompile.se/pub/@PACKAGE@/@PACKAGE@@ANY_VERSION@\.orig@ARCHIVE_EXT@ === added file 'default-mandos' --- default-mandos 1970-01-01 00:00:00 +0000 +++ default-mandos 2008-09-17 00:34:09 +0000 @@ -0,0 +1,7 @@ +# Directory where configuration files are located. Default is +# "/etc/mandos". +# +#CONFIGDIR=/etc/mandos + +# Additional options that are passed to the Daemon. +DAEMON_ARGS="" === added directory 'dracut-module' === added file 'dracut-module/ask-password-mandos.path' --- dracut-module/ask-password-mandos.path 1970-01-01 00:00:00 +0000 +++ dracut-module/ask-password-mandos.path 2019-07-27 10:11:45 +0000 @@ -0,0 +1,47 @@ +# -*- systemd -*- +# +# Copyright © 2019 Teddy Hogeborn +# Copyright © 2019 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos 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. +# +# Mandos 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 Mandos. If not, see . +# +# Contact the authors at . +# +# This systemd.path(5) unit will wait until there are any password +# questions present, represented by files named "ask.*" in the +# /run/systemd/ask-password directory, and then start the +# "ask-password-mandos.service" systemd.service(5) unit. + +# This file should be installed in the root file system as +# "/usr/lib/dracut/modules.d/90mandos/ask-password-mandos.path" and +# will be installed in the initramfs image file as +# "/lib/systemd/system/ask-password-mandos.path", and symlinked to +# "/lib/systemd/system//sysinit.target.wants/ask-password-mandos.path" +# by dracut when dracut creates the initramfs image file. + +[Unit] +Description=Forward Password Requests to remote Mandos server +Documentation=man:intro(8mandos) man:password-agent(8mandos) man:mandos-client(8mandos) +DefaultDependencies=no +Conflicts=shutdown.target +Before=basic.target shutdown.target +ConditionKernelCommandLine=!mandos=off +ConditionFileIsExecutable=/lib/mandos/password-agent +ConditionPathIsMountPoint=!/sysroot + +[Path] +PathExistsGlob=/run/systemd/ask-password/ask.* +MakeDirectory=yes === added file 'dracut-module/ask-password-mandos.service' --- dracut-module/ask-password-mandos.service 1970-01-01 00:00:00 +0000 +++ dracut-module/ask-password-mandos.service 2019-07-27 10:11:45 +0000 @@ -0,0 +1,51 @@ +# -*- systemd -*- +# +# Copyright © 2019 Teddy Hogeborn +# Copyright © 2019 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos 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. +# +# Mandos 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 Mandos. If not, see . +# +# Contact the authors at . +# +# This systemd.service(5) unit file will start the Mandos +# password-agent(8mandos) program, which will in turn run +# mandos-client(8mandos) to get a password and send the password to +# any and all active password questions using the systemd “Password +# Agent” mechanism. + +# This file should be installed in the root file system as +# "/usr/lib/dracut/modules.d/90mandos/ask-password-mandos.service" and +# will be installed in the initramfs image file as +# "/lib/systemd/system/ask-password-mandos.service" by dracut when +# dracut creates the initramfs image file. + +[Unit] +Description=Forward Password Requests to remote Mandos server +Documentation=man:intro(8mandos) man:password-agent(8mandos) man:mandos-client(8mandos) +DefaultDependencies=no +Conflicts=shutdown.target +Before=shutdown.target +ConditionKernelCommandLine=!mandos=off +ConditionFileIsExecutable=/lib/mandos/password-agent +ConditionFileIsExecutable=/lib/mandos/mandos-client +ConditionFileNotEmpty=/etc/mandos/keys/pubkey.txt +ConditionFileNotEmpty=/etc/mandos/keys/seckey.txt +ConditionFileNotEmpty=/etc/mandos/keys/tls-pubkey.pem +ConditionFileNotEmpty=/etc/mandos/keys/tls-privkey.pem +ConditionPathIsMountPoint=!/sysroot + +[Service] +ExecStart=/lib/mandos/password-agent -- /lib/mandos/mandos-client --pubkey=/etc/mandos/keys/pubkey.txt --seckey=/etc/mandos/keys/seckey.txt --tls-pubkey=/etc/mandos/keys/tls-pubkey.pem --tls-privkey=/etc/mandos/keys/tls-privkey.pem === added file 'dracut-module/cmdline-mandos.sh' --- dracut-module/cmdline-mandos.sh 1970-01-01 00:00:00 +0000 +++ dracut-module/cmdline-mandos.sh 2019-07-27 10:11:45 +0000 @@ -0,0 +1,74 @@ +#!/bin/sh +# +# This file should be present in the root file system directory +# /usr/lib/dracut/modules.d/90mandos. When dracut creates the +# initramfs image, dracut will run the "module-setup.sh" file in the +# same directory, which (when *not* using the "systemd" dracut module) +# will copy this file ("cmdline-mandos.sh") into the initramfs as +# "/lib/dracut/hooks/cmdline/20-cmdline-mandos.sh". +# +# Despite the above #!/bin/sh line and the executable flag, this file +# is not executed; this file is sourced by the /init script in the +# initramfs image created by dracut. + +if getargbool 1 mandos && [ -e /lib/dracut-crypt-lib.sh ]; then + cat >> /lib/dracut-crypt-lib.sh <<- "EOF" + ask_for_password(){ + local cmd; local prompt; local tries=3 + local ply_cmd; local ply_prompt; local ply_tries=3 + local tty_cmd; local tty_prompt; local tty_tries=3 + local ret + + while [ $# -gt 0 ]; do + case "$1" in + --cmd) ply_cmd="$2"; tty_cmd="$2"; shift;; + --ply-cmd) ply_cmd="$2"; shift;; + --tty-cmd) tty_cmd="$2"; shift;; + --prompt) ply_prompt="$2"; tty_prompt="$2"; shift;; + --ply-prompt) ply_prompt="$2"; shift;; + --tty-prompt) tty_prompt="$2"; shift;; + --tries) ply_tries="$2"; tty_tries="$2"; shift;; + --ply-tries) ply_tries="$2"; shift;; + --tty-tries) tty_tries="$2"; shift;; + --tty-echo-off) tty_echo_off=yes;; + -*) :;; + esac + shift + done + if [ -z "$ply_cmd" ]; then + ply_cmd="$tty_cmd" + fi + # Extract device and luksname from $ply_cmd + set -- $ply_cmd + shift + for arg in "$@"; do + case "$arg" in + -*) :;; + *) + if [ -z "$device" ]; then + device="$arg" + else + luksname="$arg" + break + fi + ;; + esac + done + { flock -s 9; + if [ -z "$ply_prompt" ]; then + if [ -z "$tty_prompt" ]; then + CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd + else + CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --options-for=password-prompt:--prompt="${tty_prompt}" --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd + fi + else + if [ -z "$tty_prompt" ]; then + CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --options-for=plymouth:--prompt="${ply_prompt}" --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd + else + CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --options-for=password-prompt:--prompt="${tty_prompt}" --options-for=plymouth:--prompt="${ply_prompt}" --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd + fi + fi + } 9>/.console_lock + } + EOF +fi === added file 'dracut-module/module-setup.sh' --- dracut-module/module-setup.sh 1970-01-01 00:00:00 +0000 +++ dracut-module/module-setup.sh 2019-07-27 10:11:45 +0000 @@ -0,0 +1,253 @@ +#!/bin/sh +# +# This file should be present in the root file system directory +# /usr/lib/dracut/modules.d/90mandos. When dracut creates the +# initramfs image, dracut will source this file and run the shell +# functions defined in this file: "install", "check", "depends", +# "cmdline", and "installkernel". +# +# Despite the above #!/bin/sh line and the executable flag, this file +# is not executed; this file is sourced by dracut when creating the +# initramfs image file. + +mandos_libdir(){ + for dir in /usr/lib \ + "/usr/lib/`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null`" \ + "`rpm --eval='%{_libdir}' 2>/dev/null`" /usr/local/lib; do + if [ -d "$dir"/mandos ]; then + echo "$dir"/mandos + return + fi + done + # Mandos not found + return 1 +} + +mandos_keydir(){ + for dir in /etc/keys/mandos /etc/mandos/keys; do + if [ -d "$dir" ]; then + echo "$dir" + return + fi + done + # Mandos key directory not found + return 1 +} + +check(){ + if [ "${hostonly:-no}" = "no" ]; then + dwarning "Mandos: Dracut not in hostonly mode" + return 1 + fi + + local libdir=`mandos_libdir` + if [ -z "$libdir" ]; then + dwarning "Mandos lib directory not found" + return 1 + fi + + local keydir=`mandos_keydir` + if [ -z "$keydir" ]; then + dwarning "Mandos key directory not found" + return 1 + fi +} + +install(){ + chmod go+w,+t "$initdir"/tmp + local libdir=`mandos_libdir` + local keydir=`mandos_keydir` + set `{ getent passwd _mandos \ + || getent passwd nobody \ + || echo ::65534:65534:::; } \ + | cut --delimiter=: --fields=3,4 --only-delimited \ + --output-delimiter=" "` + local mandos_user="$1" + local mandos_group="$2" + inst "${libdir}" /lib/mandos + if dracut_module_included "systemd"; then + plugindir=/lib/mandos + inst "${libdir}/plugins.d/mandos-client" \ + "${plugindir}/mandos-client" + chmod u-s "${initdir}/${plugindir}/mandos-client" + inst "${moddir}/ask-password-mandos.service" \ + "${systemdsystemunitdir}/ask-password-mandos.service" + if [ ${mandos_user} != 65534 ]; then + sed --in-place \ + --expression="s,^ExecStart=/lib/mandos/password-agent ,&--user=${mandos_user} ," \ + "${initdir}/${systemdsystemunitdir}/ask-password-mandos.service" + fi + if [ ${mandos_group} != 65534 ]; then + sed --in-place \ + --expression="s,^ExecStart=/lib/mandos/password-agent ,&--group=${mandos_group} ," \ + "${initdir}/${systemdsystemunitdir}/ask-password-mandos.service" + fi + else + inst_hook cmdline 20 "$moddir"/cmdline-mandos.sh + plugindir=/lib/mandos/plugins.d + inst "${libdir}/plugin-runner" /lib/mandos/plugin-runner + inst /etc/mandos/plugin-runner.conf + sed --in-place \ + --expression='1i--options-for=mandos-client:--pubkey=/etc/mandos/keys/pubkey.txt,--seckey=/etc/mandos/keys/seckey.txt,--tls-pubkey=/etc/mandos/keys/tls-pubkey.pem,--tls-privkey=/etc/mandos/keys/tls-privkey.pem' \ + "${initdir}/etc/mandos/plugin-runner.conf" + if [ ${mandos_user} != 65534 ]; then + sed --in-place --expression="1i--userid=${mandos_user}" \ + "${initdir}/etc/mandos/plugin-runner.conf" + fi + if [ ${mandos_group} != 65534 ]; then + sed --in-place \ + --expression="1i--groupid=${mandos_group}" \ + "${initdir}/etc/mandos/plugin-runner.conf" + fi + inst "${libdir}/plugins.d" "$plugindir" + chown ${mandos_user}:${mandos_group} "${initdir}/${plugindir}" + # Copy the packaged plugins + for file in "$libdir"/plugins.d/*; do + base="`basename \"$file\"`" + # Is this plugin overridden? + if [ -e "/etc/mandos/plugins.d/$base" ]; then + continue + fi + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") dwarning "Mandos client plugin directory is empty." >&2 ;; + askpass-fifo) : ;; # Ignore packaged for dracut + *) inst "${file}" "${plugindir}/${base}" ;; + esac + done + # Copy any user-supplied plugins + for file in /etc/mandos/plugins.d/*; do + base="`basename \"$file\"`" + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) inst "$file" "${plugindir}/${base}" ;; + esac + done + # Copy any user-supplied plugin helpers + for file in /etc/mandos/plugin-helpers/*; do + base="`basename \"$file\"`" + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) inst "$file" "/lib/mandos/plugin-helpers/$base";; + esac + done + fi + # Copy network hooks + for hook in /etc/mandos/network-hooks.d/*; do + basename=`basename "$hook"` + case "$basename" in + "*") continue ;; + *[!A-Za-z0-9_.-]*) continue ;; + *) test -d "$hook" || inst "$hook" "/lib/mandos/network-hooks.d/$basename" ;; + esac + if [ -x "$hook" ]; then + # Copy any files needed by the network hook + MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=files \ + VERBOSITY=0 "$hook" files | while read file target; do + if [ ! -e "${file}" ]; then + dwarning "WARNING: file ${file} not found, requested by Mandos network hook '${basename}'" >&2 + fi + if [ -z "${target}" ]; then + inst "$file" + else + inst "$file" "$target" + fi + done + fi + done + # Copy the packaged plugin helpers + for file in "$libdir"/plugin-helpers/*; do + base="`basename \"$file\"`" + # Is this plugin overridden? + if [ -e "/etc/mandos/plugin-helpers/$base" ]; then + continue + fi + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) inst "$file" "/lib/mandos/plugin-helpers/$base";; + esac + done + local gpg=/usr/bin/gpg + if [ -e /usr/bin/gpgconf ]; then + inst /usr/bin/gpgconf + gpg="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg:[^:]*://p'`" + gpgagent="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg-agent:[^:]*://p'`" + # Newer versions of GnuPG 2 requires the gpg-agent binary + if [ -e "$gpgagent" ]; then + inst "$gpgagent" + fi + fi + inst "$gpg" + if dracut_module_included "systemd"; then + inst "${moddir}/password-agent" /lib/mandos/password-agent + inst "${moddir}/ask-password-mandos.path" \ + "${systemdsystemunitdir}/ask-password-mandos.path" + ln_r "${systemdsystemunitdir}/ask-password-mandos.path" \ + "${systemdsystemunitdir}/sysinit.target.wants/ask-password-mandos.path" + fi + # Key files + for file in "$keydir"/*; do + if [ -d "$file" ]; then + continue + fi + case "$file" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) + inst "$file" "/etc/mandos/keys/`basename \"$file\"`" + chown ${mandos_user}:${mandos_group} \ + "${initdir}/etc/mandos/keys/`basename \"$file\"`" + if [ `basename "$file"` = dhparams.pem ]; then + # Use Diffie-Hellman parameters file + if dracut_module_included "systemd"; then + sed --in-place \ + --expression='/^ExecStart/s/$/ --dh-params=\/etc\/mandos\/keys\/dhparams.pem/' \ + "${initdir}/${systemdsystemunitdir}/ask-password-mandos.service" + else + sed --in-place \ + --expression="1i--options-for=mandos-client:--dh-params=/etc/mandos/keys/dhparams.pem" \ + "${initdir}/etc/mandos/plugin-runner.conf" + fi + fi + ;; + esac + done +} + +installkernel(){ + instmods =drivers/net + hostonly='' instmods ipv6 + # Copy any kernel modules needed by network hooks + for hook in /etc/mandos/network-hooks.d/*; do + basename=`basename "$hook"` + case "$basename" in + "*") continue ;; + *[!A-Za-z0-9_.-]*) continue ;; + esac + if [ -x "$hook" ]; then + # Copy and load any modules needed by the network hook + MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=modules \ + VERBOSITY=0 "$hook" modules | while read module; do + if [ -z "${target}" ]; then + instmods "$module" + fi + done + fi + done +} + +depends(){ + echo crypt +} + +cmdline(){ + : +} === added file 'dracut-module/password-agent.c' --- dracut-module/password-agent.c 1970-01-01 00:00:00 +0000 +++ dracut-module/password-agent.c 2019-10-20 01:48:38 +0000 @@ -0,0 +1,8112 @@ +/* -*- mode: c; coding: utf-8; after-save-hook: (lambda () (let* ((find-build-directory (lambda (try-directory &optional base-directory) (let ((base-directory (or base-directory try-directory))) (cond ((equal try-directory "/") base-directory) ((file-readable-p (concat (file-name-as-directory try-directory) "Makefile")) try-directory) ((funcall find-build-directory (directory-file-name (file-name-directory try-directory)) base-directory)))))) (build-directory (funcall find-build-directory (buffer-file-name))) (local-build-directory (if (fboundp 'file-local-name) (file-local-name build-directory) (or (file-remote-p build-directory 'localname) build-directory))) (command (file-relative-name (file-name-sans-extension (buffer-file-name)) build-directory))) (pcase (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (let ((qbdir (shell-quote-argument local-build-directory)) (qcmd (shell-quote-argument command))) (format "cd %s && CFLAGS=-Werror make --silent %s && %s --test --verbose" qbdir qcmd qcmd)) nil "*Test*")) (0 (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w)))) (_ (with-current-buffer "*Test*" (compilation-mode) (cd-absolute build-directory)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); -*- */ +/* + * Mandos password agent - Simple password agent to run Mandos client + * + * Copyright © 2019 Teddy Hogeborn + * Copyright © 2019 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos 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. + * + * Mandos 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 Mandos. If not, see . + * + * Contact the authors at . + */ + +#define _GNU_SOURCE +#include /* uintmax_t, PRIuMAX, PRIdMAX, + intmax_t, uint32_t, SCNx32, + SCNuMAX, SCNxMAX */ +#include /* size_t */ +#include /* pid_t, uid_t, gid_t, getuid(), + getpid() */ +#include /* bool, true, false */ +#include /* struct sigaction, sigset_t, + sigemptyset(), sigaddset(), + SIGCHLD, pthread_sigmask(), + SIG_BLOCK, SIG_SETMASK, SA_RESTART, + SA_NOCLDSTOP, sigfillset(), kill(), + SIGTERM, sigdelset(), SIGKILL, + NSIG, sigismember(), SA_ONSTACK, + SIG_DFL, SIG_IGN, SIGINT, SIGQUIT, + SIGHUP, SIGSTOP, SIG_UNBLOCK */ +#include /* EXIT_SUCCESS, EXIT_FAILURE, + malloc(), free(), strtoumax(), + realloc(), setenv(), calloc(), + mkdtemp(), mkostemp() */ +#include /* not, or, and, xor */ +#include /* error() */ +#include /* EX_USAGE, EX_OSERR, EX_OSFILE */ +#include /* errno, error_t, EACCES, + ENAMETOOLONG, ENOENT, ENOTDIR, + EEXIST, ECHILD, EPERM, ENOMEM, + EAGAIN, EINTR, ENOBUFS, EADDRINUSE, + ECONNREFUSED, ECONNRESET, + ETOOMANYREFS, EMSGSIZE, EBADF, + EINVAL */ +#include /* strdup(), memcpy(), + explicit_bzero(), memset(), + strcmp(), strlen(), strncpy(), + memcmp(), basename() */ +#include /* argz_create(), argz_count(), + argz_extract(), argz_next(), + argz_add() */ +#include /* epoll_create1(), EPOLL_CLOEXEC, + epoll_ctl(), EPOLL_CTL_ADD, + struct epoll_event, EPOLLIN, + EPOLLRDHUP, EPOLLOUT, + epoll_pwait() */ +#include /* struct timespec, clock_gettime(), + CLOCK_MONOTONIC */ +#include /* struct argp_option, OPTION_HIDDEN, + OPTION_ALIAS, struct argp_state, + ARGP_ERR_UNKNOWN, ARGP_KEY_ARGS, + struct argp, argp_parse(), + ARGP_NO_EXIT */ +#include /* uid_t, gid_t, close(), pipe2(), + fork(), _exit(), dup2(), + STDOUT_FILENO, setresgid(), + setresuid(), execv(), ssize_t, + read(), dup3(), getuid(), dup(), + STDERR_FILENO, pause(), write(), + rmdir(), unlink(), getpid() */ +#include /* munlock(), mlock() */ +#include /* O_CLOEXEC, O_NONBLOCK, fcntl(), + F_GETFD, F_GETFL, FD_CLOEXEC, + open(), O_WRONLY, O_NOCTTY, + O_RDONLY, O_NOFOLLOW */ +#include /* waitpid(), WNOHANG, WIFEXITED(), + WEXITSTATUS() */ +#include /* PIPE_BUF, NAME_MAX, INT_MAX */ +#include /* inotify_init1(), IN_NONBLOCK, + IN_CLOEXEC, inotify_add_watch(), + IN_CLOSE_WRITE, IN_MOVED_TO, + IN_MOVED_FROM, IN_DELETE, + IN_EXCL_UNLINK, IN_ONLYDIR, + struct inotify_event */ +#include /* fnmatch(), FNM_FILE_NAME */ +#include /* asprintf(), FILE, fopen(), + getline(), sscanf(), feof(), + ferror(), fclose(), stderr, + rename(), fdopen(), fprintf(), + fscanf() */ +#include /* GKeyFile, g_key_file_free(), g_key_file_new(), + GError, g_key_file_load_from_file(), + G_KEY_FILE_NONE, TRUE, G_FILE_ERROR_NOENT, + g_key_file_get_string(), guint64, + g_key_file_get_uint64(), + G_KEY_FILE_ERROR_KEY_NOT_FOUND, gconstpointer, + g_assert_true(), g_assert_nonnull(), + g_assert_null(), g_assert_false(), + g_assert_cmpint(), g_assert_cmpuint(), + g_test_skip(), g_assert_cmpstr(), + g_test_init(), g_test_add(), g_test_run(), + GOptionContext, g_option_context_new(), + g_option_context_set_help_enabled(), FALSE, + g_option_context_set_ignore_unknown_options(), + gboolean, GOptionEntry, G_OPTION_ARG_NONE, + g_option_context_add_main_entries(), + g_option_context_parse(), + g_option_context_free(), g_error() */ +#include /* struct sockaddr_un, SUN_LEN */ +#include /* AF_LOCAL, socket(), PF_LOCAL, + SOCK_DGRAM, SOCK_NONBLOCK, + SOCK_CLOEXEC, connect(), + struct sockaddr, socklen_t, + shutdown(), SHUT_RD, send(), + MSG_NOSIGNAL, bind(), recv(), + socketpair() */ +#include /* globfree(), glob_t, glob(), + GLOB_ERR, GLOB_NOSORT, GLOB_MARK, + GLOB_ABORTED, GLOB_NOMATCH, + GLOB_NOSPACE */ + +/* End of includes */ + +/* Start of declarations of private types and functions */ + +/* microseconds of CLOCK_MONOTONIC absolute time; 0 means unset */ +typedef uintmax_t mono_microsecs; + +/* "task_queue" - A queue of tasks to be run */ +typedef struct { + struct task_struct *tasks; /* Tasks in this queue */ + size_t length; /* Number of tasks */ + /* Memory allocated for "tasks", in bytes */ + size_t allocated; + /* Time when this queue should be run, at the latest */ + mono_microsecs next_run; +} __attribute__((designated_init)) task_queue; + +/* "task_func" - A function type for task functions + + I.e. functions for the code which runs when a task is run, all have + this type */ +typedef void (task_func) (const struct task_struct, + task_queue *const) + __attribute__((nonnull)); + +/* "buffer" - A data buffer for a growing array of bytes + + Used for the "password" variable */ +typedef struct { + char *data; + size_t length; + size_t allocated; +} __attribute__((designated_init)) buffer; + +/* "string_set" - A set type which can contain strings + + Used by the "cancelled_filenames" variable */ +typedef struct { + char *argz; /* Do not access these except in */ + size_t argz_len; /* the string_set_* functions */ +} __attribute__((designated_init)) string_set; + +/* "task_context" - local variables for tasks + + This data structure distinguishes between different tasks which are + using the same function. This data structure is passed to every + task function when each task is run. + + Note that not every task uses every struct member. */ +typedef struct task_struct { + task_func *const func; /* The function run by this task */ + char *const question_filename; /* The question file */ + const pid_t pid; /* Mandos client process ID */ + const int epoll_fd; /* The epoll set file descriptor */ + bool *const quit_now; /* Set to true on fatal errors */ + const int fd; /* General purpose file descriptor */ + bool *const mandos_client_exited; /* Set true when client exits */ + buffer *const password; /* As read from client process */ + bool *const password_is_read; /* "password" is done growing */ + char *filename; /* General purpose file name */ + /* A set of strings of all the file names of questions which have + been cancelled for any reason; tasks pertaining to these question + files should not be run */ + string_set *const cancelled_filenames; + const mono_microsecs notafter; /* "NotAfter" from question file */ + /* Updated before each queue run; is compared with queue.next_run */ + const mono_microsecs *const current_time; +} __attribute__((designated_init)) task_context; + +/* Declare all our functions here so we can define them in any order + below. Note: test functions are *not* declared here, they are + declared in the test section. */ +__attribute__((warn_unused_result)) +static bool should_only_run_tests(int *, char **[]); +__attribute__((warn_unused_result, cold)) +static bool run_tests(int, char *[]); +static void handle_sigchld(__attribute__((unused)) int sig){} +__attribute__((warn_unused_result, malloc)) +task_queue *create_queue(void); +__attribute__((nonnull, warn_unused_result)) +bool add_to_queue(task_queue *const, const task_context); +__attribute__((nonnull)) +void cleanup_task(const task_context *const); +__attribute__((nonnull)) +void cleanup_queue(task_queue *const *const); +__attribute__((pure, nonnull, warn_unused_result)) +bool queue_has_question(const task_queue *const); +__attribute__((nonnull)) +void cleanup_close(const int *const); +__attribute__((nonnull)) +void cleanup_string(char *const *const); +__attribute__((nonnull)) +void cleanup_buffer(buffer *const); +__attribute__((pure, nonnull, warn_unused_result)) +bool string_set_contains(const string_set, const char *const); +__attribute__((nonnull, warn_unused_result)) +bool string_set_add(string_set *const, const char *const); +__attribute__((nonnull)) +void string_set_clear(string_set *); +void string_set_swap(string_set *const, string_set *const); +__attribute__((nonnull, warn_unused_result)) +bool start_mandos_client(task_queue *const, const int, bool *const, + bool *const, buffer *const, bool *const, + const struct sigaction *const, + const sigset_t, const char *const, + const uid_t, const gid_t, + const char *const *const); +__attribute__((nonnull)) +task_func wait_for_mandos_client_exit; +__attribute__((nonnull)) +task_func read_mandos_client_output; +__attribute__((warn_unused_result)) +bool add_inotify_dir_watch(task_queue *const, const int, bool *const, + buffer *const, const char *const, + string_set *, const mono_microsecs *const, + bool *const, bool *const); +__attribute__((nonnull)) +task_func read_inotify_event; +__attribute__((nonnull)) +task_func open_and_parse_question; +__attribute__((nonnull)) +task_func cancel_old_question; +__attribute__((nonnull)) +task_func connect_question_socket; +__attribute__((nonnull)) +task_func send_password_to_socket; +__attribute__((warn_unused_result)) +bool add_existing_questions(task_queue *const, const int, + buffer *const, string_set *, + const mono_microsecs *const, + bool *const, bool *const, + const char *const); +__attribute__((nonnull, warn_unused_result)) +bool wait_for_event(const int, const mono_microsecs, + const mono_microsecs); +bool run_queue(task_queue **const, string_set *const, bool *const); +bool clear_all_fds_from_epoll_set(const int); +mono_microsecs get_current_time(void); +__attribute__((nonnull, warn_unused_result)) +bool setup_signal_handler(struct sigaction *const); +__attribute__((nonnull)) +bool restore_signal_handler(const struct sigaction *const); +__attribute__((nonnull, warn_unused_result)) +bool block_sigchld(sigset_t *const); +__attribute__((nonnull)) +bool restore_sigmask(const sigset_t *const); +__attribute__((nonnull)) +bool parse_arguments(int, char *[], const bool, char **, char **, + uid_t *const , gid_t *const, char **, size_t *); + +/* End of declarations of private types and functions */ + +/* Start of "main" section; this section LACKS TESTS! + + Code here should be as simple as possible. */ + +/* These are required to be global by Argp */ +const char *argp_program_version = "password-agent " VERSION; +const char *argp_program_bug_address = ""; + +int main(int argc, char *argv[]){ + + /* If the --test option is passed, skip all normal operations and + instead only run the run_tests() function, which also does all + its own option parsing, so we don't have to do anything here. */ + if(should_only_run_tests(&argc, &argv)){ + if(run_tests(argc, argv)){ + return EXIT_SUCCESS; /* All tests successful */ + } + return EXIT_FAILURE; /* Some test(s) failed */ + } + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + + uid_t user = 0; + gid_t group = 0; + + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + if(not parse_arguments(argc, argv, true, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)){ + /* This should never happen, since "true" is passed as the third + argument to parse_arguments() above, which should make + argp_parse() call exit() if any parsing error occurs. */ + error(EX_USAGE, errno, "Failed to parse arguments"); + } + + const char default_agent_directory[] = "/run/systemd/ask-password"; + const char default_helper_directory[] + = "/lib/mandos/plugin-helpers"; + const char *const default_argv[] + = {"/lib/mandos/plugins.d/mandos-client", NULL }; + + /* Set variables to default values if unset */ + if(agent_directory == NULL){ + agent_directory = strdup(default_agent_directory); + if(agent_directory == NULL){ + error(EX_OSERR, errno, "Failed strdup()"); + } + } + if(helper_directory == NULL){ + helper_directory = strdup(default_helper_directory); + if(helper_directory == NULL){ + error(EX_OSERR, errno, "Failed strdup()"); + } + } + if(user == 0){ + user = 65534; /* nobody */ + } + if(group == 0){ + group = 65534; /* nogroup */ + } + /* If parse_opt did not create an argz vector, create one with + default values */ + if(mandos_argz == NULL){ +#ifdef __GNUC__ +#pragma GCC diagnostic push + /* argz_create() takes a non-const argv for some unknown reason - + argz_create() isn't modifying the strings, just copying them. + Therefore, this cast to non-const should be safe. */ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + errno = argz_create((char *const *)default_argv, &mandos_argz, + &mandos_argz_length); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + if(errno != 0){ + error(EX_OSERR, errno, "Failed argz_create()"); + } + } + /* Use argz vector to create a normal argv, usable by execv() */ + + char **mandos_argv = malloc((argz_count(mandos_argz, + mandos_argz_length) + + 1) * sizeof(char *)); + if(mandos_argv == NULL){ + error_t saved_errno = errno; + free(mandos_argz); + error(EX_OSERR, saved_errno, "Failed malloc()"); + } + argz_extract(mandos_argz, mandos_argz_length, mandos_argv); + + sigset_t orig_sigmask; + if(not block_sigchld(&orig_sigmask)){ + return EX_OSERR; + } + + struct sigaction old_sigchld_action; + if(not setup_signal_handler(&old_sigchld_action)){ + return EX_OSERR; + } + + mono_microsecs current_time = 0; + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + if(epoll_fd < 0){ + error(EX_OSERR, errno, "Failed to create epoll set fd"); + } + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + if(queue == NULL){ + error(EX_OSERR, errno, "Failed to create task queue"); + } + + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + + /* Add tasks to queue */ + if(not start_mandos_client(queue, epoll_fd, &mandos_client_exited, + &quit_now, &password, &password_is_read, + &old_sigchld_action, orig_sigmask, + helper_directory, user, group, + (const char *const *)mandos_argv)){ + return EX_OSERR; /* Error has already been printed */ + } + /* These variables were only for start_mandos_client() and are not + needed anymore */ + free(mandos_argv); + free(mandos_argz); + mandos_argz = NULL; + if(not add_inotify_dir_watch(queue, epoll_fd, &quit_now, &password, + agent_directory, &cancelled_filenames, + ¤t_time, &mandos_client_exited, + &password_is_read)){ + switch(errno){ /* Error has already been printed */ + case EACCES: + case ENAMETOOLONG: + case ENOENT: + case ENOTDIR: + return EX_OSFILE; + default: + return EX_OSERR; + } + } + if(not add_existing_questions(queue, epoll_fd, &password, + &cancelled_filenames, ¤t_time, + &mandos_client_exited, + &password_is_read, agent_directory)){ + return EXIT_FAILURE; /* Error has already been printed */ + } + + /* Run queue */ + do { + current_time = get_current_time(); + if(not wait_for_event(epoll_fd, queue->next_run, current_time)){ + const error_t saved_errno = errno; + error(EXIT_FAILURE, saved_errno, "Failure while waiting for" + " events"); + } + + current_time = get_current_time(); + if(not run_queue(&queue, &cancelled_filenames, &quit_now)){ + const error_t saved_errno = errno; + error(EXIT_FAILURE, saved_errno, "Failure while running queue"); + } + + /* When no tasks about questions are left in the queue, break out + of the loop (and implicitly exit the program) */ + } while(queue_has_question(queue)); + + restore_signal_handler(&old_sigchld_action); + restore_sigmask(&orig_sigmask); + + return EXIT_SUCCESS; +} + +__attribute__((warn_unused_result)) +mono_microsecs get_current_time(void){ + struct timespec currtime; + if(clock_gettime(CLOCK_MONOTONIC, &currtime) != 0){ + error(0, errno, "Failed to get current time"); + return 0; + } + return ((mono_microsecs)currtime.tv_sec * 1000000) /* seconds */ + + ((mono_microsecs)currtime.tv_nsec / 1000); /* nanoseconds */ +} + +/* End of "main" section */ + +/* Start of regular code section; ALL this code has tests */ + +__attribute__((nonnull)) +bool parse_arguments(int argc, char *argv[], const bool exit_failure, + char **agent_directory, char **helper_directory, + uid_t *const user, gid_t *const group, + char **mandos_argz, size_t *mandos_argz_length){ + + const struct argp_option options[] = { + { .name="agent-directory",.key='d', .arg="DIRECTORY", + .doc="Systemd password agent directory" }, + { .name="helper-directory",.key=128, .arg="DIRECTORY", + .doc="Mandos Client password helper directory" }, + { .name="plugin-helper-dir", .key=129, /* From plugin-runner */ + .flags=OPTION_HIDDEN | OPTION_ALIAS }, + { .name="user", .key='u', .arg="USERID", + .doc="User ID the Mandos Client will use as its unprivileged" + " user" }, + { .name="userid", .key=130, /* From plugin--runner */ + .flags=OPTION_HIDDEN | OPTION_ALIAS }, + { .name="group", .key='g', .arg="GROUPID", + .doc="Group ID the Mandos Client will use as its unprivileged" + " group" }, + { .name="groupid", .key=131, /* From plugin--runner */ + .flags=OPTION_HIDDEN | OPTION_ALIAS }, + { .name="test", .key=255, /* See should_only_run_tests() */ + .doc="Skip normal operation, and only run self-tests. See" + " --test --help.", .group=10, }, + { NULL }, + }; + + __attribute__((nonnull(3))) + error_t parse_opt(int key, char *arg, struct argp_state *state){ + errno = 0; + switch(key){ + case 'd': /* --agent-directory */ + *agent_directory = strdup(arg); + break; + case 128: /* --helper-directory */ + case 129: /* --plugin-helper-dir */ + *helper_directory = strdup(arg); + break; + case 'u': /* --user */ + case 130: /* --userid */ + { + char *tmp; + uintmax_t tmp_id = 0; + errno = 0; + tmp_id = (uid_t)strtoumax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmp_id != (uid_t)tmp_id or (uid_t)tmp_id == 0){ + return ARGP_ERR_UNKNOWN; + } + *user = (uid_t)tmp_id; + errno = 0; + break; + } + case 'g': /* --group */ + case 131: /* --groupid */ + { + char *tmp; + uintmax_t tmp_id = 0; + errno = 0; + tmp_id = (uid_t)strtoumax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmp_id != (gid_t)tmp_id or (gid_t)tmp_id == 0){ + return ARGP_ERR_UNKNOWN; + } + *group = (gid_t)tmp_id; + errno = 0; + break; + } + case ARGP_KEY_ARGS: + /* Copy arguments into argz vector */ + return argz_create(state->argv + state->next, mandos_argz, + mandos_argz_length); + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + const struct argp argp = { + .options=options, + .parser=parse_opt, + .args_doc="[MANDOS_CLIENT [OPTION...]]\n--test", + .doc = "Mandos password agent -- runs Mandos client as a" + " systemd password agent", + }; + + errno = argp_parse(&argp, argc, argv, + exit_failure ? 0 : ARGP_NO_EXIT, NULL, NULL); + + return errno == 0; +} + +__attribute__((nonnull, warn_unused_result)) +bool block_sigchld(sigset_t *const orig_sigmask){ + sigset_t sigchld_sigmask; + if(sigemptyset(&sigchld_sigmask) < 0){ + error(0, errno, "Failed to empty signal set"); + return false; + } + if(sigaddset(&sigchld_sigmask, SIGCHLD) < 0){ + error(0, errno, "Failed to add SIGCHLD to signal set"); + return false; + } + if(pthread_sigmask(SIG_BLOCK, &sigchld_sigmask, orig_sigmask) != 0){ + error(0, errno, "Failed to block SIGCHLD signal"); + return false; + } + return true; +} + +__attribute__((nonnull, warn_unused_result, const)) +bool restore_sigmask(const sigset_t *const orig_sigmask){ + if(pthread_sigmask(SIG_SETMASK, orig_sigmask, NULL) != 0){ + error(0, errno, "Failed to restore blocked signals"); + return false; + } + return true; +} + +__attribute__((nonnull, warn_unused_result)) +bool setup_signal_handler(struct sigaction *const old_sigchld_action){ + struct sigaction sigchld_action = { + .sa_handler=handle_sigchld, + .sa_flags=SA_RESTART | SA_NOCLDSTOP, + }; + /* Set all signals in "sa_mask" struct member; this makes all + signals automatically blocked during signal handler */ + if(sigfillset(&sigchld_action.sa_mask) != 0){ + error(0, errno, "Failed to do sigfillset()"); + return false; + } + if(sigaction(SIGCHLD, &sigchld_action, old_sigchld_action) != 0){ + error(0, errno, "Failed to set SIGCHLD signal handler"); + return false; + } + return true; +} + +__attribute__((nonnull, warn_unused_result)) +bool restore_signal_handler(const struct sigaction *const + old_sigchld_action){ + if(sigaction(SIGCHLD, old_sigchld_action, NULL) != 0){ + error(0, errno, "Failed to restore signal handler"); + return false; + } + return true; +} + +__attribute__((warn_unused_result, malloc)) +task_queue *create_queue(void){ + task_queue *queue = malloc(sizeof(task_queue)); + if(queue){ + queue->tasks = NULL; + queue->length = 0; + queue->allocated = 0; + queue->next_run = 0; + } + return queue; +} + +__attribute__((nonnull, warn_unused_result)) +bool add_to_queue(task_queue *const queue, const task_context task){ + const size_t needed_size = sizeof(task_context)*(queue->length + 1); + if(needed_size > (queue->allocated)){ + task_context *const new_tasks = realloc(queue->tasks, + needed_size); + if(new_tasks == NULL){ + error(0, errno, "Failed to allocate %" PRIuMAX + " bytes for queue->tasks", (uintmax_t)needed_size); + return false; + } + queue->tasks = new_tasks; + queue->allocated = needed_size; + } + /* Using memcpy here is necessary because doing */ + /* queue->tasks[queue->length++] = task; */ + /* would violate const-ness of task members */ + memcpy(&(queue->tasks[queue->length++]), &task, + sizeof(task_context)); + return true; +} + +__attribute__((nonnull)) +void cleanup_task(const task_context *const task){ + const error_t saved_errno = errno; + /* free and close all task data */ + free(task->question_filename); + if(task->filename != task->question_filename){ + free(task->filename); + } + if(task->pid > 0){ + kill(task->pid, SIGTERM); + } + if(task->fd > 0){ + close(task->fd); + } + errno = saved_errno; +} + +__attribute__((nonnull)) +void free_queue(task_queue *const queue){ + free(queue->tasks); + free(queue); +} + +__attribute__((nonnull)) +void cleanup_queue(task_queue *const *const queue){ + if(*queue == NULL){ + return; + } + for(size_t i = 0; i < (*queue)->length; i++){ + const task_context *const task = ((*queue)->tasks)+i; + cleanup_task(task); + } + free_queue(*queue); +} + +__attribute__((pure, nonnull, warn_unused_result)) +bool queue_has_question(const task_queue *const queue){ + for(size_t i=0; i < queue->length; i++){ + if(queue->tasks[i].question_filename != NULL){ + return true; + } + } + return false; +} + +__attribute__((nonnull)) +void cleanup_close(const int *const fd){ + const error_t saved_errno = errno; + close(*fd); + errno = saved_errno; +} + +__attribute__((nonnull)) +void cleanup_string(char *const *const ptr){ + free(*ptr); +} + +__attribute__((nonnull)) +void cleanup_buffer(buffer *buf){ + if(buf->allocated > 0){ +#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 25) + explicit_bzero(buf->data, buf->allocated); +#else + memset(buf->data, '\0', buf->allocated); +#endif + } + if(buf->data != NULL){ + if(munlock(buf->data, buf->allocated) != 0){ + error(0, errno, "Failed to unlock memory of old buffer"); + } + free(buf->data); + buf->data = NULL; + } + buf->length = 0; + buf->allocated = 0; +} + +__attribute__((pure, nonnull, warn_unused_result)) +bool string_set_contains(const string_set set, const char *const str){ + for(const char *s = set.argz; s != NULL and set.argz_len > 0; + s = argz_next(set.argz, set.argz_len, s)){ + if(strcmp(s, str) == 0){ + return true; + } + } + return false; +} + +__attribute__((nonnull, warn_unused_result)) +bool string_set_add(string_set *const set, const char *const str){ + if(string_set_contains(*set, str)){ + return true; + } + error_t error = argz_add(&set->argz, &set->argz_len, str); + if(error == 0){ + return true; + } + errno = error; + return false; +} + +__attribute__((nonnull)) +void string_set_clear(string_set *set){ + free(set->argz); + set->argz = NULL; + set->argz_len = 0; +} + +__attribute__((nonnull)) +void string_set_swap(string_set *const set1, string_set *const set2){ + /* Swap contents of two string sets */ + { + char *const tmp_argz = set1->argz; + set1->argz = set2->argz; + set2->argz = tmp_argz; + } + { + const size_t tmp_argz_len = set1->argz_len; + set1->argz_len = set2->argz_len; + set2->argz_len = tmp_argz_len; + } +} + +__attribute__((nonnull, warn_unused_result)) +bool start_mandos_client(task_queue *const queue, + const int epoll_fd, + bool *const mandos_client_exited, + bool *const quit_now, buffer *const password, + bool *const password_is_read, + const struct sigaction *const + old_sigchld_action, const sigset_t sigmask, + const char *const helper_directory, + const uid_t user, const gid_t group, + const char *const *const argv){ + int pipefds[2]; + if(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK) != 0){ + error(0, errno, "Failed to pipe2(..., O_CLOEXEC | O_NONBLOCK)"); + return false; + } + + const pid_t pid = fork(); + if(pid == 0){ + if(not restore_signal_handler(old_sigchld_action)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&sigmask)){ + _exit(EXIT_FAILURE); + } + if(close(pipefds[0]) != 0){ + error(0, errno, "Failed to close() parent pipe fd"); + _exit(EXIT_FAILURE); + } + if(dup2(pipefds[1], STDOUT_FILENO) == -1){ + error(0, errno, "Failed to dup2() pipe fd to stdout"); + _exit(EXIT_FAILURE); + } + if(close(pipefds[1]) != 0){ + error(0, errno, "Failed to close() old child pipe fd"); + _exit(EXIT_FAILURE); + } + if(setenv("MANDOSPLUGINHELPERDIR", helper_directory, 1) != 0){ + error(0, errno, "Failed to setenv(\"MANDOSPLUGINHELPERDIR\"," + " \"%s\", 1)", helper_directory); + _exit(EXIT_FAILURE); + } + if(group != 0 and setresgid(group, 0, 0) == -1){ + error(0, errno, "Failed to setresgid(-1, %" PRIuMAX ", %" + PRIuMAX")", (uintmax_t)group, (uintmax_t)group); + _exit(EXIT_FAILURE); + } + if(user != 0 and setresuid(user, 0, 0) == -1){ + error(0, errno, "Failed to setresuid(-1, %" PRIuMAX ", %" + PRIuMAX")", (uintmax_t)user, (uintmax_t)user); + _exit(EXIT_FAILURE); + } +#ifdef __GNUC__ +#pragma GCC diagnostic push + /* For historical reasons, the "argv" argument to execv() is not + const, but it is safe to override this. */ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + execv(argv[0], (char **)argv); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + error(0, errno, "execv(\"%s\", ...) failed", argv[0]); + _exit(EXIT_FAILURE); + } + close(pipefds[1]); + + if(not add_to_queue(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=mandos_client_exited, + .quit_now=quit_now, + })){ + error(0, errno, "Failed to add wait_for_mandos_client to queue"); + close(pipefds[0]); + return false; + } + + const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pipefds[0], + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }); + if(ret != 0 and errno != EEXIST){ + error(0, errno, "Failed to add file descriptor to epoll set"); + close(pipefds[0]); + return false; + } + + return add_to_queue(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=quit_now, + .password=password, + .password_is_read=password_is_read, + }); +} + +__attribute__((nonnull)) +void wait_for_mandos_client_exit(const task_context task, + task_queue *const queue){ + const pid_t pid = task.pid; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const quit_now = task.quit_now; + + int status; + switch(waitpid(pid, &status, WNOHANG)){ + case 0: /* Not exited yet */ + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add myself to queue"); + *quit_now = true; + } + break; + case -1: /* Error */ + error(0, errno, "waitpid(%" PRIdMAX ") failed", (intmax_t)pid); + if(errno != ECHILD){ + kill(pid, SIGTERM); + } + *quit_now = true; + break; + default: /* Has exited */ + *mandos_client_exited = true; + if((not WIFEXITED(status)) + or (WEXITSTATUS(status) != EXIT_SUCCESS)){ + error(0, 0, "Mandos client failed or was killed"); + *quit_now = true; + } + } +} + +__attribute__((nonnull)) +void read_mandos_client_output(const task_context task, + task_queue *const queue){ + buffer *const password = task.password; + bool *const quit_now = task.quit_now; + bool *const password_is_read = task.password_is_read; + const int fd = task.fd; + const int epoll_fd = task.epoll_fd; + + const size_t new_potential_size = (password->length + PIPE_BUF); + if(password->allocated < new_potential_size){ + char *const new_buffer = calloc(new_potential_size, 1); + if(new_buffer == NULL){ + error(0, errno, "Failed to allocate %" PRIuMAX + " bytes for password", (uintmax_t)new_potential_size); + *quit_now = true; + close(fd); + return; + } + if(mlock(new_buffer, new_potential_size) != 0){ + /* Warn but do not treat as fatal error */ + if(errno != EPERM and errno != ENOMEM){ + error(0, errno, "Failed to lock memory for password"); + } + } + if(password->length > 0){ + memcpy(new_buffer, password->data, password->length); +#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 25) + explicit_bzero(password->data, password->allocated); +#else + memset(password->data, '\0', password->allocated); +#endif + } + if(password->data != NULL){ + if(munlock(password->data, password->allocated) != 0){ + error(0, errno, "Failed to unlock memory of old buffer"); + } + free(password->data); + } + password->data = new_buffer; + password->allocated = new_potential_size; + } + + const ssize_t read_length = read(fd, password->data + + password->length, PIPE_BUF); + + if(read_length == 0){ /* EOF */ + *password_is_read = true; + close(fd); + return; + } + if(read_length < 0 and errno != EAGAIN){ /* Actual error */ + error(0, errno, "Failed to read password from Mandos client"); + *quit_now = true; + close(fd); + return; + } + if(read_length > 0){ /* Data has been read */ + password->length += (size_t)read_length; + } + + /* Either data was read, or EAGAIN was indicated, meaning no data + available yet */ + + /* Re-add the fd to the epoll set */ + const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }); + if(ret != 0 and errno != EEXIST){ + error(0, errno, "Failed to re-add file descriptor to epoll set"); + *quit_now = true; + close(fd); + return; + } + + /* Re-add myself to the queue */ + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add myself to queue"); + *quit_now = true; + close(fd); + } +} + +__attribute__((nonnull, warn_unused_result)) +bool add_inotify_dir_watch(task_queue *const queue, + const int epoll_fd, bool *const quit_now, + buffer *const password, + const char *const dir, + string_set *cancelled_filenames, + const mono_microsecs *const current_time, + bool *const mandos_client_exited, + bool *const password_is_read){ + const int fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if(fd == -1){ + error(0, errno, "Failed to create inotify instance"); + return false; + } + + if(inotify_add_watch(fd, dir, IN_CLOSE_WRITE | IN_MOVED_TO + | IN_MOVED_FROM| IN_DELETE | IN_EXCL_UNLINK + | IN_ONLYDIR) + == -1){ + error(0, errno, "Failed to create inotify watch on %s", dir); + return false; + } + + /* Add the inotify fd to the epoll set */ + const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }); + if(ret != 0 and errno != EEXIST){ + error(0, errno, "Failed to add file descriptor to epoll set"); + close(fd); + return false; + } + + const task_context read_inotify_event_task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .quit_now=quit_now, + .password=password, + .fd=fd, + .filename=strdup(dir), + .cancelled_filenames=cancelled_filenames, + .current_time=current_time, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + }; + if(read_inotify_event_task.filename == NULL){ + error(0, errno, "Failed to strdup(\"%s\")", dir); + close(fd); + return false; + } + + return add_to_queue(queue, read_inotify_event_task); +} + +__attribute__((nonnull)) +void read_inotify_event(const task_context task, + task_queue *const queue){ + const int fd = task.fd; + const int epoll_fd = task.epoll_fd; + char *const filename = task.filename; + bool *quit_now = task.quit_now; + buffer *const password = task.password; + string_set *const cancelled_filenames = task.cancelled_filenames; + const mono_microsecs *const current_time = task.current_time; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const password_is_read = task.password_is_read; + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct { + struct inotify_event event; + char name_buffer[NAME_MAX + 1]; + } ievent_buffer; + struct inotify_event *const ievent = &ievent_buffer.event; + + const ssize_t read_length = read(fd, ievent, ievent_size); + if(read_length == 0){ /* EOF */ + error(0, 0, "Got EOF from inotify fd for directory %s", filename); + *quit_now = true; + cleanup_task(&task); + return; + } + if(read_length < 0 and errno != EAGAIN){ /* Actual error */ + error(0, errno, "Failed to read from inotify fd for directory %s", + filename); + *quit_now = true; + cleanup_task(&task); + return; + } + if(read_length > 0 /* Data has been read */ + and fnmatch("ask.*", ievent->name, FNM_FILE_NAME) == 0){ + char *question_filename = NULL; + const ssize_t question_filename_length + = asprintf(&question_filename, "%s/%s", filename, ievent->name); + if(question_filename_length < 0){ + error(0, errno, "Failed to create file name from directory name" + " %s and file name %s", filename, ievent->name); + } else { + if(ievent->mask & (IN_CLOSE_WRITE | IN_MOVED_TO)){ + if(not add_to_queue(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .question_filename=question_filename, + .filename=question_filename, + .password=password, + .cancelled_filenames=cancelled_filenames, + .current_time=current_time, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + })){ + error(0, errno, "Failed to add open_and_parse_question task" + " for file name %s to queue", filename); + } else { + /* Force the added task (open_and_parse_question) to run + immediately */ + queue->next_run = 1; + } + } else if(ievent->mask & (IN_MOVED_FROM | IN_DELETE)){ + if(not string_set_add(cancelled_filenames, + question_filename)){ + error(0, errno, "Could not add question %s to" + " cancelled_questions", question_filename); + *quit_now = true; + free(question_filename); + cleanup_task(&task); + return; + } + free(question_filename); + } + } + } + + /* Either data was read, or EAGAIN was indicated, meaning no data + available yet */ + + /* Re-add myself to the queue */ + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to re-add read_inotify_event(%s) to" + " queue", filename); + *quit_now = true; + cleanup_task(&task); + return; + } + + /* Re-add the fd to the epoll set */ + const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }); + if(ret != 0 and errno != EEXIST){ + error(0, errno, "Failed to re-add inotify file descriptor %d for" + " directory %s to epoll set", fd, filename); + /* Force the added task (read_inotify_event) to run again, at most + one second from now */ + if((queue->next_run == 0) + or (queue->next_run > (*current_time + 1000000))){ + queue->next_run = *current_time + 1000000; + } + } +} + +__attribute__((nonnull)) +void open_and_parse_question(const task_context task, + task_queue *const queue){ + __attribute__((cleanup(cleanup_string))) + char *question_filename = task.question_filename; + const int epoll_fd = task.epoll_fd; + buffer *const password = task.password; + string_set *const cancelled_filenames = task.cancelled_filenames; + const mono_microsecs *const current_time = task.current_time; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const password_is_read = task.password_is_read; + + /* We use the GLib "Key-value file parser" functions to parse the + question file. See for specification of contents */ + __attribute__((nonnull)) + void cleanup_g_key_file(GKeyFile **key_file){ + if(*key_file != NULL){ + g_key_file_free(*key_file); + } + } + + __attribute__((cleanup(cleanup_g_key_file))) + GKeyFile *key_file = g_key_file_new(); + if(key_file == NULL){ + error(0, errno, "Failed g_key_file_new() for \"%s\"", + question_filename); + return; + } + GError *glib_error = NULL; + if(g_key_file_load_from_file(key_file, question_filename, + G_KEY_FILE_NONE, &glib_error) != TRUE){ + /* If a file was removed, we should ignore it, so */ + /* only show error message if file actually existed */ + if(glib_error->code != G_FILE_ERROR_NOENT){ + error(0, 0, "Failed to load question data from file \"%s\": %s", + question_filename, glib_error->message); + } + return; + } + + __attribute__((cleanup(cleanup_string))) + char *socket_name = g_key_file_get_string(key_file, "Ask", + "Socket", + &glib_error); + if(socket_name == NULL){ + error(0, 0, "Question file \"%s\" did not contain \"Socket\": %s", + question_filename, glib_error->message); + return; + } + + if(strlen(socket_name) == 0){ + error(0, 0, "Question file \"%s\" had empty \"Socket\" value", + question_filename); + return; + } + + const guint64 pid = g_key_file_get_uint64(key_file, "Ask", "PID", + &glib_error); + if(glib_error != NULL){ + error(0, 0, "Question file \"%s\" contained bad \"PID\": %s", + question_filename, glib_error->message); + return; + } + + if((pid != (guint64)((pid_t)pid)) + or (kill((pid_t)pid, 0) != 0)){ + error(0, 0, "PID %" PRIuMAX " in question file \"%s\" is bad or" + " does not exist", (uintmax_t)pid, question_filename); + return; + } + + guint64 notafter = g_key_file_get_uint64(key_file, "Ask", + "NotAfter", &glib_error); + if(glib_error != NULL){ + if(glib_error->code != G_KEY_FILE_ERROR_KEY_NOT_FOUND){ + error(0, 0, "Question file \"%s\" contained bad \"NotAfter\":" + " %s", question_filename, glib_error->message); + } + notafter = 0; + } + if(notafter != 0){ + if(queue->next_run == 0 or (queue->next_run > notafter)){ + queue->next_run = notafter; + } + if(*current_time >= notafter){ + return; + } + } + + const task_context connect_question_socket_task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=password, + .filename=strdup(socket_name), + .cancelled_filenames=task.cancelled_filenames, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + .current_time=current_time, + }; + if(connect_question_socket_task.question_filename == NULL + or connect_question_socket_task.filename == NULL + or not add_to_queue(queue, connect_question_socket_task)){ + error(0, errno, "Failed to add connect_question_socket for socket" + " %s (from \"%s\") to queue", socket_name, + question_filename); + cleanup_task(&connect_question_socket_task); + return; + } + /* Force the added task (connect_question_socket) to run + immediately */ + queue->next_run = 1; + + if(notafter > 0){ + char *const dup_filename = strdup(question_filename); + const task_context cancel_old_question_task = { + .func=cancel_old_question, + .question_filename=dup_filename, + .notafter=notafter, + .filename=dup_filename, + .cancelled_filenames=cancelled_filenames, + .current_time=current_time, + }; + if(cancel_old_question_task.question_filename == NULL + or not add_to_queue(queue, cancel_old_question_task)){ + error(0, errno, "Failed to add cancel_old_question for file " + "\"%s\" to queue", question_filename); + cleanup_task(&cancel_old_question_task); + return; + } + } +} + +__attribute__((nonnull)) +void cancel_old_question(const task_context task, + task_queue *const queue){ + char *const question_filename = task.question_filename; + string_set *const cancelled_filenames = task.cancelled_filenames; + const mono_microsecs notafter = task.notafter; + const mono_microsecs *const current_time = task.current_time; + + if(*current_time >= notafter){ + if(not string_set_add(cancelled_filenames, question_filename)){ + error(0, errno, "Failed to cancel question for file %s", + question_filename); + } + cleanup_task(&task); + return; + } + + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add cancel_old_question for file " + "%s to queue", question_filename); + cleanup_task(&task); + return; + } + + if((queue->next_run == 0) or (queue->next_run > notafter)){ + queue->next_run = notafter; + } +} + +__attribute__((nonnull)) +void connect_question_socket(const task_context task, + task_queue *const queue){ + char *const question_filename = task.question_filename; + char *const filename = task.filename; + const int epoll_fd = task.epoll_fd; + buffer *const password = task.password; + string_set *const cancelled_filenames = task.cancelled_filenames; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const password_is_read = task.password_is_read; + const mono_microsecs *const current_time = task.current_time; + + struct sockaddr_un sock_name = { .sun_family=AF_LOCAL }; + + if(sizeof(sock_name.sun_path) <= strlen(filename)){ + error(0, 0, "Socket filename is larger than" + " sizeof(sockaddr_un.sun_path); %" PRIuMAX ": \"%s\"", + (uintmax_t)sizeof(sock_name.sun_path), filename); + if(not string_set_add(cancelled_filenames, question_filename)){ + error(0, errno, "Failed to cancel question for file %s", + question_filename); + } + cleanup_task(&task); + return; + } + + const int fd = socket(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if(fd < 0){ + error(0, errno, + "Failed to create socket(PF_LOCAL, SOCK_DGRAM, 0)"); + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add connect_question_socket for file" + " \"%s\" and socket \"%s\" to queue", question_filename, + filename); + cleanup_task(&task); + } else { + /* Force the added task (connect_question_socket) to run + immediately */ + queue->next_run = 1; + } + return; + } + + strncpy(sock_name.sun_path, filename, sizeof(sock_name.sun_path)); + if(connect(fd, (struct sockaddr *)&sock_name, + (socklen_t)SUN_LEN(&sock_name)) != 0){ + error(0, errno, "Failed to connect socket to \"%s\"", filename); + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add connect_question_socket for file" + " \"%s\" and socket \"%s\" to queue", question_filename, + filename); + cleanup_task(&task); + } else { + /* Force the added task (connect_question_socket) to run again, + at most one second from now */ + if((queue->next_run == 0) + or (queue->next_run > (*current_time + 1000000))){ + queue->next_run = *current_time + 1000000; + } + } + return; + } + + /* Not necessary, but we can try, and merely warn on failure */ + if(shutdown(fd, SHUT_RD) != 0){ + error(0, errno, "Failed to shutdown reading from socket \"%s\"", + filename); + } + + /* Add the fd to the epoll set */ + if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event){ .events=EPOLLOUT }) + != 0){ + error(0, errno, "Failed to add inotify file descriptor %d for" + " socket %s to epoll set", fd, filename); + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add connect_question_socket for file" + " \"%s\" and socket \"%s\" to queue", question_filename, + filename); + cleanup_task(&task); + } else { + /* Force the added task (connect_question_socket) to run again, + at most one second from now */ + if((queue->next_run == 0) + or (queue->next_run > (*current_time + 1000000))){ + queue->next_run = *current_time + 1000000; + } + } + return; + } + + /* add task send_password_to_socket to queue */ + const task_context send_password_to_socket_task = { + .func=send_password_to_socket, + .question_filename=question_filename, + .filename=filename, + .epoll_fd=epoll_fd, + .fd=fd, + .password=password, + .cancelled_filenames=cancelled_filenames, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + .current_time=current_time, + }; + + if(not add_to_queue(queue, send_password_to_socket_task)){ + error(0, errno, "Failed to add send_password_to_socket for" + " file \"%s\" and socket \"%s\" to queue", + question_filename, filename); + cleanup_task(&send_password_to_socket_task); + } +} + +__attribute__((nonnull)) +void send_password_to_socket(const task_context task, + task_queue *const queue){ + char *const question_filename=task.question_filename; + char *const filename=task.filename; + const int epoll_fd=task.epoll_fd; + const int fd=task.fd; + buffer *const password=task.password; + string_set *const cancelled_filenames=task.cancelled_filenames; + bool *const mandos_client_exited = task.mandos_client_exited; + bool *const password_is_read = task.password_is_read; + const mono_microsecs *const current_time = task.current_time; + + if(*mandos_client_exited and *password_is_read){ + + const size_t send_buffer_length = password->length + 2; + char *send_buffer = malloc(send_buffer_length); + if(send_buffer == NULL){ + error(0, errno, "Failed to allocate send_buffer"); + } else { + if(mlock(send_buffer, send_buffer_length) != 0){ + /* Warn but do not treat as fatal error */ + if(errno != EPERM and errno != ENOMEM){ + error(0, errno, "Failed to lock memory for password" + " buffer"); + } + } + /* “[…] send a single datagram to the socket consisting of the + password string either prefixed with "+" or with "-" + depending on whether the password entry was successful or + not. You may but don't have to include a final NUL byte in + your message. + + — (Wed 08 Oct 2014 02:14:28 AM UTC) + */ + send_buffer[0] = '+'; /* Prefix with "+" */ + /* Always add an extra NUL */ + send_buffer[password->length + 1] = '\0'; + if(password->length > 0){ + memcpy(send_buffer + 1, password->data, password->length); + } + errno = 0; + ssize_t ssret = send(fd, send_buffer, send_buffer_length, + MSG_NOSIGNAL); + const error_t saved_errno = errno; +#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 25) + explicit_bzero(send_buffer, send_buffer_length); +#else + memset(send_buffer, '\0', send_buffer_length); +#endif + if(munlock(send_buffer, send_buffer_length) != 0){ + error(0, errno, "Failed to unlock memory of send buffer"); + } + free(send_buffer); + if(ssret < 0 or ssret < (ssize_t)send_buffer_length){ + switch(saved_errno){ + case EINTR: + case ENOBUFS: + case ENOMEM: + case EADDRINUSE: + case ECONNREFUSED: + case ECONNRESET: + case ENOENT: + case ETOOMANYREFS: + case EAGAIN: + /* Retry, below */ + break; + case EMSGSIZE: + error(0, 0, "Password of size %" PRIuMAX " is too big", + (uintmax_t)password->length); +#if __GNUC__ < 7 + /* FALLTHROUGH */ +#else + __attribute__((fallthrough)); +#endif + case 0: + if(ssret >= 0 and ssret < (ssize_t)send_buffer_length){ + error(0, 0, "Password only partially sent to socket"); + } +#if __GNUC__ < 7 + /* FALLTHROUGH */ +#else + __attribute__((fallthrough)); +#endif + default: + error(0, saved_errno, "Failed to send() to socket %s", + filename); + if(not string_set_add(cancelled_filenames, + question_filename)){ + error(0, errno, "Failed to cancel question for file %s", + question_filename); + } + cleanup_task(&task); + return; + } + } else { + /* Success */ + cleanup_task(&task); + return; + } + } + } + + /* We failed or are not ready yet; retry later */ + + if(not add_to_queue(queue, task)){ + error(0, errno, "Failed to add send_password_to_socket for" + " file %s and socket %s to queue", question_filename, + filename); + cleanup_task(&task); + } + + /* Add the fd to the epoll set */ + if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, + &(struct epoll_event){ .events=EPOLLOUT }) + != 0){ + error(0, errno, "Failed to add socket file descriptor %d for" + " socket %s to epoll set", fd, filename); + /* Force the added task (send_password_to_socket) to run again, at + most one second from now */ + if((queue->next_run == 0) + or (queue->next_run > (*current_time + 1000000))){ + queue->next_run = *current_time + 1000000; + } + } +} + +__attribute__((warn_unused_result)) +bool add_existing_questions(task_queue *const queue, + const int epoll_fd, + buffer *const password, + string_set *cancelled_filenames, + const mono_microsecs *const current_time, + bool *const mandos_client_exited, + bool *const password_is_read, + const char *const dirname){ + __attribute__((cleanup(cleanup_string))) + char *dir_pattern = NULL; + const int ret = asprintf(&dir_pattern, "%s/ask.*", dirname); + if(ret < 0 or dir_pattern == NULL){ + error(0, errno, "Could not create glob pattern for directory %s", + dirname); + return false; + } + __attribute__((cleanup(globfree))) + glob_t question_filenames = {}; + switch(glob(dir_pattern, GLOB_ERR | GLOB_NOSORT | GLOB_MARK, + NULL, &question_filenames)){ + case GLOB_ABORTED: + default: + error(0, errno, "Failed to open directory %s", dirname); + return false; + case GLOB_NOMATCH: + error(0, errno, "There are no question files in %s", dirname); + return false; + case GLOB_NOSPACE: + error(0, errno, "Could not allocate memory for question file" + " names in %s", dirname); +#if __GNUC__ < 7 + /* FALLTHROUGH */ +#else + __attribute__((fallthrough)); +#endif + case 0: + for(size_t i = 0; i < question_filenames.gl_pathc; i++){ + char *const question_filename = strdup(question_filenames + .gl_pathv[i]); + const task_context task = { + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .question_filename=question_filename, + .filename=question_filename, + .password=password, + .cancelled_filenames=cancelled_filenames, + .current_time=current_time, + .mandos_client_exited=mandos_client_exited, + .password_is_read=password_is_read, + }; + + if(question_filename == NULL + or not add_to_queue(queue, task)){ + error(0, errno, "Failed to add open_and_parse_question for" + " file %s to queue", + question_filenames.gl_pathv[i]); + free(question_filename); + } else { + queue->next_run = 1; + } + } + return true; + } +} + +__attribute__((nonnull, warn_unused_result)) +bool wait_for_event(const int epoll_fd, + const mono_microsecs queue_next_run, + const mono_microsecs current_time){ + __attribute__((const)) + int milliseconds_to_wait(const mono_microsecs currtime, + const mono_microsecs nextrun){ + if(currtime >= nextrun){ + return 0; + } + const uintmax_t wait_time_ms = (nextrun - currtime) / 1000; + if(wait_time_ms > (uintmax_t)INT_MAX){ + return INT_MAX; + } + return (int)wait_time_ms; + } + + const int wait_time_ms = milliseconds_to_wait(current_time, + queue_next_run); + + /* Prepare unblocking of SIGCHLD during epoll_pwait */ + sigset_t temporary_unblocked_sigmask; + /* Get current signal mask */ + if(pthread_sigmask(-1, NULL, &temporary_unblocked_sigmask) != 0){ + return false; + } + /* Remove SIGCHLD from the signal mask */ + if(sigdelset(&temporary_unblocked_sigmask, SIGCHLD) != 0){ + return false; + } + struct epoll_event events[8]; /* Ignored */ + int ret = epoll_pwait(epoll_fd, events, + sizeof(events) / sizeof(struct epoll_event), + queue_next_run == 0 ? -1 : (int)wait_time_ms, + &temporary_unblocked_sigmask); + if(ret < 0 and errno != EINTR){ + error(0, errno, "Failed epoll_pwait(epfd=%d, ..., timeout=%d," + " ...", epoll_fd, + queue_next_run == 0 ? -1 : (int)wait_time_ms); + return false; + } + return clear_all_fds_from_epoll_set(epoll_fd); +} + +bool clear_all_fds_from_epoll_set(const int epoll_fd){ + /* Create a new empty epoll set */ + __attribute__((cleanup(cleanup_close))) + const int new_epoll_fd = epoll_create1(EPOLL_CLOEXEC); + if(new_epoll_fd < 0){ + return false; + } + /* dup3() the new epoll set fd over the old one, replacing it */ + if(dup3(new_epoll_fd, epoll_fd, O_CLOEXEC) < 0){ + return false; + } + return true; +} + +__attribute__((nonnull, warn_unused_result)) +bool run_queue(task_queue **const queue, + string_set *const cancelled_filenames, + bool *const quit_now){ + + task_queue *new_queue = create_queue(); + if(new_queue == NULL){ + return false; + } + + __attribute__((cleanup(string_set_clear))) + string_set old_cancelled_filenames = {}; + string_set_swap(cancelled_filenames, &old_cancelled_filenames); + + /* Declare i outside the for loop, since we might need i after the + loop in case we aborted in the middle */ + size_t i; + for(i=0; i < (*queue)->length and not *quit_now; i++){ + task_context *const task = &((*queue)->tasks[i]); + const char *const question_filename = task->question_filename; + /* Skip any task referencing a cancelled question filename */ + if(question_filename != NULL + and string_set_contains(old_cancelled_filenames, + question_filename)){ + cleanup_task(task); + continue; + } + task->func(*task, new_queue); + } + + if(*quit_now){ + /* we might be in the middle of the queue, so clean up any + remaining tasks in the current queue */ + for(; i < (*queue)->length; i++){ + cleanup_task(&((*queue)->tasks[i])); + } + free_queue(*queue); + *queue = new_queue; + new_queue = NULL; + return false; + } + free_queue(*queue); + *queue = new_queue; + new_queue = NULL; + + return true; +} + +/* End of regular code section */ + +/* Start of tests section; here are the tests for the above code */ + +/* This "fixture" data structure is used by the test setup and + teardown functions */ +typedef struct { + struct sigaction orig_sigaction; + sigset_t orig_sigmask; +} test_fixture; + +static void test_setup(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + g_assert_true(setup_signal_handler(&fixture->orig_sigaction)); + g_assert_true(block_sigchld(&fixture->orig_sigmask)); +} + +static void test_teardown(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + g_assert_true(restore_signal_handler(&fixture->orig_sigaction)); + g_assert_true(restore_sigmask(&fixture->orig_sigmask)); +} + +/* Utility function used by tests to search queue for matching task */ +__attribute__((pure, nonnull, warn_unused_result)) +static task_context *find_matching_task(const task_queue *const queue, + const task_context task){ + /* The argument "task" structure is a pattern to match; 0 in any + member means any value matches, otherwise the value must match. + The filename strings are compared by strcmp(), not by pointer. */ + for(size_t i = 0; i < queue->length; i++){ + task_context *const current_task = queue->tasks+i; + /* Check all members of task_context, if set to a non-zero value. + If a member does not match, continue to next task in queue */ + + /* task_func *const func */ + if(task.func != NULL and current_task->func != task.func){ + continue; + } + /* char *const question_filename; */ + if(task.question_filename != NULL + and (current_task->question_filename == NULL + or strcmp(current_task->question_filename, + task.question_filename) != 0)){ + continue; + } + /* const pid_t pid; */ + if(task.pid != 0 and current_task->pid != task.pid){ + continue; + } + /* const int epoll_fd; */ + if(task.epoll_fd != 0 + and current_task->epoll_fd != task.epoll_fd){ + continue; + } + /* bool *const quit_now; */ + if(task.quit_now != NULL + and current_task->quit_now != task.quit_now){ + continue; + } + /* const int fd; */ + if(task.fd != 0 and current_task->fd != task.fd){ + continue; + } + /* bool *const mandos_client_exited; */ + if(task.mandos_client_exited != NULL + and current_task->mandos_client_exited + != task.mandos_client_exited){ + continue; + } + /* buffer *const password; */ + if(task.password != NULL + and current_task->password != task.password){ + continue; + } + /* bool *const password_is_read; */ + if(task.password_is_read != NULL + and current_task->password_is_read != task.password_is_read){ + continue; + } + /* char *filename; */ + if(task.filename != NULL + and (current_task->filename == NULL + or strcmp(current_task->filename, task.filename) != 0)){ + continue; + } + /* string_set *const cancelled_filenames; */ + if(task.cancelled_filenames != NULL + and current_task->cancelled_filenames + != task.cancelled_filenames){ + continue; + } + /* const mono_microsecs notafter; */ + if(task.notafter != 0 + and current_task->notafter != task.notafter){ + continue; + } + /* const mono_microsecs *const current_time; */ + if(task.current_time != NULL + and current_task->current_time != task.current_time){ + continue; + } + /* Current task matches all members; return it */ + return current_task; + } + /* No task in queue matches passed pattern task */ + return NULL; +} + +static void test_create_queue(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *const queue = create_queue(); + g_assert_nonnull(queue); + g_assert_null(queue->tasks); + g_assert_true(queue->length == 0); + g_assert_true(queue->next_run == 0); +} + +static task_func dummy_func; + +static void test_add_to_queue(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + g_assert_true(add_to_queue(queue, + (task_context){ .func=dummy_func })); + g_assert_true(queue->length == 1); + g_assert_nonnull(queue->tasks); + g_assert_true(queue->tasks[0].func == dummy_func); +} + +static void dummy_func(__attribute__((unused)) + const task_context task, + __attribute__((unused)) + task_queue *const queue){ +} + +static void test_queue_has_question_empty(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + g_assert_false(queue_has_question(queue)); +} + +static void test_queue_has_question_false(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + g_assert_true(add_to_queue(queue, + (task_context){ .func=dummy_func })); + g_assert_false(queue_has_question(queue)); +} + +static void test_queue_has_question_true(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + char *const question_filename + = strdup("/nonexistent/question_filename"); + g_assert_nonnull(question_filename); + task_context task = { + .func=dummy_func, + .question_filename=question_filename, + }; + g_assert_true(add_to_queue(queue, task)); + g_assert_true(queue_has_question(queue)); +} + +static void test_queue_has_question_false2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + task_context task = { .func=dummy_func }; + g_assert_true(add_to_queue(queue, task)); + g_assert_true(add_to_queue(queue, task)); + g_assert_cmpint((int)queue->length, ==, 2); + g_assert_false(queue_has_question(queue)); +} + +static void test_queue_has_question_true2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + task_context task1 = { .func=dummy_func }; + g_assert_true(add_to_queue(queue, task1)); + char *const question_filename + = strdup("/nonexistent/question_filename"); + g_assert_nonnull(question_filename); + task_context task2 = { + .func=dummy_func, + .question_filename=question_filename, + }; + g_assert_true(add_to_queue(queue, task2)); + g_assert_cmpint((int)queue->length, ==, 2); + g_assert_true(queue_has_question(queue)); +} + +static void test_cleanup_buffer(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + buffer buf = {}; + + const size_t buffersize = 10; + + buf.data = malloc(buffersize); + g_assert_nonnull(buf.data); + if(mlock(buf.data, buffersize) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + + cleanup_buffer(&buf); + g_assert_null(buf.data); +} + +static +void test_string_set_new_set_contains_nothing(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set = {}; + g_assert_false(string_set_contains(set, "")); /* Empty string */ + g_assert_false(string_set_contains(set, "test_string")); +} + +static void +test_string_set_with_added_string_contains_it(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set = {}; + g_assert_true(string_set_add(&set, "test_string")); + g_assert_true(string_set_contains(set, "test_string")); +} + +static void +test_string_set_cleared_does_not_contain_str(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set = {}; + g_assert_true(string_set_add(&set, "test_string")); + string_set_clear(&set); + g_assert_false(string_set_contains(set, "test_string")); +} + +static +void test_string_set_swap_one_with_empty(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set1 = {}; + __attribute__((cleanup(string_set_clear))) + string_set set2 = {}; + g_assert_true(string_set_add(&set1, "test_string1")); + string_set_swap(&set1, &set2); + g_assert_false(string_set_contains(set1, "test_string1")); + g_assert_true(string_set_contains(set2, "test_string1")); +} + +static +void test_string_set_swap_empty_with_one(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set1 = {}; + __attribute__((cleanup(string_set_clear))) + string_set set2 = {}; + g_assert_true(string_set_add(&set2, "test_string2")); + string_set_swap(&set1, &set2); + g_assert_true(string_set_contains(set1, "test_string2")); + g_assert_false(string_set_contains(set2, "test_string2")); +} + +static void test_string_set_swap_one_with_one(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(string_set_clear))) + string_set set1 = {}; + __attribute__((cleanup(string_set_clear))) + string_set set2 = {}; + g_assert_true(string_set_add(&set1, "test_string1")); + g_assert_true(string_set_add(&set2, "test_string2")); + string_set_swap(&set1, &set2); + g_assert_false(string_set_contains(set1, "test_string1")); + g_assert_true(string_set_contains(set1, "test_string2")); + g_assert_false(string_set_contains(set2, "test_string2")); + g_assert_true(string_set_contains(set2, "test_string1")); +} + +static bool fd_has_cloexec_and_nonblock(const int); + +static bool epoll_set_contains(int, int, uint32_t); + +static void test_start_mandos_client(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/bin/true", NULL }; + + g_assert_true(start_mandos_client(queue, epoll_fd, + &mandos_client_exited, &quit_now, + &password, &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, argv)); + + g_assert_cmpuint((unsigned int)queue->length, >=, 2); + + const task_context *const added_wait_task + = find_matching_task(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + }); + g_assert_nonnull(added_wait_task); + g_assert_cmpint(added_wait_task->pid, >, 0); + g_assert_cmpint(kill(added_wait_task->pid, SIGKILL), ==, 0); + waitpid(added_wait_task->pid, NULL, 0); + + const task_context *const added_read_task + = find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }); + g_assert_nonnull(added_read_task); + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + g_assert_true(epoll_set_contains(epoll_fd, added_read_task->fd, + EPOLLIN | EPOLLRDHUP)); +} + +static bool fd_has_cloexec_and_nonblock(const int fd){ + const int socket_fd_flags = fcntl(fd, F_GETFD, 0); + const int socket_file_flags = fcntl(fd, F_GETFL, 0); + return ((socket_fd_flags >= 0) + and (socket_fd_flags & FD_CLOEXEC) + and (socket_file_flags >= 0) + and (socket_file_flags & O_NONBLOCK)); +} + +__attribute__((const)) +bool is_privileged(void){ + uid_t user = getuid() + 1; + if(user == 0){ /* Overflow check */ + user++; + } + gid_t group = getuid() + 1; + if(group == 0){ /* Overflow check */ + group++; + } + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(setresgid((uid_t)-1, group, group) == -1){ + if(errno != EPERM){ + error(EXIT_FAILURE, errno, "Failed to setresgid(-1, %" PRIuMAX + ", %" PRIuMAX")", (uintmax_t)group, (uintmax_t)group); + } + exit(EXIT_FAILURE); + } + if(setresuid((uid_t)-1, user, user) == -1){ + if(errno != EPERM){ + error(EXIT_FAILURE, errno, "Failed to setresuid(-1, %" PRIuMAX + ", %" PRIuMAX")", (uintmax_t)user, (uintmax_t)user); + } + exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); + } + int status; + waitpid(pid, &status, 0); + if(WIFEXITED(status) and (WEXITSTATUS(status) == EXIT_SUCCESS)){ + return true; + } + return false; +} + +static bool epoll_set_contains(int epoll_fd, int fd, uint32_t events){ + /* Only scan for events in this eventmask */ + const uint32_t eventmask = EPOLLIN | EPOLLOUT | EPOLLRDHUP; + __attribute__((cleanup(cleanup_string))) + char *fdinfo_name = NULL; + int ret = asprintf(&fdinfo_name, "/proc/self/fdinfo/%d", epoll_fd); + g_assert_cmpint(ret, >, 0); + g_assert_nonnull(fdinfo_name); + + FILE *fdinfo = fopen(fdinfo_name, "r"); + g_assert_nonnull(fdinfo); + uint32_t reported_events; + buffer line = {}; + int found_fd = -1; + + do { + if(getline(&line.data, &line.allocated, fdinfo) < 0){ + break; + } + /* See proc(5) for format of /proc/PID/fdinfo/FD for epoll fd's */ + if(sscanf(line.data, "tfd: %d events: %" SCNx32 " ", + &found_fd, &reported_events) == 2){ + if(found_fd == fd){ + break; + } + } + } while(not feof(fdinfo) and not ferror(fdinfo)); + g_assert_cmpint(fclose(fdinfo), ==, 0); + free(line.data); + if(found_fd != fd){ + return false; + } + + if(events == 0){ + /* Don't check events if none are given */ + return true; + } + return (reported_events & eventmask) == (events & eventmask); +} + +static void test_start_mandos_client_execv(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + const char helper_directory[] = "/nonexistent"; + /* Can't execv("/", ...), so this should fail */ + const char *const argv[] = { "/", NULL }; + + { + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC | O_NOCTTY); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + dup2(devnull_fd, STDERR_FILENO); + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, + &password, + (bool[]){false}, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, + argv); + dup2(real_stderr_fd, STDERR_FILENO); + g_assert_true(success); + } + g_assert_cmpuint((unsigned int)queue->length, ==, 2); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + + { + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC | O_NOCTTY); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + dup2(devnull_fd, STDERR_FILENO); + const bool success = run_queue(&queue, &cancelled_filenames, + &quit_now); + dup2(real_stderr_fd, STDERR_FILENO); + if(not success){ + break; + } + } + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_true(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); +} + +static void test_start_mandos_client_suid_euid(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + if(not is_privileged()){ + g_test_skip("Not privileged"); + return; + } + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/usr/bin/id", "--user", NULL }; + uid_t user = 1000; + gid_t group = 1001; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, user, + group, argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_nonnull(password.data); + + uintmax_t id; + g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id), + ==, 1); + g_assert_true((uid_t)id == id); + + g_assert_cmpuint((unsigned int)id, ==, 0); +} + +static void test_start_mandos_client_suid_egid(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + if(not is_privileged()){ + g_test_skip("Not privileged"); + return; + } + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/usr/bin/id", "--group", NULL }; + uid_t user = 1000; + gid_t group = 1001; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, user, + group, argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_nonnull(password.data); + + uintmax_t id; + g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id), + ==, 1); + g_assert_true((gid_t)id == id); + + g_assert_cmpuint((unsigned int)id, ==, 0); +} + +static void test_start_mandos_client_suid_ruid(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + if(not is_privileged()){ + g_test_skip("Not privileged"); + return; + } + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/usr/bin/id", "--user", "--real", + NULL }; + uid_t user = 1000; + gid_t group = 1001; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, user, + group, argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_nonnull(password.data); + + uintmax_t id; + g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id), + ==, 1); + g_assert_true((uid_t)id == id); + + g_assert_cmpuint((unsigned int)id, ==, user); +} + +static void test_start_mandos_client_suid_rgid(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + if(not is_privileged()){ + g_test_skip("Not privileged"); + return; + } + + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/usr/bin/id", "--group", "--real", + NULL }; + uid_t user = 1000; + gid_t group = 1001; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, user, + group, argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_nonnull(password.data); + + uintmax_t id; + g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id), + ==, 1); + g_assert_true((gid_t)id == id); + + g_assert_cmpuint((unsigned int)id, ==, group); +} + +static void test_start_mandos_client_read(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char dummy_test_password[] = "dummy test password"; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/bin/echo", "-n", dummy_test_password, + NULL }; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, + argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_cmpint((int)password.length, ==, + sizeof(dummy_test_password)-1); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(dummy_test_password, password.data, + sizeof(dummy_test_password)-1), ==, 0); +} + +static +void test_start_mandos_client_helper_directory(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + const char *const argv[] = { "/bin/sh", "-c", + "echo -n ${MANDOSPLUGINHELPERDIR}", NULL }; + + const bool success = start_mandos_client(queue, epoll_fd, + &mandos_client_exited, + &quit_now, &password, + &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, + argv); + g_assert_true(success); + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while(((queue->length) > 0) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(mandos_client_exited); + + g_assert_true(password_is_read); + g_assert_cmpint((int)password.length, ==, + sizeof(helper_directory)-1); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(helper_directory, password.data, + sizeof(helper_directory)-1), ==, 0); +} + +__attribute__((nonnull, warn_unused_result)) +static bool proc_status_sigblk_to_sigset(const char *const, + sigset_t *const); + +static void test_start_mandos_client_sigmask(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + bool password_is_read = false; + const char helper_directory[] = "/nonexistent"; + /* see proc(5) for format of /proc/self/status */ + const char *const argv[] = { "/usr/bin/awk", + "$1==\"SigBlk:\"{ print $2 }", "/proc/self/status", NULL }; + + g_assert_true(start_mandos_client(queue, epoll_fd, + &mandos_client_exited, &quit_now, + &password, &password_is_read, + &fixture->orig_sigaction, + fixture->orig_sigmask, + helper_directory, 0, 0, argv)); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + queue->next_run = 0; + string_set cancelled_filenames = {}; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while((not (mandos_client_exited and password_is_read)) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + g_assert_true(mandos_client_exited); + g_assert_true(password_is_read); + + sigset_t parsed_sigmask; + g_assert_true(proc_status_sigblk_to_sigset(password.data, + &parsed_sigmask)); + + for(int signum = 1; signum < NSIG; signum++){ + const bool has_signal = sigismember(&parsed_sigmask, signum); + if(sigismember(&fixture->orig_sigmask, signum)){ + g_assert_true(has_signal); + } else { + g_assert_false(has_signal); + } + } +} + +__attribute__((nonnull, warn_unused_result)) +static bool proc_status_sigblk_to_sigset(const char *const sigblk, + sigset_t *const sigmask){ + /* parse /proc/PID/status SigBlk value and convert to a sigset_t */ + uintmax_t scanned_sigmask; + if(sscanf(sigblk, "%" SCNxMAX " ", &scanned_sigmask) != 1){ + return false; + } + if(sigemptyset(sigmask) != 0){ + return false; + } + for(int signum = 1; signum < NSIG; signum++){ + if(scanned_sigmask & ((uintmax_t)1 << (signum-1))){ + if(sigaddset(sigmask, signum) != 0){ + return false; + } + } + } + return true; +} + +static void run_task_with_stderr_to_dev_null(const task_context task, + task_queue *const queue); + +static +void test_wait_for_mandos_client_exit_badpid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + + bool mandos_client_exited = false; + bool quit_now = false; + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const task_context task = { + .func=wait_for_mandos_client_exit, + .pid=1, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + }; + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_false(mandos_client_exited); + g_assert_true(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static void run_task_with_stderr_to_dev_null(const task_context task, + task_queue *const queue){ + FILE *real_stderr = stderr; + FILE *devnull = fopen("/dev/null", "we"); + g_assert_nonnull(devnull); + + stderr = devnull; + task.func(task, queue); + stderr = real_stderr; + + g_assert_cmpint(fclose(devnull), ==, 0); +} + +static +void test_wait_for_mandos_client_exit_noexit(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + + pid_t create_eternal_process(void){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + while(true){ + pause(); + } + } + return pid; + } + pid_t pid = create_eternal_process(); + g_assert_true(pid != -1); + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const task_context task = { + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + }; + task.func(task, queue); + + g_assert_false(mandos_client_exited); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .pid=task.pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + })); +} + +static +void test_wait_for_mandos_client_exit_success(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + + pid_t create_successful_process(void){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); + } + return pid; + } + const pid_t pid = create_successful_process(); + g_assert_true(pid != -1); + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const task_context initial_task = { + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + }; + g_assert_true(add_to_queue(queue, initial_task)); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + do { + queue->next_run = 0; + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + g_assert_true(run_queue(&queue, (string_set[]){{}}, &quit_now)); + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while((not mandos_client_exited) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_true(mandos_client_exited); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static +void test_wait_for_mandos_client_exit_failure(test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + + pid_t create_failing_process(void){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + exit(EXIT_FAILURE); + } + return pid; + } + const pid_t pid = create_failing_process(); + g_assert_true(pid != -1); + + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + g_assert_true(add_to_queue(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + })); + + g_assert_true(sigismember(&fixture->orig_sigmask, SIGCHLD) == 0); + + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC | O_NOCTTY); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + dup2(devnull_fd, STDERR_FILENO); + const bool success = run_queue(&queue, &cancelled_filenames, + &quit_now); + dup2(real_stderr_fd, STDERR_FILENO); + if(not success){ + break; + } + + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while((not mandos_client_exited) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_true(quit_now); + g_assert_true(mandos_client_exited); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static +void test_wait_for_mandos_client_exit_killed(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + bool mandos_client_exited = false; + bool quit_now = false; + + pid_t create_killed_process(void){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + while(true){ + pause(); + } + } + kill(pid, SIGKILL); + return pid; + } + const pid_t pid = create_killed_process(); + g_assert_true(pid != -1); + + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + g_assert_true(add_to_queue(queue, (task_context){ + .func=wait_for_mandos_client_exit, + .pid=pid, + .mandos_client_exited=&mandos_client_exited, + .quit_now=&quit_now, + })); + + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC, O_NOCTTY); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + + struct timespec starttime, currtime; + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0); + do { + g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0)); + dup2(devnull_fd, STDERR_FILENO); + const bool success = run_queue(&queue, &cancelled_filenames, + &quit_now); + dup2(real_stderr_fd, STDERR_FILENO); + if(not success){ + break; + } + + g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0); + } while((not mandos_client_exited) + and (not quit_now) + and ((currtime.tv_sec - starttime.tv_sec) < 10)); + + g_assert_true(mandos_client_exited); + g_assert_true(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static bool epoll_set_does_not_contain(int, int); + +static +void test_read_mandos_client_output_readerror(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + /* Reading /proc/self/mem from offset 0 will always give EIO */ + const int fd = open("/proc/self/mem", + O_RDONLY | O_CLOEXEC | O_NOCTTY); + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=fd, + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, 0); + g_assert_true(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, fd)); + + g_assert_cmpint(close(fd), ==, -1); +} + +static bool epoll_set_does_not_contain(int epoll_fd, int fd){ + return not epoll_set_contains(epoll_fd, fd, 0); +} + +static +void test_read_mandos_client_output_nodata(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, 0); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static void test_read_mandos_client_output_eof(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + g_assert_true(password_is_read); + g_assert_cmpint((int)password.length, ==, 0); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, pipefds[0])); + + g_assert_cmpint(close(pipefds[0]), ==, -1); +} + +static +void test_read_mandos_client_output_once(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + const char dummy_test_password[] = "dummy test password"; + /* Start with a pre-allocated buffer */ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(sizeof(dummy_test_password)), + .length=0, + .allocated=sizeof(dummy_test_password), + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + g_assert_true(sizeof(dummy_test_password) <= PIPE_BUF); + g_assert_cmpint((int)write(pipefds[1], dummy_test_password, + sizeof(dummy_test_password)), + ==, (int)sizeof(dummy_test_password)); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, + (int)sizeof(dummy_test_password)); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(password.data, dummy_test_password, + sizeof(dummy_test_password)), ==, 0); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static +void test_read_mandos_client_output_malloc(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + const char dummy_test_password[] = "dummy test password"; + /* Start with an empty buffer */ + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + g_assert_true(sizeof(dummy_test_password) <= PIPE_BUF); + g_assert_cmpint((int)write(pipefds[1], dummy_test_password, + sizeof(dummy_test_password)), + ==, (int)sizeof(dummy_test_password)); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, + (int)sizeof(dummy_test_password)); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(password.data, dummy_test_password, + sizeof(dummy_test_password)), ==, 0); + + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static +void test_read_mandos_client_output_append(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + const char dummy_test_password[] = "dummy test password"; + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(PIPE_BUF), + .length=PIPE_BUF, + .allocated=PIPE_BUF, + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + + memset(password.data, 'x', PIPE_BUF); + char password_expected[PIPE_BUF]; + memcpy(password_expected, password.data, PIPE_BUF); + + bool password_is_read = false; + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + g_assert_true(sizeof(dummy_test_password) <= PIPE_BUF); + g_assert_cmpint((int)write(pipefds[1], dummy_test_password, + sizeof(dummy_test_password)), + ==, (int)sizeof(dummy_test_password)); + + task_context task = { + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + }; + task.func(task, queue); + + g_assert_false(password_is_read); + g_assert_cmpint((int)password.length, ==, + PIPE_BUF + sizeof(dummy_test_password)); + g_assert_nonnull(password.data); + g_assert_cmpint(memcmp(password_expected, password.data, PIPE_BUF), + ==, 0); + g_assert_cmpint(memcmp(password.data + PIPE_BUF, + dummy_test_password, + sizeof(dummy_test_password)), ==, 0); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_mandos_client_output, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .password=&password, + .password_is_read=&password_is_read, + .quit_now=&quit_now, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); +} + +static char *make_temporary_directory(void); + +static void test_add_inotify_dir_watch(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .quit_now=&quit_now, + .password=&password, + .filename=tempdir, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + g_assert_true(epoll_set_contains(added_read_task->epoll_fd, + added_read_task->fd, + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_temporary_directory(void){ + char *name = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(name); + char *result = mkdtemp(name); + if(result == NULL){ + free(name); + } + return result; +} + +static void test_add_inotify_dir_watch_fail(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + const char nonexistent_dir[] = "/nonexistent"; + + FILE *real_stderr = stderr; + FILE *devnull = fopen("/dev/null", "we"); + g_assert_nonnull(devnull); + stderr = devnull; + g_assert_false(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, nonexistent_dir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + stderr = real_stderr; + g_assert_cmpint(fclose(devnull), ==, 0); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static void test_add_inotify_dir_watch_nondir(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + const char not_a_directory[] = "/dev/tty"; + + FILE *real_stderr = stderr; + FILE *devnull = fopen("/dev/null", "we"); + g_assert_nonnull(devnull); + stderr = devnull; + g_assert_false(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, not_a_directory, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + stderr = real_stderr; + g_assert_cmpint(fclose(devnull), ==, 0); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static void test_add_inotify_dir_watch_EAGAIN(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + g_assert_cmpint(read(added_read_task->fd, ievent, ievent_size), ==, + -1); + g_assert_cmpint(errno, ==, EAGAIN); + + free(ievent); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_temporary_file_in_directory(const char + *const dir); + +static +void test_add_inotify_dir_watch_IN_CLOSE_WRITE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + + __attribute__((cleanup(cleanup_string))) + char *filename = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(filename); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + ssize_t read_size = 0; + read_size = read(added_read_task->fd, ievent, ievent_size); + + g_assert_cmpint((int)read_size, >, 0); + g_assert_true(ievent->mask & IN_CLOSE_WRITE); + g_assert_cmpstr(ievent->name, ==, basename(filename)); + + free(ievent); + + g_assert_cmpint(unlink(filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_temporary_prefixed_file_in_directory(const char + *const prefix, + const char + *const dir){ + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%sXXXXXX", dir, prefix), + >, 0); + g_assert_nonnull(filename); + const int fd = mkostemp(filename, O_CLOEXEC); + g_assert_cmpint(fd, >=, 0); + g_assert_cmpint(close(fd), ==, 0); + return filename; +} + +static char *make_temporary_file_in_directory(const char + *const dir){ + return make_temporary_prefixed_file_in_directory("temp", dir); +} + +static +void test_add_inotify_dir_watch_IN_MOVED_TO(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *watchdir = make_temporary_directory(); + g_assert_nonnull(watchdir); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, watchdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + + char *sourcedir = make_temporary_directory(); + g_assert_nonnull(sourcedir); + + __attribute__((cleanup(cleanup_string))) + char *filename = make_temporary_file_in_directory(sourcedir); + g_assert_nonnull(filename); + + __attribute__((cleanup(cleanup_string))) + char *targetfilename = NULL; + g_assert_cmpint(asprintf(&targetfilename, "%s/%s", watchdir, + basename(filename)), >, 0); + g_assert_nonnull(targetfilename); + + g_assert_cmpint(rename(filename, targetfilename), ==, 0); + g_assert_cmpint(rmdir(sourcedir), ==, 0); + free(sourcedir); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + ssize_t read_size = read(added_read_task->fd, ievent, ievent_size); + + g_assert_cmpint((int)read_size, >, 0); + g_assert_true(ievent->mask & IN_MOVED_TO); + g_assert_cmpstr(ievent->name, ==, basename(targetfilename)); + + free(ievent); + + g_assert_cmpint(unlink(targetfilename), ==, 0); + g_assert_cmpint(rmdir(watchdir), ==, 0); +} + +static +void test_add_inotify_dir_watch_IN_MOVED_FROM(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(tempfilename); + + __attribute__((cleanup(cleanup_string))) + char *targetdir = make_temporary_directory(); + g_assert_nonnull(targetdir); + + __attribute__((cleanup(cleanup_string))) + char *targetfilename = NULL; + g_assert_cmpint(asprintf(&targetfilename, "%s/%s", targetdir, + basename(tempfilename)), >, 0); + g_assert_nonnull(targetfilename); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + + g_assert_cmpint(rename(tempfilename, targetfilename), ==, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + ssize_t read_size = read(added_read_task->fd, ievent, ievent_size); + + g_assert_cmpint((int)read_size, >, 0); + g_assert_true(ievent->mask & IN_MOVED_FROM); + g_assert_cmpstr(ievent->name, ==, basename(tempfilename)); + + free(ievent); + + g_assert_cmpint(unlink(targetfilename), ==, 0); + g_assert_cmpint(rmdir(targetdir), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static +void test_add_inotify_dir_watch_IN_DELETE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + __attribute__((cleanup(cleanup_string))) + char *tempfile = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(tempfile); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + g_assert_cmpint(unlink(tempfile), ==, 0); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + ssize_t read_size = 0; + read_size = read(added_read_task->fd, ievent, ievent_size); + + g_assert_cmpint((int)read_size, >, 0); + g_assert_true(ievent->mask & IN_DELETE); + g_assert_cmpstr(ievent->name, ==, basename(tempfile)); + + free(ievent); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static +void test_add_inotify_dir_watch_IN_EXCL_UNLINK(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + __attribute__((cleanup(cleanup_string))) + char *tempfile = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(tempfile); + int tempfile_fd = open(tempfile, O_WRONLY | O_CLOEXEC | O_NOCTTY + | O_NOFOLLOW); + g_assert_cmpint(tempfile_fd, >, 2); + + g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now, + &password, tempdir, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read)); + g_assert_cmpint(unlink(tempfile), ==, 0); + + g_assert_cmpuint((unsigned int)queue->length, >, 0); + + const task_context *const added_read_task + = find_matching_task(queue, + (task_context){ .func=read_inotify_event }); + g_assert_nonnull(added_read_task); + + g_assert_cmpint(added_read_task->fd, >, 2); + g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd)); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + struct inotify_event *ievent = malloc(ievent_size); + g_assert_nonnull(ievent); + + ssize_t read_size = 0; + read_size = read(added_read_task->fd, ievent, ievent_size); + + g_assert_cmpint((int)read_size, >, 0); + g_assert_true(ievent->mask & IN_DELETE); + g_assert_cmpstr(ievent->name, ==, basename(tempfile)); + + g_assert_cmpint(close(tempfile_fd), ==, 0); + + /* IN_EXCL_UNLINK should make the closing of the previously unlinked + file not appear as an ievent, so we should not see it now. */ + read_size = read(added_read_task->fd, ievent, ievent_size); + g_assert_cmpint((int)read_size, ==, -1); + g_assert_true(errno == EAGAIN); + + free(ievent); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void test_read_inotify_event_readerror(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + /* Reading /proc/self/mem from offset 0 will always result in EIO */ + const int fd = open("/proc/self/mem", + O_RDONLY | O_CLOEXEC | O_NOCTTY); + + bool quit_now = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=fd, + .quit_now=&quit_now, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + }; + g_assert_nonnull(task.filename); + run_task_with_stderr_to_dev_null(task, queue); + g_assert_true(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, fd)); + + g_assert_cmpint(close(fd), ==, -1); +} + +static void test_read_inotify_event_bad_epoll(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + const mono_microsecs current_time = 17; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + const int epoll_fd = pipefds[0]; /* This will obviously fail */ + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + g_assert_nonnull(task.filename); + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_nonnull(find_matching_task(queue, task)); + g_assert_true(queue->next_run == 1000000 + current_time); + + g_assert_cmpint(close(pipefds[0]), ==, 0); + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static void test_read_inotify_event_nodata(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + g_assert_nonnull(task.filename); + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpint(close(pipefds[1]), ==, 0); +} + +static void test_read_inotify_event_eof(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_true(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, pipefds[0])); + + g_assert_cmpint(close(pipefds[0]), ==, -1); +} + +static +void test_read_inotify_event_IN_CLOSE_WRITE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + struct { + struct inotify_event event; + char name_buffer[NAME_MAX + 1]; + } ievent_buffer; + struct inotify_event *const ievent = &ievent_buffer.event; + + const char dummy_file_name[] = "ask.dummy_file_name"; + ievent->mask = IN_CLOSE_WRITE; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run != 0); + g_assert_cmpuint((unsigned int)queue->length, >=, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpuint((unsigned int)queue->length, >=, 2); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .filename=filename, + .question_filename=filename, + .password=&password, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); +} + +static +void test_read_inotify_event_IN_MOVED_TO(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + struct { + struct inotify_event event; + char name_buffer[NAME_MAX + 1]; + } ievent_buffer; + struct inotify_event *const ievent = &ievent_buffer.event; + + const char dummy_file_name[] = "ask.dummy_file_name"; + ievent->mask = IN_MOVED_TO; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run != 0); + g_assert_cmpuint((unsigned int)queue->length, >=, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + g_assert_cmpuint((unsigned int)queue->length, >=, 2); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .filename=filename, + .question_filename=filename, + .password=&password, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); +} + +static +void test_read_inotify_event_IN_MOVED_FROM(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + struct { + struct inotify_event event; + char name_buffer[NAME_MAX + 1]; + } ievent_buffer; + struct inotify_event *const ievent = &ievent_buffer.event; + + const char dummy_file_name[] = "ask.dummy_file_name"; + ievent->mask = IN_MOVED_FROM; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_true(string_set_contains(*task.cancelled_filenames, + filename)); +} + +static void test_read_inotify_event_IN_DELETE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + struct { + struct inotify_event event; + char name_buffer[NAME_MAX + 1]; + } ievent_buffer; + struct inotify_event *const ievent = &ievent_buffer.event; + + const char dummy_file_name[] = "ask.dummy_file_name"; + ievent->mask = IN_DELETE; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_true(string_set_contains(*task.cancelled_filenames, + filename)); +} + +static void +test_read_inotify_event_IN_CLOSE_WRITE_badname(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + struct { + struct inotify_event event; + char name_buffer[NAME_MAX + 1]; + } ievent_buffer; + struct inotify_event *const ievent = &ievent_buffer.event; + + const char dummy_file_name[] = "ignored.dummy_file_name"; + ievent->mask = IN_CLOSE_WRITE; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); +} + +static void +test_read_inotify_event_IN_MOVED_TO_badname(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + struct { + struct inotify_event event; + char name_buffer[NAME_MAX + 1]; + } ievent_buffer; + struct inotify_event *const ievent = &ievent_buffer.event; + + const char dummy_file_name[] = "ignored.dummy_file_name"; + ievent->mask = IN_MOVED_TO; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames = &(string_set){}, + .notafter=0, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=task.cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); +} + +static void +test_read_inotify_event_IN_MOVED_FROM_badname(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + struct { + struct inotify_event event; + char name_buffer[NAME_MAX + 1]; + } ievent_buffer; + struct inotify_event *const ievent = &ievent_buffer.event; + + const char dummy_file_name[] = "ignored.dummy_file_name"; + ievent->mask = IN_MOVED_FROM; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_false(string_set_contains(cancelled_filenames, filename)); +} + +static +void test_read_inotify_event_IN_DELETE_badname(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + + /* "sufficient to read at least one event." - inotify(7) */ + const size_t ievent_max_size = (sizeof(struct inotify_event) + + NAME_MAX + 1); + g_assert_cmpint(ievent_max_size, <=, PIPE_BUF); + struct { + struct inotify_event event; + char name_buffer[NAME_MAX + 1]; + } ievent_buffer; + struct inotify_event *const ievent = &ievent_buffer.event; + + const char dummy_file_name[] = "ignored.dummy_file_name"; + ievent->mask = IN_DELETE; + ievent->len = sizeof(dummy_file_name); + memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name)); + const size_t ievent_size = (sizeof(struct inotify_event) + + sizeof(dummy_file_name)); + g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size), + ==, ievent_size); + g_assert_cmpint(close(pipefds[1]), ==, 0); + + bool quit_now = false; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + task_context task = { + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=strdup("/nonexistent"), + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_false(quit_now); + g_assert_true(queue->next_run == 0); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=read_inotify_event, + .epoll_fd=epoll_fd, + .fd=pipefds[0], + .quit_now=&quit_now, + .password=&password, + .filename=task.filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(epoll_set_contains(epoll_fd, pipefds[0], + EPOLLIN | EPOLLRDHUP)); + + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename, + dummy_file_name), >, 0); + g_assert_nonnull(filename); + g_assert_false(string_set_contains(cancelled_filenames, filename)); +} + +static +void test_open_and_parse_question_ENOENT(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + char *const filename = strdup("/nonexistent"); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static void test_open_and_parse_question_EIO(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const mono_microsecs current_time = 0; + + char *filename = strdup("/proc/self/mem"); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static void +test_open_and_parse_question_parse_error(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int tempfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(tempfile, >, 0); + const char bad_data[] = "this is bad syntax\n"; + g_assert_cmpint(write(tempfile, bad_data, sizeof(bad_data)), + ==, sizeof(bad_data)); + g_assert_cmpint(close(tempfile), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void test_open_and_parse_question_nosocket(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nPID=1\n"), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void test_open_and_parse_question_badsocket(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=\nPID=1\n"), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void test_open_and_parse_question_nopid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\n"), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void test_open_and_parse_question_badpid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=\n"), + >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=(mono_microsecs[]){0}, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static void +test_open_and_parse_question_noexist_pid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const mono_microsecs current_time = 0; + + /* Find value of sysctl kernel.pid_max */ + uintmax_t pid_max = 0; + FILE *sysctl_pid_max = fopen("/proc/sys/kernel/pid_max", "r"); + g_assert_nonnull(sysctl_pid_max); + g_assert_cmpint(fscanf(sysctl_pid_max, "%" PRIuMAX, &pid_max), + ==, 1); + g_assert_cmpint(fclose(sysctl_pid_max), ==, 0); + + pid_t nonexisting_pid = ((pid_t)pid_max)+1; + g_assert_true(nonexisting_pid > 0); /* Overflow check */ + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%" + PRIuMAX"\n", (uintmax_t)nonexisting_pid), + >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const question_filename = strdup(tempfilename); + g_assert_nonnull(question_filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=question_filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=question_filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static void +test_open_and_parse_question_no_notafter(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const mono_microsecs current_time = 0; + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%" + PRIuMAX "\n", (uintmax_t)getpid()), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + __attribute__((cleanup(cleanup_string))) + char *socket_filename = strdup("/nonexistent"); + g_assert_nonnull(socket_filename); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=connect_question_socket, + .question_filename=tempfilename, + .filename=socket_filename, + .epoll_fd=epoll_fd, + .password=&password, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(queue->next_run != 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static void +test_open_and_parse_question_bad_notafter(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + const mono_microsecs current_time = 0; + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%" + PRIuMAX "\nNotAfter=\n", + (uintmax_t)getpid()), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + run_task_with_stderr_to_dev_null(task, queue); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + __attribute__((cleanup(cleanup_string))) + char *socket_filename = strdup("/nonexistent"); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=connect_question_socket, + .question_filename=tempfilename, + .filename=socket_filename, + .epoll_fd=epoll_fd, + .password=&password, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + g_assert_true(queue->next_run != 0); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static +void assert_open_and_parse_question_with_notafter(const mono_microsecs + current_time, + const mono_microsecs + notafter, + const mono_microsecs + next_queue_run){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + queue->next_run = next_queue_run; + + __attribute__((cleanup(cleanup_string))) + char *tempfilename = strdup("/tmp/mandosXXXXXX"); + g_assert_nonnull(tempfilename); + int questionfile = mkostemp(tempfilename, O_CLOEXEC); + g_assert_cmpint(questionfile, >, 0); + FILE *qf = fdopen(questionfile, "w"); + g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%" + PRIuMAX "\nNotAfter=%" PRIuMAX "\n", + (uintmax_t)getpid(), notafter), >, 0); + g_assert_cmpint(fclose(qf), ==, 0); + + char *const filename = strdup(tempfilename); + g_assert_nonnull(filename); + task_context task = { + .func=open_and_parse_question, + .question_filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }; + task.func(task, queue); + + if(queue->length >= 1){ + __attribute__((cleanup(cleanup_string))) + char *socket_filename = strdup("/nonexistent"); + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=connect_question_socket, + .filename=socket_filename, + .epoll_fd=epoll_fd, + .password=&password, + .current_time=¤t_time, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + g_assert_true(queue->next_run != 0); + } + + if(notafter == 0){ + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + } else if(current_time >= notafter) { + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + } else { + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=cancel_old_question, + .question_filename=tempfilename, + .filename=tempfilename, + .notafter=notafter, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + })); + } + g_assert_true(queue->next_run == 1); + + g_assert_cmpint(unlink(tempfilename), ==, 0); +} + +static void +test_open_and_parse_question_notafter_0(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(0, 0, 0); +} + +static void +test_open_and_parse_question_notafter_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(0, 1, 0); +} + +static void +test_open_and_parse_question_notafter_1_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(0, 1, 1); +} + +static void +test_open_and_parse_question_notafter_1_2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(0, 1, 2); +} + +static void +test_open_and_parse_question_equal_notafter(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(1, 1, 0); +} + +static void +test_open_and_parse_question_late_notafter(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* current_time, notafter, next_queue_run */ + assert_open_and_parse_question_with_notafter(2, 1, 0); +} + +static void assert_cancel_old_question_param(const mono_microsecs + next_queue_run, + const mono_microsecs + notafter, + const mono_microsecs + current_time, + const mono_microsecs + next_set_to){ + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + queue->next_run = next_queue_run; + + char *const question_filename = strdup("/nonexistent"); + g_assert_nonnull(question_filename); + task_context task = { + .func=cancel_old_question, + .question_filename=question_filename, + .filename=question_filename, + .notafter=notafter, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + }; + task.func(task, queue); + + if(current_time >= notafter){ + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(string_set_contains(cancelled_filenames, + "/nonexistent")); + } else { + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=cancel_old_question, + .question_filename=question_filename, + .filename=question_filename, + .notafter=notafter, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + })); + + g_assert_false(string_set_contains(cancelled_filenames, + question_filename)); + } + g_assert_cmpuint((unsigned int)queue->next_run, ==, + (unsigned int)next_set_to); +} + +static void test_cancel_old_question_0_1_2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* next_queue_run unset, + cancellation should happen because time has come, + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(0, 1, 2, 0); +} + +static void test_cancel_old_question_0_2_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* If next_queue_run is 0, meaning unset, and notafter is 2, + and current_time is not yet notafter or greater, + update value of next_queue_run to value of notafter */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(0, 2, 1, 2); +} + +static void test_cancel_old_question_1_2_3(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* next_queue_run 1, + cancellation should happen because time has come, + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(1, 2, 3, 1); +} + +static void test_cancel_old_question_1_3_2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* If next_queue_run is set, + and current_time is not yet notafter or greater, + and notafter is larger than next_queue_run + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(1, 3, 2, 1); +} + +static void test_cancel_old_question_2_1_3(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* next_queue_run 2, + cancellation should happen because time has come, + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(2, 1, 3, 2); +} + +static void test_cancel_old_question_2_3_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* If next_queue_run is set, + and current_time is not yet notafter or greater, + and notafter is larger than next_queue_run + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(2, 3, 1, 2); +} + +static void test_cancel_old_question_3_1_2(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* next_queue_run 3, + cancellation should happen because time has come, + next_queue_run should be unchanged */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(3, 1, 2, 3); +} + +static void test_cancel_old_question_3_2_1(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* If next_queue_run is set, + and current_time is not yet notafter or greater, + and notafter is smaller than next_queue_run + update value of next_queue_run to value of notafter */ + /* next_queue_run, notafter, current_time, next_set_to */ + assert_cancel_old_question_param(3, 2, 1, 2); +} + +static void +test_connect_question_socket_name_too_long(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const char question_filename[] = "/nonexistent/question"; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + struct sockaddr_un unix_socket = { .sun_family=AF_LOCAL }; + char socket_name[sizeof(unix_socket.sun_path)]; + memset(socket_name, 'x', sizeof(socket_name)); + socket_name[sizeof(socket_name)-1] = '\0'; + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name), + >, 0); + g_assert_nonnull(filename); + + task_context task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_true(string_set_contains(cancelled_filenames, + question_filename)); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(queue->next_run == 0); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static +void test_connect_question_socket_connect_fail(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const char question_filename[] = "/nonexistent/question"; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 3; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + char socket_name[] = "nonexistent"; + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name), + >, 0); + g_assert_nonnull(filename); + + task_context task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=filename, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + .current_time=¤t_time, + }; + g_assert_nonnull(task.question_filename); + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_nonnull(find_matching_task(queue, task)); + + g_assert_false(string_set_contains(cancelled_filenames, + question_filename)); + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + g_assert_true(queue->next_run == 1000000 + current_time); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static +void test_connect_question_socket_bad_epoll(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC | O_NOCTTY); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 5; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_close))) + const int sock_fd = socket(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + g_assert_cmpint(sock_fd, >=, 0); + struct sockaddr_un sock_name = { .sun_family=AF_LOCAL }; + const char socket_name[] = "socket_name"; + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name), + >, 0); + g_assert_nonnull(filename); + g_assert_cmpint((int)strlen(filename), <, + (int)sizeof(sock_name.sun_path)); + strncpy(sock_name.sun_path, filename, sizeof(sock_name.sun_path)); + sock_name.sun_path[sizeof(sock_name.sun_path)-1] = '\0'; + g_assert_cmpint((int)bind(sock_fd, (struct sockaddr *)&sock_name, + (socklen_t)SUN_LEN(&sock_name)), >=, 0); + task_context task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=(buffer[]){{}}, + .filename=strdup(filename), + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + .current_time=¤t_time, + }; + g_assert_nonnull(task.question_filename); + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + const task_context *const added_task + = find_matching_task(queue, task); + g_assert_nonnull(added_task); + g_assert_true(queue->next_run == 1000000 + current_time); + + g_assert_cmpint(unlink(filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static +void test_connect_question_socket_usable(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool mandos_client_exited = false; + bool password_is_read = false; + const mono_microsecs current_time = 0; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_close))) + const int sock_fd = socket(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + g_assert_cmpint(sock_fd, >=, 0); + struct sockaddr_un sock_name = { .sun_family=AF_LOCAL }; + const char socket_name[] = "socket_name"; + __attribute__((cleanup(cleanup_string))) + char *filename = NULL; + g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name), + >, 0); + g_assert_nonnull(filename); + g_assert_cmpint((int)strlen(filename), <, + (int)sizeof(sock_name.sun_path)); + strncpy(sock_name.sun_path, filename, sizeof(sock_name.sun_path)); + sock_name.sun_path[sizeof(sock_name.sun_path)-1] = '\0'; + g_assert_cmpint((int)bind(sock_fd, (struct sockaddr *)&sock_name, + (socklen_t)SUN_LEN(&sock_name)), >=, 0); + task_context task = { + .func=connect_question_socket, + .question_filename=strdup(question_filename), + .epoll_fd=epoll_fd, + .password=&password, + .filename=strdup(filename), + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + .current_time=¤t_time, + }; + g_assert_nonnull(task.question_filename); + task.func(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + const task_context *const added_task + = find_matching_task(queue, (task_context){ + .func=send_password_to_socket, + .question_filename=question_filename, + .filename=filename, + .epoll_fd=epoll_fd, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + .current_time=¤t_time, + }); + g_assert_nonnull(added_task); + g_assert_cmpint(added_task->fd, >, 0); + + g_assert_true(epoll_set_contains(epoll_fd, added_task->fd, + EPOLLOUT)); + + const int fd = added_task->fd; + g_assert_cmpint(fd, >, 0); + g_assert_true(fd_has_cloexec_and_nonblock(fd)); + + /* write to fd */ + char write_data[PIPE_BUF]; + { + /* Construct test password buffer */ + /* Start with + since that is what the real procotol uses */ + write_data[0] = '+'; + /* Set a special character at string end just to mark the end */ + write_data[sizeof(write_data)-2] = 'y'; + /* Set NUL at buffer end, as suggested by the protocol */ + write_data[sizeof(write_data)-1] = '\0'; + /* Fill rest of password with 'x' */ + memset(write_data+1, 'x', sizeof(write_data)-3); + g_assert_cmpint((int)send(fd, write_data, sizeof(write_data), + MSG_NOSIGNAL), ==, sizeof(write_data)); + } + + /* read from sock_fd */ + char read_data[sizeof(write_data)]; + g_assert_cmpint((int)read(sock_fd, read_data, sizeof(read_data)), + ==, sizeof(read_data)); + + g_assert_true(memcmp(write_data, read_data, sizeof(write_data)) + == 0); + + /* writing to sock_fd should fail */ + g_assert_cmpint(send(sock_fd, write_data, sizeof(write_data), + MSG_NOSIGNAL), <, 0); + + /* reading from fd should fail */ + g_assert_cmpint((int)recv(fd, read_data, sizeof(read_data), + MSG_NOSIGNAL), <, 0); + + g_assert_cmpint(unlink(filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void +test_send_password_to_socket_client_not_exited(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(cleanup_string))) + char *const filename = strdup("/nonexistent/socket"); + g_assert_nonnull(filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + bool password_is_read = true; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=strdup(filename), + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=&password_is_read, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + + task.func(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + const task_context *const added_task + = find_matching_task(queue, task); + g_assert_nonnull(added_task); + g_assert_cmpuint((unsigned int)password.length, ==, 0); + g_assert_true(password_is_read); + + g_assert_cmpint(added_task->fd, >, 0); + g_assert_true(epoll_set_contains(epoll_fd, added_task->fd, + EPOLLOUT)); +} + +static void +test_send_password_to_socket_password_not_read(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(cleanup_string))) + char *const filename = strdup("/nonexistent/socket"); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + buffer password = {}; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=strdup(filename), + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){false}, + .password_is_read=(bool[]){false}, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + + task.func(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + const task_context *const added_task = find_matching_task(queue, + task); + g_assert_nonnull(added_task); + g_assert_cmpuint((unsigned int)password.length, ==, 0); + g_assert_true(queue->next_run == 0); + + g_assert_cmpint(added_task->fd, >, 0); + g_assert_true(epoll_set_contains(epoll_fd, added_task->fd, + EPOLLOUT)); +} + +static +void test_send_password_to_socket_EMSGSIZE(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + const char question_filename[] = "/nonexistent/question"; + char *const filename = strdup("/nonexistent/socket"); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const size_t oversized = 1024*1024; /* Limit seems to be 212960 */ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(oversized), + .length=oversized, + .allocated=oversized, + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + /* Construct test password buffer */ + /* Start with + since that is what the real procotol uses */ + password.data[0] = '+'; + /* Set a special character at string end just to mark the end */ + password.data[oversized-3] = 'y'; + /* Set NUL at buffer end, as suggested by the protocol */ + password.data[oversized-2] = '\0'; + /* Fill rest of password with 'x' */ + memset(password.data+1, 'x', oversized-3); + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + __attribute__((cleanup(cleanup_close))) + const int write_socket = socketfds[1]; + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=filename, + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){true}, + .password_is_read=(bool[]){true}, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + g_assert_true(string_set_contains(cancelled_filenames, + question_filename)); +} + +static void test_send_password_to_socket_retry(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(cleanup_string))) + char *const filename = strdup("/nonexistent/socket"); + g_assert_nonnull(filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + /* Close the server side socket to force ECONNRESET on client */ + g_assert_cmpint(close(read_socket), ==, 0); + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=strdup(filename), + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){true}, + .password_is_read=(bool[]){true}, + .current_time=(mono_microsecs[]){0}, + }; + g_assert_nonnull(task.question_filename); + + task.func(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + const task_context *const added_task = find_matching_task(queue, + task); + g_assert_nonnull(added_task); + g_assert_cmpuint((unsigned int)password.length, ==, 0); + + g_assert_true(epoll_set_contains(epoll_fd, added_task->fd, + EPOLLOUT)); +} + +static +void test_send_password_to_socket_bad_epoll(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC | O_NOCTTY); + __attribute__((cleanup(cleanup_string))) + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + __attribute__((cleanup(cleanup_string))) + char *const filename = strdup("/nonexistent/socket"); + g_assert_nonnull(filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + + const mono_microsecs current_time = 11; + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + /* Close the server side socket to force ECONNRESET on client */ + g_assert_cmpint(close(read_socket), ==, 0); + task_context task = { + .func=send_password_to_socket, + .question_filename=strdup(question_filename), + .filename=strdup(filename), + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){true}, + .password_is_read=(bool[]){true}, + .current_time=¤t_time, + }; + g_assert_nonnull(task.question_filename); + + run_task_with_stderr_to_dev_null(task, queue); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + const task_context *const added_task = find_matching_task(queue, + task); + g_assert_nonnull(added_task); + g_assert_true(queue->next_run == current_time + 1000000); + g_assert_cmpuint((unsigned int)password.length, ==, 0); +} + +static void assert_send_password_to_socket_password(buffer password){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + char *const question_filename = strdup("/nonexistent/question"); + g_assert_nonnull(question_filename); + char *const filename = strdup("/nonexistent/socket"); + g_assert_nonnull(filename); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + int socketfds[2]; + g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM + | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socketfds), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_socket = socketfds[0]; + const int write_socket = socketfds[1]; + task_context task = { + .func=send_password_to_socket, + .question_filename=question_filename, + .filename=filename, + .epoll_fd=epoll_fd, + .fd=write_socket, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .mandos_client_exited=(bool[]){true}, + .password_is_read=(bool[]){true}, + .current_time=(mono_microsecs[]){0}, + }; + + char *expected_written_data = malloc(password.length + 2); + g_assert_nonnull(expected_written_data); + expected_written_data[0] = '+'; + expected_written_data[password.length + 1] = '\0'; + if(password.length > 0){ + g_assert_nonnull(password.data); + memcpy(expected_written_data + 1, password.data, password.length); + } + + task.func(task, queue); + + char buf[PIPE_BUF]; + g_assert_cmpint((int)read(read_socket, buf, PIPE_BUF), ==, + (int)(password.length + 2)); + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_true(memcmp(expected_written_data, buf, + password.length + 2) == 0); + + g_assert_true(epoll_set_does_not_contain(epoll_fd, write_socket)); + + free(expected_written_data); +} + +static void +test_send_password_to_socket_null_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_empty_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(1), /* because malloc(0) may return NULL */ + .length=0, + .allocated=0, /* deliberate lie */ + }; + g_assert_nonnull(password.data); + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_empty_str_pass(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=strdup(""), + .length=0, + .allocated=1, + }; + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_text_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + const char dummy_test_password[] = "dummy test password"; + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data = strdup(dummy_test_password), + .length = strlen(dummy_test_password), + .allocated = sizeof(dummy_test_password), + }; + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_binary_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(255), + .length=255, + .allocated=255, + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) != 0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + char c = 1; /* Start at 1, avoiding NUL */ + for(int i=0; i < 255; i++){ + password.data[i] = c++; + } + assert_send_password_to_socket_password(password); +} + +static void +test_send_password_to_socket_nuls_in_password(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + char test_password[] = {'\0', 'a', '\0', 'b', '\0', 'c', '\0'}; + __attribute__((cleanup(cleanup_buffer))) + buffer password = { + .data=malloc(sizeof(test_password)), + .length=sizeof(test_password), + .allocated=sizeof(test_password), + }; + g_assert_nonnull(password.data); + if(mlock(password.data, password.allocated) !=0){ + g_assert_true(errno == EPERM or errno == ENOMEM); + } + memcpy(password.data, test_password, password.allocated); + assert_send_password_to_socket_password(password); +} + +static bool assert_add_existing_questions_to_devnull(task_queue + *const, + const int, + buffer *const, + string_set *, + const + mono_microsecs + *const, + bool *const, + bool *const, + const char + *const); + +static void test_add_existing_questions_ENOENT(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + + g_assert_false(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + (buffer[]){{}}, /* password */ + &cancelled_filenames, + (mono_microsecs[]){0}, /* current_time */ + (bool[]){false}, /* mandos_client_exited */ + (bool[]){false}, /* password_is_read */ + "/nonexistent")); /* dirname */ + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); +} + +static +bool assert_add_existing_questions_to_devnull(task_queue + *const queue, + const int + epoll_fd, + buffer *const + password, + string_set + *cancelled_filenames, + const mono_microsecs + *const current_time, + bool *const + mandos_client_exited, + bool *const + password_is_read, + const char *const + dirname){ + __attribute__((cleanup(cleanup_close))) + const int devnull_fd = open("/dev/null", + O_WRONLY | O_CLOEXEC | O_NOCTTY); + g_assert_cmpint(devnull_fd, >=, 0); + __attribute__((cleanup(cleanup_close))) + const int real_stderr_fd = dup(STDERR_FILENO); + g_assert_cmpint(real_stderr_fd, >=, 0); + dup2(devnull_fd, STDERR_FILENO); + const bool ret = add_existing_questions(queue, epoll_fd, password, + cancelled_filenames, + current_time, + mandos_client_exited, + password_is_read, dirname); + dup2(real_stderr_fd, STDERR_FILENO); + return ret; +} + +static +void test_add_existing_questions_no_questions(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + + g_assert_false(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + (buffer[]){{}}, /* password */ + &cancelled_filenames, + (mono_microsecs[]){0}, /* current_time */ + (bool[]){false}, /* mandos_client_exited */ + (bool[]){false}, /* password_is_read */ + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_question_file_in_directory(const char *const); + +static +void test_add_existing_questions_one_question(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_string))) + char *question_filename + = make_question_file_in_directory(tempdir); + g_assert_nonnull(question_filename); + + g_assert_true(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + &password, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read, + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .filename=question_filename, + .question_filename=question_filename, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(queue->next_run == 1); + + g_assert_cmpint(unlink(question_filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static char *make_question_file_in_directory(const char + *const dir){ + return make_temporary_prefixed_file_in_directory("ask.", dir); +} + +static +void test_add_existing_questions_two_questions(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_string))) + char *question_filename1 + = make_question_file_in_directory(tempdir); + g_assert_nonnull(question_filename1); + __attribute__((cleanup(cleanup_string))) + char *question_filename2 + = make_question_file_in_directory(tempdir); + g_assert_nonnull(question_filename2); + + g_assert_true(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + &password, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read, + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 2); + + g_assert_true(queue->next_run == 1); + + __attribute__((cleanup(string_set_clear))) + string_set seen_questions = {}; + + bool queue_contains_question_opener(char *const question_filename){ + return(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .question_filename=question_filename, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + }) != NULL); + } + + g_assert_true(queue_contains_question_opener(question_filename1)); + g_assert_true(queue_contains_question_opener(question_filename2)); + + g_assert_true(queue->next_run == 1); + + g_assert_cmpint(unlink(question_filename1), ==, 0); + g_assert_cmpint(unlink(question_filename2), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void +test_add_existing_questions_non_questions(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_string))) + char *question_filename1 + = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(question_filename1); + __attribute__((cleanup(cleanup_string))) + char *question_filename2 + = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(question_filename2); + + g_assert_false(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + (buffer[]){{}}, /* password */ + &cancelled_filenames, + (mono_microsecs[]){0}, /* current_time */ + (bool[]){false}, /* mandos_client_exited */ + (bool[]){false}, /* password_is_read */ + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 0); + + g_assert_cmpint(unlink(question_filename1), ==, 0); + g_assert_cmpint(unlink(question_filename2), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void +test_add_existing_questions_both_types(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + __attribute__((cleanup(cleanup_buffer))) + buffer password = {}; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + const mono_microsecs current_time = 0; + bool mandos_client_exited = false; + bool password_is_read = false; + __attribute__((cleanup(cleanup_string))) + char *tempdir = make_temporary_directory(); + g_assert_nonnull(tempdir); + __attribute__((cleanup(cleanup_string))) + char *tempfilename1 = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(tempfilename1); + __attribute__((cleanup(cleanup_string))) + char *tempfilename2 = make_temporary_file_in_directory(tempdir); + g_assert_nonnull(tempfilename2); + __attribute__((cleanup(cleanup_string))) + char *question_filename + = make_question_file_in_directory(tempdir); + g_assert_nonnull(question_filename); + + g_assert_true(assert_add_existing_questions_to_devnull + (queue, + epoll_fd, + &password, + &cancelled_filenames, + ¤t_time, + &mandos_client_exited, + &password_is_read, + tempdir)); + + g_assert_cmpuint((unsigned int)queue->length, ==, 1); + + g_assert_nonnull(find_matching_task(queue, (task_context){ + .func=open_and_parse_question, + .epoll_fd=epoll_fd, + .filename=question_filename, + .question_filename=question_filename, + .password=&password, + .cancelled_filenames=&cancelled_filenames, + .current_time=¤t_time, + .mandos_client_exited=&mandos_client_exited, + .password_is_read=&password_is_read, + })); + + g_assert_true(queue->next_run == 1); + + g_assert_cmpint(unlink(tempfilename1), ==, 0); + g_assert_cmpint(unlink(tempfilename2), ==, 0); + g_assert_cmpint(unlink(question_filename), ==, 0); + g_assert_cmpint(rmdir(tempdir), ==, 0); +} + +static void test_wait_for_event_timeout(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + g_assert_true(wait_for_event(epoll_fd, 1, 0)); +} + +static void test_wait_for_event_event(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_pipe = pipefds[0]; + __attribute__((cleanup(cleanup_close))) + const int write_pipe = pipefds[1]; + g_assert_cmpint(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, read_pipe, + &(struct epoll_event) + { .events=EPOLLIN | EPOLLRDHUP }), ==, 0); + g_assert_cmpint((int)write(write_pipe, "x", 1), ==, 1); + + g_assert_true(wait_for_event(epoll_fd, 0, 0)); +} + +static void test_wait_for_event_sigchld(test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + const pid_t pid = fork(); + if(pid == 0){ /* Child */ + if(not restore_signal_handler(&fixture->orig_sigaction)){ + _exit(EXIT_FAILURE); + } + if(not restore_sigmask(&fixture->orig_sigmask)){ + _exit(EXIT_FAILURE); + } + exit(EXIT_SUCCESS); + } + g_assert_true(pid != -1); + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + g_assert_cmpint(epoll_fd, >=, 0); + + g_assert_true(wait_for_event(epoll_fd, 0, 0)); + + int status; + g_assert_true(waitpid(pid, &status, 0) == pid); + g_assert_true(WIFEXITED(status)); + g_assert_cmpint(WEXITSTATUS(status), ==, EXIT_SUCCESS); +} + +static void test_run_queue_zeroes_next_run(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + queue->next_run = 1; + __attribute__((cleanup(cleanup_close))) + const int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)queue->next_run, ==, 0); +} + +static +void test_run_queue_clears_cancelled_filenames(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + const char question_filename[] = "/nonexistent/question_filename"; + g_assert_true(string_set_add(&cancelled_filenames, + question_filename)); + + g_assert_true(add_to_queue(queue, + (task_context){ .func=dummy_func })); + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); + g_assert_false(string_set_contains(cancelled_filenames, + question_filename)); +} + +static +void test_run_queue_skips_cancelled_filenames(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_pipe = pipefds[0]; + g_assert_cmpint(close(pipefds[1]), ==, 0); + const char question_filename[] = "/nonexistent/question_filename"; + g_assert_true(string_set_add(&cancelled_filenames, + question_filename)); + __attribute__((nonnull)) + void quit_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + g_assert_nonnull(task.quit_now); + *task.quit_now = true; + } + task_context task = { + .func=quit_func, + .question_filename=strdup(question_filename), + .quit_now=&quit_now, + .fd=read_pipe, + }; + g_assert_nonnull(task.question_filename); + + g_assert_true(add_to_queue(queue, task)); + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_false(quit_now); + + /* read_pipe should be closed already */ + errno = 0; + bool read_pipe_closed = (close(read_pipe) == -1); + read_pipe_closed &= (errno == EBADF); + g_assert_true(read_pipe_closed); +} + +static void test_run_queue_one_task(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + + __attribute__((nonnull)) + void next_run_func(__attribute__((unused)) + const task_context task, + task_queue *const q){ + q->next_run = 1; + } + + task_context task = { + .func=next_run_func, + }; + g_assert_true(add_to_queue(queue, task)); + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_cmpuint((unsigned int)(queue->next_run), ==, 1); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); +} + +static void test_run_queue_two_tasks(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + queue->next_run = 1; + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + bool mandos_client_exited = false; + + __attribute__((nonnull)) + void next_run_func(__attribute__((unused)) + const task_context task, + task_queue *const q){ + q->next_run = 1; + } + + __attribute__((nonnull)) + void exited_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.mandos_client_exited = true; + } + + task_context task1 = { + .func=next_run_func, + }; + g_assert_true(add_to_queue(queue, task1)); + + task_context task2 = { + .func=exited_func, + .mandos_client_exited=&mandos_client_exited, + }; + g_assert_true(add_to_queue(queue, task2)); + + g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_false(quit_now); + g_assert_cmpuint((unsigned int)(queue->next_run), ==, 1); + g_assert_true(mandos_client_exited); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); +} + +static void test_run_queue_two_tasks_quit(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + bool quit_now = false; + bool mandos_client_exited = false; + bool password_is_read = false; + + __attribute__((nonnull)) + void set_exited_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.mandos_client_exited = true; + *task.quit_now = true; + } + task_context task1 = { + .func=set_exited_func, + .quit_now=&quit_now, + .mandos_client_exited=&mandos_client_exited, + }; + g_assert_true(add_to_queue(queue, task1)); + + __attribute__((nonnull)) + void set_read_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.quit_now = true; + *task.password_is_read = true; + } + task_context task2 = { + .func=set_read_func, + .quit_now=&quit_now, + .password_is_read=&password_is_read, + }; + g_assert_true(add_to_queue(queue, task2)); + + g_assert_false(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(quit_now); + g_assert_true(mandos_client_exited xor password_is_read); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); +} + +static void test_run_queue_two_tasks_cleanup(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + __attribute__((cleanup(cleanup_queue))) + task_queue *queue = create_queue(); + g_assert_nonnull(queue); + __attribute__((cleanup(string_set_clear))) + string_set cancelled_filenames = {}; + int pipefds[2]; + g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0); + __attribute__((cleanup(cleanup_close))) + const int read_pipe = pipefds[0]; + __attribute__((cleanup(cleanup_close))) + const int write_pipe = pipefds[1]; + bool quit_now = false; + + __attribute__((nonnull)) + void read_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.quit_now = true; + } + task_context task1 = { + .func=read_func, + .quit_now=&quit_now, + .fd=read_pipe, + }; + g_assert_true(add_to_queue(queue, task1)); + + __attribute__((nonnull)) + void write_func(const task_context task, + __attribute__((unused)) task_queue *const q){ + *task.quit_now = true; + } + task_context task2 = { + .func=write_func, + .quit_now=&quit_now, + .fd=write_pipe, + }; + g_assert_true(add_to_queue(queue, task2)); + + g_assert_false(run_queue(&queue, &cancelled_filenames, &quit_now)); + g_assert_true(quit_now); + + /* Either read_pipe or write_pipe should be closed already */ + errno = 0; + bool close_read_pipe = (close(read_pipe) == -1); + close_read_pipe &= (errno == EBADF); + errno = 0; + bool close_write_pipe = (close(write_pipe) == -1); + close_write_pipe &= (errno == EBADF); + g_assert_true(close_read_pipe xor close_write_pipe); + g_assert_cmpuint((unsigned int)(queue->length), ==, 0); +} + +static void test_setup_signal_handler(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* Save current SIGCHLD action, whatever it is */ + struct sigaction expected_sigchld_action; + g_assert_cmpint(sigaction(SIGCHLD, NULL, &expected_sigchld_action), + ==, 0); + + /* Act; i.e. run the setup_signal_handler() function */ + struct sigaction actual_old_sigchld_action; + g_assert_true(setup_signal_handler(&actual_old_sigchld_action)); + + /* Check that the function correctly set "actual_old_sigchld_action" + to the same values as the previously saved + "expected_sigchld_action" */ + /* Check member sa_handler */ + g_assert_true(actual_old_sigchld_action.sa_handler + == expected_sigchld_action.sa_handler); + /* Check member sa_mask */ + for(int signum = 1; signum < NSIG; signum++){ + const int expected_old_block_state + = sigismember(&expected_sigchld_action.sa_mask, signum); + g_assert_cmpint(expected_old_block_state, >=, 0); + const int actual_old_block_state + = sigismember(&actual_old_sigchld_action.sa_mask, signum); + g_assert_cmpint(actual_old_block_state, >=, 0); + g_assert_cmpint(actual_old_block_state, + ==, expected_old_block_state); + } + /* Check member sa_flags */ + g_assert_true((actual_old_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART)) + == (expected_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART))); + + /* Retrieve the current signal handler for SIGCHLD as set by + setup_signal_handler() */ + struct sigaction actual_new_sigchld_action; + g_assert_cmpint(sigaction(SIGCHLD, NULL, + &actual_new_sigchld_action), ==, 0); + /* Check that the signal handler (member sa_handler) is correctly + set to the "handle_sigchld" function */ + g_assert_true(actual_new_sigchld_action.sa_handler != SIG_DFL); + g_assert_true(actual_new_sigchld_action.sa_handler != SIG_IGN); + g_assert_true(actual_new_sigchld_action.sa_handler + == handle_sigchld); + /* Check (in member sa_mask) that at least a handful of signals are + actually blocked during the signal handler */ + for(int signum = 1; signum < NSIG; signum++){ + int actual_new_block_state; + switch(signum){ + case SIGTERM: + case SIGINT: + case SIGQUIT: + case SIGHUP: + actual_new_block_state + = sigismember(&actual_new_sigchld_action.sa_mask, signum); + g_assert_cmpint(actual_new_block_state, ==, 1); + continue; + case SIGKILL: /* non-blockable */ + case SIGSTOP: /* non-blockable */ + case SIGCHLD: /* always blocked */ + default: + continue; + } + } + /* Check member sa_flags */ + g_assert_true((actual_new_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART)) + == (SA_NOCLDSTOP | SA_RESTART)); + + /* Restore signal handler */ + g_assert_cmpint(sigaction(SIGCHLD, &expected_sigchld_action, NULL), + ==, 0); +} + +static void test_restore_signal_handler(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* Save current SIGCHLD action, whatever it is */ + struct sigaction expected_sigchld_action; + g_assert_cmpint(sigaction(SIGCHLD, NULL, &expected_sigchld_action), + ==, 0); + /* Since we haven't established a signal handler yet, there should + not be one established. But another test may have relied on + restore_signal_handler() to restore the signal handler, and if + restore_signal_handler() is buggy (which we should be prepared + for in this test) the signal handler may not have been restored + properly; check for this: */ + g_assert_true(expected_sigchld_action.sa_handler != handle_sigchld); + + /* Establish a signal handler */ + struct sigaction sigchld_action = { + .sa_handler=handle_sigchld, + .sa_flags=SA_RESTART | SA_NOCLDSTOP, + }; + g_assert_cmpint(sigfillset(&sigchld_action.sa_mask), ==, 0); + g_assert_cmpint(sigaction(SIGCHLD, &sigchld_action, NULL), ==, 0); + + /* Act; i.e. run the restore_signal_handler() function */ + g_assert_true(restore_signal_handler(&expected_sigchld_action)); + + /* Retrieve the restored signal handler data */ + struct sigaction actual_restored_sigchld_action; + g_assert_cmpint(sigaction(SIGCHLD, NULL, + &actual_restored_sigchld_action), ==, 0); + + /* Check that the function correctly restored the signal action, as + saved in "actual_restored_sigchld_action", to the same values as + the previously saved "expected_sigchld_action" */ + /* Check member sa_handler */ + g_assert_true(actual_restored_sigchld_action.sa_handler + == expected_sigchld_action.sa_handler); + /* Check member sa_mask */ + for(int signum = 1; signum < NSIG; signum++){ + const int expected_old_block_state + = sigismember(&expected_sigchld_action.sa_mask, signum); + g_assert_cmpint(expected_old_block_state, >=, 0); + const int actual_restored_block_state + = sigismember(&actual_restored_sigchld_action.sa_mask, signum); + g_assert_cmpint(actual_restored_block_state, >=, 0); + g_assert_cmpint(actual_restored_block_state, + ==, expected_old_block_state); + } + /* Check member sa_flags */ + g_assert_true((actual_restored_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART)) + == (expected_sigchld_action.sa_flags + & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART))); +} + +static void test_block_sigchld(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* Save original signal mask */ + sigset_t expected_sigmask; + g_assert_cmpint(pthread_sigmask(-1, NULL, &expected_sigmask), + ==, 0); + + /* Make sure SIGCHLD is unblocked for this test */ + sigset_t sigchld_sigmask; + g_assert_cmpint(sigemptyset(&sigchld_sigmask), ==, 0); + g_assert_cmpint(sigaddset(&sigchld_sigmask, SIGCHLD), ==, 0); + g_assert_cmpint(pthread_sigmask(SIG_UNBLOCK, &sigchld_sigmask, + NULL), ==, 0); + + /* Act; i.e. run the block_sigchld() function */ + sigset_t actual_old_sigmask; + g_assert_true(block_sigchld(&actual_old_sigmask)); + + /* Check the actual_old_sigmask; it should be the same as the + previously saved signal mask "expected_sigmask". */ + for(int signum = 1; signum < NSIG; signum++){ + const int expected_old_block_state + = sigismember(&expected_sigmask, signum); + g_assert_cmpint(expected_old_block_state, >=, 0); + const int actual_old_block_state + = sigismember(&actual_old_sigmask, signum); + g_assert_cmpint(actual_old_block_state, >=, 0); + g_assert_cmpint(actual_old_block_state, + ==, expected_old_block_state); + } + + /* Retrieve the newly set signal mask */ + sigset_t actual_sigmask; + g_assert_cmpint(pthread_sigmask(-1, NULL, &actual_sigmask), ==, 0); + + /* SIGCHLD should be blocked */ + g_assert_cmpint(sigismember(&actual_sigmask, SIGCHLD), ==, 1); + + /* Restore signal mask */ + g_assert_cmpint(pthread_sigmask(SIG_SETMASK, &expected_sigmask, + NULL), ==, 0); +} + +static void test_restore_sigmask(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + /* Save original signal mask */ + sigset_t orig_sigmask; + g_assert_cmpint(pthread_sigmask(-1, NULL, &orig_sigmask), ==, 0); + + /* Make sure SIGCHLD is blocked for this test */ + sigset_t sigchld_sigmask; + g_assert_cmpint(sigemptyset(&sigchld_sigmask), ==, 0); + g_assert_cmpint(sigaddset(&sigchld_sigmask, SIGCHLD), ==, 0); + g_assert_cmpint(pthread_sigmask(SIG_BLOCK, &sigchld_sigmask, + NULL), ==, 0); + + /* Act; i.e. run the restore_sigmask() function */ + g_assert_true(restore_sigmask(&orig_sigmask)); + + /* Retrieve the newly restored signal mask */ + sigset_t restored_sigmask; + g_assert_cmpint(pthread_sigmask(-1, NULL, &restored_sigmask), + ==, 0); + + /* Check the restored_sigmask; it should be the same as the + previously saved signal mask "orig_sigmask". */ + for(int signum = 1; signum < NSIG; signum++){ + const int orig_block_state = sigismember(&orig_sigmask, signum); + g_assert_cmpint(orig_block_state, >=, 0); + const int restored_block_state = sigismember(&restored_sigmask, + signum); + g_assert_cmpint(restored_block_state, >=, 0); + g_assert_cmpint(restored_block_state, ==, orig_block_state); + } + + /* Restore signal mask */ + g_assert_cmpint(pthread_sigmask(SIG_SETMASK, &orig_sigmask, + NULL), ==, 0); +} + +static void test_parse_arguments_noargs(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + g_assert_null(agent_directory); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +__attribute__((nonnull)) +static bool parse_arguments_devnull(int argc, char *argv[], + const bool exit_failure, + char **agent_directory, + char **helper_directory, + uid_t *const user, + gid_t *const group, + char **mandos_argz, + size_t *mandos_argz_length){ + + FILE *real_stderr = stderr; + FILE *devnull = fopen("/dev/null", "we"); + g_assert_nonnull(devnull); + stderr = devnull; + + const bool ret = parse_arguments(argc, argv, exit_failure, + agent_directory, + helper_directory, user, group, + mandos_argz, mandos_argz_length); + const error_t saved_errno = errno; + + stderr = real_stderr; + g_assert_cmpint(fclose(devnull), ==, 0); + + errno = saved_errno; + + return ret; +} + +static void test_parse_arguments_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--invalid"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_true(errno == EINVAL); + g_assert_null(agent_directory); + g_assert_null(helper_directory); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_long_dir(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--agent-directory"), + strdup("/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(agent_directory, ==, "/tmp"); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_short_dir(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("-d"), + strdup("/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(agent_directory, ==, "/tmp"); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static +void test_parse_arguments_helper_directory(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--helper-directory"), + strdup("/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(helper_directory, ==, "/tmp"); + g_assert_null(agent_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static +void test_parse_arguments_plugin_helper_dir(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--plugin-helper-dir"), + strdup("/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(helper_directory, ==, "/tmp"); + g_assert_null(agent_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_user(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--user"), + strdup("1000"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_cmpuint((unsigned int)user, ==, 1000); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_user_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--user"), + strdup("invalid"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_cmpuint((unsigned int)user, ==, 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static +void test_parse_arguments_user_zero_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--user"), + strdup("0"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_cmpuint((unsigned int)user, ==, 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_group(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--group"), + strdup("1000"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_true(user == 0); + g_assert_cmpuint((unsigned int)group, ==, 1000); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_group_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--group"), + strdup("invalid"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static +void test_parse_arguments_group_zero_invalid(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--group"), + strdup("0"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_false(parse_arguments_devnull(argc, argv, false, + &agent_directory, + &helper_directory, &user, + &group, &mandos_argz, + &mandos_argz_length)); + + g_assert_null(helper_directory); + g_assert_null(agent_directory); + g_assert_cmpuint((unsigned int)group, ==, 0); + g_assert_true(group == 0); + g_assert_null(mandos_argz); + g_assert_true(mandos_argz_length == 0); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_mandos_noargs(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer + user_data){ + char *argv[] = { + strdup("prgname"), + strdup("mandos-client"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_null(agent_directory); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + g_assert_cmpstr(mandos_argz, ==, "mandos-client"); + g_assert_cmpuint((unsigned int)argz_count(mandos_argz, + mandos_argz_length), + ==, 1); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_mandos_args(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("mandos-client"), + strdup("one"), + strdup("two"), + strdup("three"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_null(agent_directory); + g_assert_null(helper_directory); + g_assert_true(user == 0); + g_assert_true(group == 0); + char *marg = mandos_argz; + g_assert_cmpstr(marg, ==, "mandos-client"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "one"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "two"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "three"); + g_assert_cmpuint((unsigned int)argz_count(mandos_argz, + mandos_argz_length), + ==, 4); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_all_args(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("--agent-directory"), + strdup("/tmp"), + strdup("--helper-directory"), + strdup("/var/tmp"), + strdup("--user"), + strdup("1"), + strdup("--group"), + strdup("2"), + strdup("mandos-client"), + strdup("one"), + strdup("two"), + strdup("three"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(agent_directory, ==, "/tmp"); + g_assert_cmpstr(helper_directory, ==, "/var/tmp"); + g_assert_true(user == 1); + g_assert_true(group == 2); + char *marg = mandos_argz; + g_assert_cmpstr(marg, ==, "mandos-client"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "one"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "two"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "three"); + g_assert_cmpuint((unsigned int)argz_count(mandos_argz, + mandos_argz_length), + ==, 4); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +static void test_parse_arguments_mixed(__attribute__((unused)) + test_fixture *fixture, + __attribute__((unused)) + gconstpointer user_data){ + char *argv[] = { + strdup("prgname"), + strdup("mandos-client"), + strdup("--user"), + strdup("1"), + strdup("one"), + strdup("--agent-directory"), + strdup("/tmp"), + strdup("two"), + strdup("three"), + strdup("--helper-directory=/var/tmp"), + NULL }; + const int argc = (sizeof(argv) / sizeof(char *)) - 1; + + __attribute__((cleanup(cleanup_string))) + char *agent_directory = NULL; + __attribute__((cleanup(cleanup_string))) + char *helper_directory = NULL; + uid_t user = 0; + gid_t group = 0; + __attribute__((cleanup(cleanup_string))) + char *mandos_argz = NULL; + size_t mandos_argz_length = 0; + + g_assert_true(parse_arguments(argc, argv, false, &agent_directory, + &helper_directory, &user, &group, + &mandos_argz, &mandos_argz_length)); + + g_assert_cmpstr(agent_directory, ==, "/tmp"); + g_assert_cmpstr(helper_directory, ==, "/var/tmp"); + g_assert_true(user == 1); + g_assert_true(group == 0); + char *marg = mandos_argz; + g_assert_cmpstr(marg, ==, "mandos-client"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "one"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "two"); + marg = argz_next(mandos_argz, mandos_argz_length, marg); + g_assert_cmpstr(marg, ==, "three"); + g_assert_cmpuint((unsigned int)argz_count(mandos_argz, + mandos_argz_length), + ==, 4); + + for(char **arg = argv; *arg != NULL; arg++){ + free(*arg); + } +} + +/* End of tests section */ + +/* Test boilerplate section; New tests should be added to the test + suite definition here, in the "run_tests" function. + + Finally, this section also contains the should_only_run_tests() + function used by main() for deciding if tests should be run or to + start normally. */ + +__attribute__((cold)) +static bool run_tests(int argc, char *argv[]){ + g_test_init(&argc, &argv, NULL); + + /* A macro to add a test with no setup or teardown functions */ +#define test_add(testpath, testfunc) \ + do { \ + g_test_add((testpath), test_fixture, NULL, NULL, \ + (testfunc), NULL); \ + } while(false) + + /* Test the signal-related functions first, since some other tests + depend on these functions in their setups and teardowns */ + test_add("/signal-handling/setup", test_setup_signal_handler); + test_add("/signal-handling/restore", test_restore_signal_handler); + test_add("/signal-handling/block", test_block_sigchld); + test_add("/signal-handling/restore-sigmask", test_restore_sigmask); + + /* Regular non-signal-related tests; these use no setups or + teardowns */ + test_add("/parse_arguments/noargs", test_parse_arguments_noargs); + test_add("/parse_arguments/invalid", test_parse_arguments_invalid); + test_add("/parse_arguments/long-dir", + test_parse_arguments_long_dir); + test_add("/parse_arguments/short-dir", + test_parse_arguments_short_dir); + test_add("/parse_arguments/helper-directory", + test_parse_arguments_helper_directory); + test_add("/parse_arguments/plugin-helper-dir", + test_parse_arguments_plugin_helper_dir); + test_add("/parse_arguments/user", test_parse_arguments_user); + test_add("/parse_arguments/user-invalid", + test_parse_arguments_user_invalid); + test_add("/parse_arguments/user-zero-invalid", + test_parse_arguments_user_zero_invalid); + test_add("/parse_arguments/group", test_parse_arguments_group); + test_add("/parse_arguments/group-invalid", + test_parse_arguments_group_invalid); + test_add("/parse_arguments/group-zero-invalid", + test_parse_arguments_group_zero_invalid); + test_add("/parse_arguments/mandos-noargs", + test_parse_arguments_mandos_noargs); + test_add("/parse_arguments/mandos-args", + test_parse_arguments_mandos_args); + test_add("/parse_arguments/all-args", + test_parse_arguments_all_args); + test_add("/parse_arguments/mixed", test_parse_arguments_mixed); + test_add("/queue/create", test_create_queue); + test_add("/queue/add", test_add_to_queue); + test_add("/queue/has_question/empty", + test_queue_has_question_empty); + test_add("/queue/has_question/false", + test_queue_has_question_false); + test_add("/queue/has_question/true", test_queue_has_question_true); + test_add("/queue/has_question/false2", + test_queue_has_question_false2); + test_add("/queue/has_question/true2", + test_queue_has_question_true2); + test_add("/buffer/cleanup", test_cleanup_buffer); + test_add("/string_set/net-set-contains-nothing", + test_string_set_new_set_contains_nothing); + test_add("/string_set/with-added-string-contains-it", + test_string_set_with_added_string_contains_it); + test_add("/string_set/cleared-does-not-contain-string", + test_string_set_cleared_does_not_contain_str); + test_add("/string_set/swap/one-with-empty", + test_string_set_swap_one_with_empty); + test_add("/string_set/swap/empty-with-one", + test_string_set_swap_empty_with_one); + test_add("/string_set/swap/one-with-one", + test_string_set_swap_one_with_one); + + /* A macro to add a test using the setup and teardown functions */ +#define test_add_st(path, func) \ + do { \ + g_test_add((path), test_fixture, NULL, test_setup, (func), \ + test_teardown); \ + } while(false) + + /* Signal-related tests; these use setups and teardowns which + establish, during each test run, a signal handler for, and a + signal mask blocking, the SIGCHLD signal, just like main() */ + test_add_st("/wait_for_event/timeout", test_wait_for_event_timeout); + test_add_st("/wait_for_event/event", test_wait_for_event_event); + test_add_st("/wait_for_event/sigchld", test_wait_for_event_sigchld); + test_add_st("/run_queue/zeroes-next-run", + test_run_queue_zeroes_next_run); + test_add_st("/run_queue/clears-cancelled_filenames", + test_run_queue_clears_cancelled_filenames); + test_add_st("/run_queue/skips-cancelled-filenames", + test_run_queue_skips_cancelled_filenames); + test_add_st("/run_queue/one-task", test_run_queue_one_task); + test_add_st("/run_queue/two-tasks", test_run_queue_two_tasks); + test_add_st("/run_queue/two-tasks/quit", + test_run_queue_two_tasks_quit); + test_add_st("/run_queue/two-tasks-cleanup", + test_run_queue_two_tasks_cleanup); + test_add_st("/task-creators/start_mandos_client", + test_start_mandos_client); + test_add_st("/task-creators/start_mandos_client/execv", + test_start_mandos_client_execv); + test_add_st("/task-creators/start_mandos_client/suid/euid", + test_start_mandos_client_suid_euid); + test_add_st("/task-creators/start_mandos_client/suid/egid", + test_start_mandos_client_suid_egid); + test_add_st("/task-creators/start_mandos_client/suid/ruid", + test_start_mandos_client_suid_ruid); + test_add_st("/task-creators/start_mandos_client/suid/rgid", + test_start_mandos_client_suid_rgid); + test_add_st("/task-creators/start_mandos_client/read", + test_start_mandos_client_read); + test_add_st("/task-creators/start_mandos_client/helper-directory", + test_start_mandos_client_helper_directory); + test_add_st("/task-creators/start_mandos_client/sigmask", + test_start_mandos_client_sigmask); + test_add_st("/task/wait_for_mandos_client_exit/badpid", + test_wait_for_mandos_client_exit_badpid); + test_add_st("/task/wait_for_mandos_client_exit/noexit", + test_wait_for_mandos_client_exit_noexit); + test_add_st("/task/wait_for_mandos_client_exit/success", + test_wait_for_mandos_client_exit_success); + test_add_st("/task/wait_for_mandos_client_exit/failure", + test_wait_for_mandos_client_exit_failure); + test_add_st("/task/wait_for_mandos_client_exit/killed", + test_wait_for_mandos_client_exit_killed); + test_add_st("/task/read_mandos_client_output/readerror", + test_read_mandos_client_output_readerror); + test_add_st("/task/read_mandos_client_output/nodata", + test_read_mandos_client_output_nodata); + test_add_st("/task/read_mandos_client_output/eof", + test_read_mandos_client_output_eof); + test_add_st("/task/read_mandos_client_output/once", + test_read_mandos_client_output_once); + test_add_st("/task/read_mandos_client_output/malloc", + test_read_mandos_client_output_malloc); + test_add_st("/task/read_mandos_client_output/append", + test_read_mandos_client_output_append); + test_add_st("/task-creators/add_inotify_dir_watch", + test_add_inotify_dir_watch); + test_add_st("/task-creators/add_inotify_dir_watch/fail", + test_add_inotify_dir_watch_fail); + test_add_st("/task-creators/add_inotify_dir_watch/not-a-directory", + test_add_inotify_dir_watch_nondir); + test_add_st("/task-creators/add_inotify_dir_watch/EAGAIN", + test_add_inotify_dir_watch_EAGAIN); + test_add_st("/task-creators/add_inotify_dir_watch/IN_CLOSE_WRITE", + test_add_inotify_dir_watch_IN_CLOSE_WRITE); + test_add_st("/task-creators/add_inotify_dir_watch/IN_MOVED_TO", + test_add_inotify_dir_watch_IN_MOVED_TO); + test_add_st("/task-creators/add_inotify_dir_watch/IN_MOVED_FROM", + test_add_inotify_dir_watch_IN_MOVED_FROM); + test_add_st("/task-creators/add_inotify_dir_watch/IN_EXCL_UNLINK", + test_add_inotify_dir_watch_IN_EXCL_UNLINK); + test_add_st("/task-creators/add_inotify_dir_watch/IN_DELETE", + test_add_inotify_dir_watch_IN_DELETE); + test_add_st("/task/read_inotify_event/readerror", + test_read_inotify_event_readerror); + test_add_st("/task/read_inotify_event/bad-epoll", + test_read_inotify_event_bad_epoll); + test_add_st("/task/read_inotify_event/nodata", + test_read_inotify_event_nodata); + test_add_st("/task/read_inotify_event/eof", + test_read_inotify_event_eof); + test_add_st("/task/read_inotify_event/IN_CLOSE_WRITE", + test_read_inotify_event_IN_CLOSE_WRITE); + test_add_st("/task/read_inotify_event/IN_MOVED_TO", + test_read_inotify_event_IN_MOVED_TO); + test_add_st("/task/read_inotify_event/IN_MOVED_FROM", + test_read_inotify_event_IN_MOVED_FROM); + test_add_st("/task/read_inotify_event/IN_DELETE", + test_read_inotify_event_IN_DELETE); + test_add_st("/task/read_inotify_event/IN_CLOSE_WRITE/badname", + test_read_inotify_event_IN_CLOSE_WRITE_badname); + test_add_st("/task/read_inotify_event/IN_MOVED_TO/badname", + test_read_inotify_event_IN_MOVED_TO_badname); + test_add_st("/task/read_inotify_event/IN_MOVED_FROM/badname", + test_read_inotify_event_IN_MOVED_FROM_badname); + test_add_st("/task/read_inotify_event/IN_DELETE/badname", + test_read_inotify_event_IN_DELETE_badname); + test_add_st("/task/open_and_parse_question/ENOENT", + test_open_and_parse_question_ENOENT); + test_add_st("/task/open_and_parse_question/EIO", + test_open_and_parse_question_EIO); + test_add_st("/task/open_and_parse_question/parse-error", + test_open_and_parse_question_parse_error); + test_add_st("/task/open_and_parse_question/nosocket", + test_open_and_parse_question_nosocket); + test_add_st("/task/open_and_parse_question/badsocket", + test_open_and_parse_question_badsocket); + test_add_st("/task/open_and_parse_question/nopid", + test_open_and_parse_question_nopid); + test_add_st("/task/open_and_parse_question/badpid", + test_open_and_parse_question_badpid); + test_add_st("/task/open_and_parse_question/noexist_pid", + test_open_and_parse_question_noexist_pid); + test_add_st("/task/open_and_parse_question/no-notafter", + test_open_and_parse_question_no_notafter); + test_add_st("/task/open_and_parse_question/bad-notafter", + test_open_and_parse_question_bad_notafter); + test_add_st("/task/open_and_parse_question/notafter-0", + test_open_and_parse_question_notafter_0); + test_add_st("/task/open_and_parse_question/notafter-1", + test_open_and_parse_question_notafter_1); + test_add_st("/task/open_and_parse_question/notafter-1-1", + test_open_and_parse_question_notafter_1_1); + test_add_st("/task/open_and_parse_question/notafter-1-2", + test_open_and_parse_question_notafter_1_2); + test_add_st("/task/open_and_parse_question/equal-notafter", + test_open_and_parse_question_equal_notafter); + test_add_st("/task/open_and_parse_question/late-notafter", + test_open_and_parse_question_late_notafter); + test_add_st("/task/cancel_old_question/0-1-2", + test_cancel_old_question_0_1_2); + test_add_st("/task/cancel_old_question/0-2-1", + test_cancel_old_question_0_2_1); + test_add_st("/task/cancel_old_question/1-2-3", + test_cancel_old_question_1_2_3); + test_add_st("/task/cancel_old_question/1-3-2", + test_cancel_old_question_1_3_2); + test_add_st("/task/cancel_old_question/2-1-3", + test_cancel_old_question_2_1_3); + test_add_st("/task/cancel_old_question/2-3-1", + test_cancel_old_question_2_3_1); + test_add_st("/task/cancel_old_question/3-1-2", + test_cancel_old_question_3_1_2); + test_add_st("/task/cancel_old_question/3-2-1", + test_cancel_old_question_3_2_1); + test_add_st("/task/connect_question_socket/name-too-long", + test_connect_question_socket_name_too_long); + test_add_st("/task/connect_question_socket/connect-fail", + test_connect_question_socket_connect_fail); + test_add_st("/task/connect_question_socket/bad-epoll", + test_connect_question_socket_bad_epoll); + test_add_st("/task/connect_question_socket/usable", + test_connect_question_socket_usable); + test_add_st("/task/send_password_to_socket/client-not-exited", + test_send_password_to_socket_client_not_exited); + test_add_st("/task/send_password_to_socket/password-not-read", + test_send_password_to_socket_password_not_read); + test_add_st("/task/send_password_to_socket/EMSGSIZE", + test_send_password_to_socket_EMSGSIZE); + test_add_st("/task/send_password_to_socket/retry", + test_send_password_to_socket_retry); + test_add_st("/task/send_password_to_socket/bad-epoll", + test_send_password_to_socket_bad_epoll); + test_add_st("/task/send_password_to_socket/null-password", + test_send_password_to_socket_null_password); + test_add_st("/task/send_password_to_socket/empty-password", + test_send_password_to_socket_empty_password); + test_add_st("/task/send_password_to_socket/empty-str-password", + test_send_password_to_socket_empty_str_pass); + test_add_st("/task/send_password_to_socket/text-password", + test_send_password_to_socket_text_password); + test_add_st("/task/send_password_to_socket/binary-password", + test_send_password_to_socket_binary_password); + test_add_st("/task/send_password_to_socket/nuls-in-password", + test_send_password_to_socket_nuls_in_password); + test_add_st("/task-creators/add_existing_questions/ENOENT", + test_add_existing_questions_ENOENT); + test_add_st("/task-creators/add_existing_questions/no-questions", + test_add_existing_questions_no_questions); + test_add_st("/task-creators/add_existing_questions/one-question", + test_add_existing_questions_one_question); + test_add_st("/task-creators/add_existing_questions/two-questions", + test_add_existing_questions_two_questions); + test_add_st("/task-creators/add_existing_questions/non-questions", + test_add_existing_questions_non_questions); + test_add_st("/task-creators/add_existing_questions/both-types", + test_add_existing_questions_both_types); + + return g_test_run() == 0; +} + +static bool should_only_run_tests(int *argc_p, char **argv_p[]){ + GOptionContext *context = g_option_context_new(""); + + g_option_context_set_help_enabled(context, FALSE); + g_option_context_set_ignore_unknown_options(context, TRUE); + + gboolean run_tests = FALSE; + GOptionEntry entries[] = { + { "test", 0, 0, G_OPTION_ARG_NONE, + &run_tests, "Run tests", NULL }, + { NULL } + }; + g_option_context_add_main_entries(context, entries, NULL); + + GError *error = NULL; + + if(g_option_context_parse(context, argc_p, argv_p, &error) != TRUE){ + g_option_context_free(context); + g_error("Failed to parse options: %s", error->message); + } + + g_option_context_free(context); + return run_tests != FALSE; +} === added file 'dracut-module/password-agent.xml' --- dracut-module/password-agent.xml 1970-01-01 00:00:00 +0000 +++ dracut-module/password-agent.xml 2019-07-27 10:11:45 +0000 @@ -0,0 +1,469 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + + Run Mandos client as a systemd password agent. + + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + + + + -- + + MANDOS_CLIENT + + OPTIONS + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + + + + + DESCRIPTION + + &COMMANDNAME; is a program which is meant to + be a systemd + 1 Password + Agent (See Password Agents). The aim of this program is therefore + to acquire and then send a password to some other program which + will use the password to unlock the encrypted root disk. + + + This program is not meant to be invoked directly, but can be in + order to test it. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OPTIONS + + + + + + + Specify a different agent directory. The default is + /run/systemd/ask-password as per the + Password Agents specification. + + + + + + + + + Specify a different helper directory. The default is + /lib/mandos/plugin-helpers, which + will exist in the initial RAM disk + environment. (This will simply be passed to the + MANDOS_CLIENT program via the + MANDOSPLUGINHELPERDIR environment variable. + See + mandos-client8mandos.) + + + + + + + + + Change real user ID to USERID + when running MANDOS_CLIENT. + The default is 65534. Note: This + must be a number, not a name. + + + + + + + + + Change real group ID to GROUPID + when running MANDOS_CLIENT. + The default is 65534. Note: This + must be a number, not a name. + + + + + + MANDOS_CLIENT + + + This specifies the file name for + mandos-client8mandos. If the + option is given, any + following options are passed to the MANDOS_CLIENT program. The default is + /lib/mandos/plugins.d/mandos-client + (which is the correct location for the initial + RAM disk environment) without any + options. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Ignore normal operation; instead only run self-tests. + Adding the option may show more + options possible in combination with + . + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + + + + + + OVERVIEW + + + This program, &COMMANDNAME;, will run on the client side in the + initial RAM disk environment, and is + responsible for getting a password from the Mandos client + program itself, and to send that password to whatever is + currently asking for a password using the systemd Password Agents mechanism. + + To accomplish this, &COMMANDNAME; runs the + mandos-client program (which is the actual + client program communicating with the Mandos server) or, + alternatively, any executable file specified as + MANDOS_CLIENT, and, as soon as a + password is acquired from the + MANDOS_CLIENT program, sends that + password (as per the Password Agents specification) to all currently + unanswered password questions. + + + This program should be started (normally as a systemd service, + which in turn is normally started by a systemd.path + 5 file) as a reaction to + files named ask.xxxx appearing in the agent directory + /run/systemd/ask-password + (or the directory specified by + ). + + + + + EXIT STATUS + + Exit status of this program is zero if no errors were + encountered, and otherwise not. + + + + + ENVIRONMENT + + This program does not use any environment variables itself, it + only passes on its environment to + MANDOS_CLIENT. Also, the + option will affect the + environment variable MANDOSPLUGINHELPERDIR for + MANDOS_CLIENT. + + + + + FILES + + + + /run/systemd/ask-password + + + The default directory to watch for password questions as + per the Password Agents specification; can be changed + by the option. + + + + + /lib/mandos/plugin-helpers + + + The helper directory as supplied to + MANDOS_CLIENT via the + MANDOSPLUGINHELPERDIR environment + variable; can be changed by the + option. + + + + + + + + + BUGS + + + + + EXAMPLE + + + Normal invocation needs no options: + + + &COMMANDNAME; + + + + + Run an alternative MANDOS_CLIENT + program:: + + + &COMMANDNAME; /usr/local/sbin/alternate + + + + + Use alternative locations for the helper directory and the + Mandos client, and add extra options suitable for running in + the normal file system: + + + + + &COMMANDNAME; --helper-directory=/usr/lib/x86_64-linux-gnu/mandos/plugin-helpers -- /usr/lib/x86_64-linux-gnu/mandos/plugins.d/mandos-client --pubkey=/etc/keys/mandos/pubkey.txt --seckey=/etc/keys/mandos/seckey.txt --tls-pubkey=/etc/keys/mandos/tls-pubkey.pem --tls-privkey=/etc/keys/mandos/tls-privkey.pem + + + + + + Use the default location for + mandos-client + 8mandos, but add many + options to it: + + + + +&COMMANDNAME; -- /lib/mandos/mandos-client --pubkey=/etc/mandos/keys/pubkey.txt --seckey=/etc/mandos/keys/seckey.txt --tls-pubkey=/etc/mandos/keys/tls-pubkey.pem --tls-privkey=/etc/mandos/keys/tls-privkey.pem + + + + + + Only run the self-tests: + + + &COMMANDNAME; --test + + + + + SECURITY + + This program will need to run as the root user in order to read + the agent directory and the ask.xxxx files + there, and will, when starting the Mandos client program, + require the ability to set the real user and + group ids to another user, by default user and group 65534, + which are assumed to be non-privileged. This is done in order + to match the expectations of mandos-client8mandos, which assumes that its executable file is + owned by the root user and also has the set-user-ID bit set (see + execve2). + + + + + SEE ALSO + + intro + 8mandos, + mandos-client + 8mandos, + systemd + 1, + + + + + Password Agents + + + + The specification for systemd Password + Agent programs, which + &COMMANDNAME; follows. + + + + + + +
+ + + + + === added file 'init.d-mandos' --- init.d-mandos 1970-01-01 00:00:00 +0000 +++ init.d-mandos 2018-02-10 13:23:58 +0000 @@ -0,0 +1,164 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: mandos +# Required-Start: $remote_fs $syslog avahi-daemon +# Required-Stop: $remote_fs $syslog avahi-daemon +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Mandos server +# Description: Server of encrypted passwords to Mandos clients +### END INIT INFO + +# Author: Teddy Hogeborn +# Author: Björn Påhlsson + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="Mandos root file system password server" +NAME=mandos +DAEMON=/usr/sbin/$NAME +DAEMON_ARGS="" +if [ -d /run/. ]; then + PIDFILE=/run/$NAME.pid +else + PIDFILE=/var/run/$NAME.pid +fi +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +if [ -n "$CONFIGDIR" ]; then + DAEMON_ARGS="$DAEMON_ARGS --configdir $CONFIGDIR" +fi + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ + $DAEMON_ARGS \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" -p "$PIDFILE" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: === added file 'initramfs-tools-conf' --- initramfs-tools-conf 1970-01-01 00:00:00 +0000 +++ initramfs-tools-conf 2018-08-19 14:06:55 +0000 @@ -0,0 +1,17 @@ +# -*- shell-script -*- + +# Since the initramfs image will contain key files, we need to +# restrict permissions on it by setting UMASK here. +# +# The proper place to set UMASK is (according to +# /etc/cryptsetup-initramfs/conf-hook), in +# /etc/initramfs-tools/initramfs.conf, which we shouldn't edit. The +# corresponding directory for drop-in files from packages is +# /usr/share/initramfs-tools/conf.d, and this file will be installed +# there as "mandos-conf". +# +# This setting of UMASK will have unfortunate unintended side effects +# on the files *inside* the initramfs, but these are later fixed by +# "initramfs-tools-hook", installed as +# "/usr/share/initramfs-tools/hooks/mandos". +UMASK=0027 === added file 'initramfs-tools-conf-hook' --- initramfs-tools-conf-hook 1970-01-01 00:00:00 +0000 +++ initramfs-tools-conf-hook 2019-04-09 19:33:36 +0000 @@ -0,0 +1,14 @@ +# -*- shell-script -*- + +# The UMASK is set by the file "initramfs-tools-conf" (which is copied +# to /usr/share/initramfs-tools/conf.d/mandos-conf on installation) +# since there, as described therein, is the proper place to do that. +# However, it is possible for other packages to override the UMASK in +# any file in /usr/share/initramfs-tools/conf-hooks.d. Therefore, +# this file ("initramfs-tools-conf-hook") will be installed as +# "zz-mandos" in that directory to make sure UMASK is set correctly. + +# For more information on the effects of setting UMASK, see the +# aforementioned /usr/share/initramfs-tools/conf.d/mandos-conf file. + +UMASK=0027 === added file 'initramfs-tools-hook' --- initramfs-tools-hook 1970-01-01 00:00:00 +0000 +++ initramfs-tools-hook 2018-08-19 14:06:55 +0000 @@ -0,0 +1,277 @@ +#!/bin/sh + +# This script will be run by 'mkinitramfs' when it creates the image. +# Its job is to decide which files to install, then install them into +# the staging area, where the initramfs is being created. This +# happens when a new 'linux-image' package is installed, or when an +# administrator runs 'update-initramfs' by hand to update an initramfs +# image. + +# The environment contains at least: +# +# DESTDIR -- The staging directory where the image is being built. + +# No initramfs pre-requirements +PREREQ="cryptroot" + +prereqs() +{ + echo "$PREREQ" +} + +case $1 in +# get pre-requisites +prereqs) + prereqs + exit 0 + ;; +esac + +. /usr/share/initramfs-tools/hook-functions + +for d in /usr/lib \ + "/usr/lib/`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null`" \ + "`rpm --eval='%{_libdir}' 2>/dev/null`" /usr/local/lib; do + if [ -d "$d"/mandos ]; then + libdir="$d" + break + fi +done +if [ -z "$libdir" ]; then + # Mandos not found + exit 1 +fi + +for d in /etc/keys/mandos /etc/mandos/keys; do + if [ -d "$d" ]; then + keydir="$d" + break + fi +done +if [ -z "$keydir" ]; then + # Mandos key directory not found + exit 1 +fi + +set `{ getent passwd _mandos \ + || getent passwd nobody \ + || echo ::65534:65534:::; } \ + | cut --delimiter=: --fields=3,4 --only-delimited \ + --output-delimiter=" "` +mandos_user="$1" +mandos_group="$2" + +# The Mandos network client uses the network +auto_add_modules net +# The Mandos network client uses IPv6 +force_load ipv6 + +# These are directories inside the initrd +CONFDIR="/conf/conf.d/mandos" +MANDOSDIR="/lib/mandos" +PLUGINDIR="${MANDOSDIR}/plugins.d" +PLUGINHELPERDIR="${MANDOSDIR}/plugin-helpers" +HOOKDIR="${MANDOSDIR}/network-hooks.d" + +# Make directories +install --directory --mode=u=rwx,go=rx "${DESTDIR}${CONFDIR}" \ + "${DESTDIR}${MANDOSDIR}" "${DESTDIR}${HOOKDIR}" +install --owner=${mandos_user} --group=${mandos_group} --directory \ + --mode=u=rwx "${DESTDIR}${PLUGINDIR}" \ + "${DESTDIR}${PLUGINHELPERDIR}" + +copy_exec "$libdir"/mandos/mandos-to-cryptroot-unlock "${MANDOSDIR}" + +# Copy the Mandos plugin runner +copy_exec "$libdir"/mandos/plugin-runner "${MANDOSDIR}" + +# Copy the plugins + +# Copy the packaged plugins +for file in "$libdir"/mandos/plugins.d/*; do + base="`basename \"$file\"`" + # Is this plugin overridden? + if [ -e "/etc/mandos/plugins.d/$base" ]; then + continue + fi + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") echo "W: Mandos client plugin directory is empty." >&2 ;; + *) copy_exec "$file" "${PLUGINDIR}" ;; + esac +done + +# Copy the packaged plugin helpers +for file in "$libdir"/mandos/plugin-helpers/*; do + base="`basename \"$file\"`" + # Is this plugin overridden? + if [ -e "/etc/mandos/plugin-helpers/$base" ]; then + continue + fi + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) copy_exec "$file" "${PLUGINHELPERDIR}" ;; + esac +done + +# Copy any user-supplied plugins +for file in /etc/mandos/plugins.d/*; do + base="`basename \"$file\"`" + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) copy_exec "$file" "${PLUGINDIR}" ;; + esac +done + +# Copy any user-supplied plugin helpers +for file in /etc/mandos/plugin-helpers/*; do + base="`basename \"$file\"`" + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) copy_exec "$file" "${PLUGINHELPERDIR}" ;; + esac +done + +# Get DEVICE from initramfs.conf and other files +. /etc/initramfs-tools/initramfs.conf +for conf in /etc/initramfs-tools/conf.d/*; do + if [ -n `basename \"$conf\" | grep '^[[:alnum:]][[:alnum:]\._-]*$' \ + | grep -v '\.dpkg-.*$'` ]; then + [ -f "${conf}" ] && . "${conf}" + fi +done +export DEVICE + +# Copy network hooks +for hook in /etc/mandos/network-hooks.d/*; do + case "`basename \"$hook\"`" in + "*") continue ;; + *[!A-Za-z0-9_.-]*) continue ;; + *) test -d "$hook" || copy_exec "$hook" "${HOOKDIR}" ;; + esac + if [ -x "$hook" ]; then + # Copy any files needed by the network hook + MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=files \ + VERBOSITY=0 "$hook" files | while read -r file target; do + if [ ! -e "${file}" ]; then + echo "WARNING: file ${file} not found, requested by Mandos network hook '${hook##*/}'" >&2 + fi + if [ -z "${target}" ]; then + copy_exec "$file" + else + copy_exec "$file" "$target" + fi + done + # Copy and load any modules needed by the network hook + MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=modules \ + VERBOSITY=0 "$hook" modules | while read -r module; do + force_load "$module" + done + fi +done + +# GPGME needs GnuPG +gpg=/usr/bin/gpg +libgpgme11_version="`dpkg-query --showformat='${Version}' --show libgpgme11`" +if dpkg --compare-versions "$libgpgme11_version" ge 1.5.0-0.1; then + if [ -e /usr/bin/gpgconf ]; then + if [ ! -e "${DESTDIR}/usr/bin/gpgconf" ]; then + copy_exec /usr/bin/gpgconf + fi + gpg="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg:[^:]*://p'`" + gpgagent="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg-agent:[^:]*://p'`" + # Newer versions of GnuPG 2 requires the gpg-agent binary + if [ -e "$gpgagent" ] && [ ! -e "${DESTDIR}$gpgagent" ]; then + copy_exec "$gpgagent" + fi + fi +elif dpkg --compare-versions "$libgpgme11_version" ge 1.4.1-0.1; then + gpg=/usr/bin/gpg2 +fi +if [ ! -e "${DESTDIR}$gpg" ]; then + copy_exec "$gpg" +fi +unset gpg +unset libgpgme11_version + +# Config files +for file in /etc/mandos/plugin-runner.conf; do + if [ -d "$file" ]; then + continue + fi + cp --archive --sparse=always "$file" "${DESTDIR}${CONFDIR}" +done + +if [ ${mandos_user} != 65534 ]; then + sed --in-place --expression="1i--userid=${mandos_user}" \ + "${DESTDIR}${CONFDIR}/plugin-runner.conf" +fi + +if [ ${mandos_group} != 65534 ]; then + sed --in-place --expression="1i--groupid=${mandos_group}" \ + "${DESTDIR}${CONFDIR}/plugin-runner.conf" +fi + +# Key files +for file in "$keydir"/*; do + if [ -d "$file" ]; then + continue + fi + case "$file" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) + cp --archive --sparse=always "$file" \ + "${DESTDIR}${CONFDIR}" + chown ${mandos_user}:${mandos_group} \ + "${DESTDIR}${CONFDIR}/`basename \"$file\"`" + ;; + esac +done +# Use Diffie-Hellman parameters file if available +if [ -e "${DESTDIR}${CONFDIR}"/dhparams.pem ]; then + sed --in-place \ + --expression="1i--options-for=mandos-client:--dh-params=${CONFDIR}/dhparams.pem" \ + "${DESTDIR}/${CONFDIR}/plugin-runner.conf" +fi + +# /lib/mandos/plugin-runner will drop priviliges, but needs access to +# its plugin directory and its config file. However, since almost all +# files in initrd have been created with umask 027, this opening of +# permissions is needed. +# +# (The umask is not really intended to affect the files inside the +# initrd; it is intended to affect the initrd.img file itself, since +# it now contains secret key files. There is, however, no other way +# to set the permission of the initrd.img file without a race +# condition. This umask is set by "initramfs-tools-conf", installed +# as "/usr/share/initramfs-tools/conf.d/mandos-conf".) +# +for full in "${MANDOSDIR}" "${CONFDIR}"; do + while [ "$full" != "/" ]; do + chmod a+rX "${DESTDIR}$full" + full="`dirname \"$full\"`" + done +done + +# Reset some other things to sane permissions which we have +# inadvertently affected with our umask setting. +for dir in / /bin /etc /keyscripts /sbin /scripts /usr /usr/bin; do + if [ -d "${DESTDIR}$dir" ]; then + chmod a+rX "${DESTDIR}$dir" + fi +done +for dir in "${DESTDIR}"/lib* "${DESTDIR}"/usr/lib*; do + if [ -d "$dir" ]; then + find "$dir" \! -perm -u+rw,g+r -prune -or \! -type l -print0 \ + | xargs --null --no-run-if-empty chmod a+rX -- + fi +done === added file 'initramfs-tools-script' --- initramfs-tools-script 1970-01-01 00:00:00 +0000 +++ initramfs-tools-script 2018-08-19 01:35:11 +0000 @@ -0,0 +1,181 @@ +#!/bin/sh -e +# +# This script will run in the initrd environment at boot and edit +# /conf/conf.d/cryptroot to set /lib/mandos/plugin-runner as keyscript +# when no other keyscript is set, before cryptsetup. +# + +# This script should be installed as +# "/usr/share/initramfs-tools/scripts/init-premount/mandos" which will +# eventually be "/scripts/init-premount/mandos" in the initrd.img +# file. + +PREREQ="udev" +prereqs() +{ + echo "$PREREQ" +} + +case $1 in +prereqs) + prereqs + exit 0 + ;; +esac + +. /scripts/functions + +for param in `cat /proc/cmdline`; do + case "$param" in + ip=*) IPOPTS="${param#ip=}" ;; + mandos=*) + # Split option line on commas + old_ifs="$IFS" + IFS="$IFS," + for mpar in ${param#mandos=}; do + IFS="$old_ifs" + case "$mpar" in + off) exit 0 ;; + connect) connect="" ;; + connect:*) connect="${mpar#connect:}" ;; + *) log_warning_msg "$0: Bad option ${mpar}" ;; + esac + done + unset mpar + IFS="$old_ifs" + unset old_ifs + ;; + esac +done +unset param + +chmod a=rwxt /tmp + +# Get DEVICE from /conf/initramfs.conf and other files +. /conf/initramfs.conf +for conf in /conf/conf.d/*; do + [ -f "${conf}" ] && . "${conf}" +done +if [ -e /conf/param.conf ]; then + . /conf/param.conf +fi + +# Override DEVICE from sixth field of ip= kernel option, if passed +case "$IPOPTS" in + *:*:*:*:*:*) # At least six fields + # Remove the first five fields + device="${IPOPTS#*:*:*:*:*:}" + # Remove all fields except the first one + DEVICE="${device%%:*}" + ;; +esac + +# Add device setting (if any) to plugin-runner.conf +if [ "${DEVICE+set}" = set ]; then + # Did we get the device from an ip= option? + if [ "${device+set}" = set ]; then + # Let ip= option override local config; append: + cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf + + --options-for=mandos-client:--interface=${DEVICE} +EOF + else + # Prepend device setting so any later options would override: + sed -i -e \ + '1i--options-for=mandos-client:--interface='"${DEVICE}" \ + /conf/conf.d/mandos/plugin-runner.conf + fi +fi +unset device + +# If we are connecting directly, run "configure_networking" (from +# /scripts/functions); it needs IPOPTS and DEVICE +if [ "${connect+set}" = set ]; then + set +e # Required by library functions + configure_networking + set -e + if [ -n "$connect" ]; then + cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf + + --options-for=mandos-client:--connect=${connect} +EOF + fi +fi + +if [ -r /conf/conf.d/cryptroot ]; then + test -w /conf/conf.d + + # Do not replace cryptroot file unless we need to. + replace_cryptroot=no + + # Our keyscript + mandos=/lib/mandos/plugin-runner + test -x "$mandos" + + # parse /conf/conf.d/cryptroot. Format: + # target=sda2_crypt,source=/dev/sda2,rootdev,key=none,keyscript=/foo/bar/baz + # Is the root device specially marked? + changeall=yes + while read -r options; do + case "$options" in + rootdev,*|*,rootdev,*|*,rootdev) + # If the root device is specially marked, don't change all + # lines in crypttab by default. + changeall=no + ;; + esac + done < /conf/conf.d/cryptroot + + exec 3>/conf/conf.d/cryptroot.mandos + while read -r options; do + newopts="" + keyscript="" + changethis="$changeall" + # Split option line on commas + old_ifs="$IFS" + IFS="$IFS," + for opt in $options; do + # Find the keyscript option, if any + case "$opt" in + keyscript=*) + keyscript="${opt#keyscript=}" + newopts="$newopts,$opt" + ;; + "") : ;; + # Always use Mandos on the root device, if marked + rootdev) + changethis=yes + newopts="$newopts,$opt" + ;; + # Don't use Mandos on resume device, if marked + resumedev) + changethis=no + newopts="$newopts,$opt" + ;; + *) + newopts="$newopts,$opt" + ;; + esac + done + IFS="$old_ifs" + unset old_ifs + # If there was no keyscript option, add one. + if [ "$changethis" = yes ] && [ -z "$keyscript" ]; then + replace_cryptroot=yes + newopts="$newopts,keyscript=$mandos" + fi + newopts="${newopts#,}" + echo "$newopts" >&3 + done < /conf/conf.d/cryptroot + exec 3>&- + + # If we need to, replace the old cryptroot file with the new file. + if [ "$replace_cryptroot" = yes ]; then + mv /conf/conf.d/cryptroot /conf/conf.d/cryptroot.mandos-old + mv /conf/conf.d/cryptroot.mandos /conf/conf.d/cryptroot + else + rm -f /conf/conf.d/cryptroot.mandos + fi +elif [ -x /usr/bin/cryptroot-unlock ]; then + setsid /lib/mandos/mandos-to-cryptroot-unlock & +fi === added file 'initramfs-tools-script-stop' --- initramfs-tools-script-stop 1970-01-01 00:00:00 +0000 +++ initramfs-tools-script-stop 2018-08-19 14:58:40 +0000 @@ -0,0 +1,65 @@ +#!/bin/sh -e +# +# Script to wait for plugin-runner to exit before continuing boot +# +# Copyright © 2018 Teddy Hogeborn +# Copyright © 2018 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos 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. +# +# Mandos 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 Mandos. If not, see . +# +# Contact the authors at . +# +# This script will run in the initrd environment at boot and remove +# the file keeping the dummy plugin running, forcing plugin-runner to +# exit if it is still running. + +# This script should be installed as +# "/usr/share/initramfs-tools/scripts/local-premount/mandos" which will +# eventually be "/scripts/local-premount/mandos" in the initrd.img +# file. + +PREREQ="" +prereqs() +{ + echo "$PREREQ" +} + +case $1 in +prereqs) + prereqs + exit 0 + ;; +esac + +. /scripts/functions + +pid=$(cat /run/mandos-plugin-runner.pid 2>/dev/null) + +# If the dummy plugin is running, removing this file should force the +# dummy plugin to exit successfully, thereby making plugin-runner shut +# down all its other plugins and then exit itself. +rm -f /run/mandos-keep-running >/dev/null 2>&1 + +# Wait for exit of plugin-runner, if still running +if [ -n "$pid" ]; then + while :; do + case "$(readlink /proc/"$pid"/exe 2>/dev/null)" in + */plugin-runner) sleep 1;; + *) break;; + esac + done + rm -f /run/mandos-plugin-runner.pid >/dev/null 2>&1 +fi === added file 'initramfs-unpack' --- initramfs-unpack 1970-01-01 00:00:00 +0000 +++ initramfs-unpack 2019-07-27 10:11:45 +0000 @@ -0,0 +1,84 @@ +#!/bin/bash +# +# Initramfs unpacker - unpacks initramfs images into /tmp +# +# Copyright © 2013-2019 Teddy Hogeborn +# Copyright © 2013-2019 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos 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. +# +# Mandos 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 Mandos. If not, see +# . +# +# Contact the authors at . + +cpio="cpio --extract --make-directories --unconditional --preserve-modification-time" + +if [ -z "$*" ]; then + set -- /boot/initrd.img-* +fi + +for imgfile in "$@"; do + if ! [ -f "$imgfile" ]; then + echo "Error: Not an existing file: $imgfile" >&2 + continue + fi + imgdir="${TMPDIR:-/tmp}/${imgfile##*/}" + if [ -d "$imgdir" ]; then + rm --recursive -- "$imgdir" + fi + mkdir --parents "$imgdir" + # Does this image contain microcode? + if $cpio --quiet --list --file="$imgfile" >/dev/null 2>&1; then + # Number of bytes to skip to get to the compressed archive + skip=$(($(LANG=C $cpio --io-size=1 --list --file="$imgfile" 2>&1 \ + | sed --quiet \ + --expression='s/^\([0-9]\+\) blocks$/\1/p')+8)) + if [ -x /usr/lib/dracut/skipcpio ]; then + catimg="/usr/lib/dracut/skipcpio $imgfile" + else + catimg="dd if=$imgfile bs=$skip skip=1 status=noxfer" + fi + else + echo "No microcode detected" + catimg="cat -- $imgfile" + fi + while :; do + # Determine the compression method + if { $catimg 2>/dev/null | zcat --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="zcat" + elif { $catimg 2>/dev/null | bzip2 --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="bzip2 --stdout --decompress" + elif { $catimg 2>/dev/null | lzop --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="lzop --stdout --decompress" + else + skip=$((${skip}+1)) + echo "Could not determine compression of ${imgfile}; trying to skip ${skip} bytes" >&2 + catimg="dd if=$imgfile bs=$skip skip=1 status=noxfer" + continue + fi + break + done + case "$catimg" in + *skipcpio*) echo "Microcode detected, skipping";; + *) echo "Microcode detected, skipping ${skip} bytes";; + esac + $catimg 2>/dev/null | $decomp | ( cd -- "$imgdir" && $cpio --quiet ) + if [ ${PIPESTATUS[-1]} -eq 0 ]; then + echo "$imgfile unpacked into $imgdir" + fi +done === added file 'intro.xml' --- intro.xml 1970-01-01 00:00:00 +0000 +++ intro.xml 2019-08-04 12:42:49 +0000 @@ -0,0 +1,472 @@ + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + intro + 8mandos + + + + intro + + Introduction to the Mandos system + + + + + DESCRIPTION + + This is the the Mandos system, which allows computers to have + encrypted root file systems and at the same time be capable of + remote and/or unattended reboots. + + + The computers run a small client program in the initial RAM disk + environment which will communicate with a server over a network. + All network communication is encrypted using TLS. The clients + are identified by the server using a TLS public key; each client + has one unique to it. The server sends the clients an encrypted + password. The encrypted password is decrypted by the clients + using a separate OpenPGP key, and the password is then used to + unlock the root file system, whereupon the computers can + continue booting normally. + + + + + INTRODUCTION + + + You know how it is. You’ve heard of it happening. The Man + comes and takes away your servers, your friends’ servers, the + servers of everybody in the same hosting facility. The servers + of their neighbors, and their neighbors’ friends. The servers + of people who owe them money. And like + that, they’re gone. And you doubt you’ll + ever see them again. + + + That is why your servers have encrypted root file systems. + However, there’s a downside. There’s no going around it: + rebooting is a pain. Dragging out that rarely-used keyboard and + screen and unraveling cables behind your servers to plug them in + to type in that password is messy, especially if you have many + servers. There are some people who do clever things like using + serial line consoles and daisy-chain it to the next server, and + keep all the servers connected in a ring with serial cables, + which will work, if your servers are physically close enough. + There are also other out-of-band management solutions, but with + all these, you still have to be on hand and + manually type in the password at boot time. Otherwise the + server just sits there, waiting for a password. + + + Wouldn’t it be great if you could have the security of encrypted + root file systems and still have servers that could boot up + automatically if there was a short power outage while you were + asleep? That you could reboot at will, without having someone + run over to the server to type in the password? + + + Well, with Mandos, you (almost) can! The gain in convenience + will only be offset by a small loss in security. The setup is + as follows: + + + The server will still have its encrypted root file system. The + password to this file system will be stored on another computer + (henceforth known as the Mandos server) on the same local + network. The password will not be stored + in plaintext, but encrypted with OpenPGP. To decrypt this + password, a key is needed. This key (the Mandos client key) + will not be stored there, but back on the original server + (henceforth known as the Mandos client) in the initial RAM disk + image. Oh, and all network Mandos client/server communications + will be encrypted, using TLS (SSL). + + + So, at boot time, the Mandos client will ask for its encrypted + data over the network, decrypt the data to get the password, use + the password to decrypt the root file system, and the client can + then continue booting. + + + Now, of course the initial RAM disk image is not on the + encrypted root file system, so anyone who had physical access + could take the Mandos client computer offline and read the disk + with their own tools to get the authentication keys used by a + client. But, by then the Mandos server + should notice that the original server has been offline for too + long, and will no longer give out the encrypted key. The timing + here is the only real weak point, and the method, frequency and + timeout of the server’s checking can be adjusted to any desired + level of paranoia. + + + (The encrypted keys on the Mandos server is on its normal file + system, so those are safe, provided the root file system of + that server is encrypted.) + + + + + FREQUENTLY ASKED QUESTIONS + + Couldn’t the security be defeated by… + + + Grabbing the Mandos client key from the + initrd <emphasis>really quickly</emphasis>? + + This, as mentioned above, is the only real weak point. But if + you set the timing values tight enough, this will be really + difficult to do. An attacker would have to physically + disassemble the client computer, extract the key from the + initial RAM disk image, and then connect to a still + online Mandos server to get the encrypted key, and do + all this before the Mandos server timeout + kicks in and the Mandos server refuses to give out the key to + anyone. + + + Now, as the typical procedure seems to be to barge in and turn + off and grab all computers, to maybe look + at them months later, this is not likely. If someone does that, + the whole system will lock itself up + completely, since Mandos servers are no longer running. + + + For sophisticated attackers who could do + the clever thing, and had physical access + to the server for enough time, it would be simpler to get a key + for an encrypted file system by using hardware memory scanners + and reading it right off the memory bus. + + + + + Replay attacks? + + Nope, the network stuff is all done over TLS, which provides + protection against that. + + + + + Man-in-the-middle? + + No. The server only gives out the passwords to clients which + have in the TLS handshake proven that + they do indeed hold the private key corresponding to that + client. + + + + + How about sniffing the network traffic and decrypting it + later by physically grabbing the Mandos client and using its + key? + + We only use PFS (Perfect Forward Security) + key exchange algorithms in TLS, which protects against this. + + + + + Physically grabbing the Mandos server computer? + + You could protect that computer the + old-fashioned way, with a must-type-in-the-password-at-boot + method. Or you could have two computers be the Mandos server + for each other. + + + Multiple Mandos servers can coexist on a network without any + trouble. They do not clash, and clients will try all + available servers. This means that if just one reboots then + the other can bring it back up, but if both reboot at the same + time they will stay down until someone types in the password + on one of them. + + + + + Faking checker results? + + If the Mandos client does not have an SSH server, the default + is for the Mandos server to use + fping, the replies to which + could be faked to eliminate the timeout. But this could + easily be changed to any shell command, with any security + measures you like. If the Mandos client + has an SSH server, the default + configuration (as generated by + mandos-keygen with the + option) is for the Mandos server + to use an ssh-keyscan command with strict + keychecking, which can not be faked. Alternatively, IPsec + could be used for the ping packets, making them secure. + + + + + + SECURITY + + So, in summary: The only weakness in the Mandos system is from + people who have: + + + + + The power to come in and physically take your servers, + and + + + + + The cunning and patience to do it carefully, one at a time, + and quickly, faking Mandos + client/server responses for each one before the timeout. + + + + + While there are some who may be threatened by people who have + both these attributes, they do not, + probably, constitute the majority. + + + If you do face such opponents, you must + figure that they could just as well open your servers and read + the file system keys right off the memory by running wires to + the memory bus. + + + What Mandos is designed to protect against is + not such determined, focused, and competent + attacks, but against the early morning knock on your door and + the sudden absence of all the servers in your server room. + Which it does nicely. + + + + + PLUGINS + + In the early designs, the + mandos-client8mandos program (which + retrieves a password from the Mandos server) also prompted for a + password on the terminal, in case a Mandos server could not be + found. Other ways of retrieving a password could easily be + envisoned, but this multiplicity of purpose was seen to be too + complex to be a viable way to continue. Instead, the original + program was separated into mandos-client8mandos and password-prompt8mandos, and a plugin-runner8mandos exist to run them both in parallel, allowing + the first successful plugin to provide the password. This + opened up for any number of additional plugins to run, all + competing to be the first to find a password and provide it to + the plugin runner. + + + Four additional plugins are provided: + + + + + plymouth + 8mandos + + + + This prompts for a password when using + plymouth8. + + + + + + usplash + 8mandos + + + + This prompts for a password when using + usplash8. + + + + + + splashy + 8mandos + + + + This prompts for a password when using + splashy8. + + + + + + askpass-fifo + 8mandos + + + + To provide compatibility with the "askpass" program from + cryptsetup, this plugin listens to the same FIFO as + askpass would do. + + + + + + More plugins can easily be written and added by the system + administrator; see the section called "WRITING PLUGINS" in + plugin-runner + 8mandos to learn the + plugin requirements. + + + + + SYSTEMD + + More advanced startup systems like systemd1, + already have their own plugin-like mechanisms for allowing + multiple agents to independently retrieve a password and deliver + it to the subsystem requesting a password to unlock the root + file system. On these systems, it would make no sense to run + plugin-runner8mandos, the plugins of + which would largely duplicate the work of (and conflict with) + the existing systems prompting for passwords. + + + As for systemd1 in particular, it has + its own Password Agents system. Mandos uses this via its + password-agent8mandos program, which + is run instead of plugin-runner8mandos when systemd1 + is used during system startup. + + + + BUGS + + + + + SEE ALSO + + mandos + 8, + mandos.conf + 5, + mandos-clients.conf + 5, + mandos-ctl + 8, + mandos-monitor + 8, + plugin-runner + 8mandos, + password-agent + 8mandos, + mandos-client + 8mandos, + password-prompt + 8mandos, + plymouth + 8mandos, + usplash + 8mandos, + splashy + 8mandos, + askpass-fifo + 8mandos, + mandos-keygen + 8 + + + + + Mandos + + + + The Mandos home page. + + + + + +
+ + + + + === removed file 'key.pem' --- key.pem 2007-10-20 21:38:25 +0000 +++ key.pem 1970-01-01 00:00:00 +0000 @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIJKAIBAAKCAgEAriPn2IkZ+KvmPGXiAJ4vKFcQPhgZ3jaj+WVd1a1g70ge2sJm -SedEiOO+JDQ8mTolCkgaQ2t1bm5FJBxMqyFwQ4vxymtQd74ymLzfiLts5o+UN0li -gxq2QcS+r3bB5cV8przIblS2YBuK6jfzwludWBTscCqrWKmL51IyglTQVHerEoLd -6KhBB2r5AHbnsF5k0OrdKtZOplfDum7gOaI5kmGkwBXkKaNAq3hkLQR0HIZy3S2n -dfezCkFj7Rr7CUUwpV7VYbL11+GQ1FWFQJc0o4K5pogyyfyK4Jw2WQjDgZWG8mP0 -At9FlLlXNFooiE6cW5vj4/FgKW3WCF9MGGTVpCOJKXUlP6q2Cm9Kp3fg6di0tGn4 -vkRwtBI+jKQbZOOxTGdhe0GI+hm4iTu/lqVCVtPbQOa5aGkIybh/DVbRDp5cvYA+ -2uMC3sdLv2oOinJIviy3ydORjd/OaNbxWmDVjgfI3ppjy1H04iNLBmEK/42iLFtl -OhScteahG1Cc7qtKnuou4kI8LWBsNzVNHiZyWvlW6ZvxqDWJBOKLRK+Gpjy7WwMw -aUv1plbRZrKR+gLrhxR0Eqb1Hp23TxjTEB69JrF1yyYr2QJ8Ik6jqcnalD35Z1KN -gSGrkMsEaGl+HOz5TVkvQdiZN/JWOZta4NlpDViG3i8d/VNjACkWMYka3/sCAwEA -AQKCAgEApV0fWvbGnOfQGOa++Ms+CNa0a+LDHctRZxElTDX6aP9ZnW8hZ2igIkXy -V7rrGK6oYd0aY4910koQijv9ajy0uM/56biCj1MkBPrGYrdosIEDxISBcfI5xLaq -RUFG24Tv2/5FbtAu55EAF27OoXASOISWCeXbFLTcT+w0XqNfufZxk4CGbdro9bxV -fGVtmoPoxKNjJryfr0KEcVO8xb4RYborkuS25/tI/Au1RTKHeFcMWJB0B4gSktiJ -pa0LWkBD86Xch4xD/J8MwzX69d0gdW08ErIfWMPitWxiB6ZU0YdGwIK+QsP29UYT -U3mSJ+5OeIfOnrSmFnFg4E5umnOWk1fCkX9FpFwjFy2H9FrF/BHyZrIbv8IeKgzw -mySRXFl2KZUk8r8YyCTm4dtf2ZCU5KcUo9TOae1VzywjXFyV+AyOtFYcJ+FjHQag -CjuT4NzmFqlwa4JWXsAtnom7eR+f4jpn3nPW2CYCE/tYxtF9fpGXdXCNaKo8fbsA -FzOkvoPSAeOjDjcKba6Z/mMBb8or1ide06NEu77e8de8rWCEfdGbMGDmdLQaYI6u -/WIY7ALRppqT9WKnAjKGgYkxFeZGGF8D4ARaLJYYJ71DGAKeSkK6RqHqhmWDcSI8 -cLh5WHqzCflQhxjVf5rCF/ASwTnbw0dsClqF35gw+TKAlXc/uhECggEBAOMVwVIR -ZkCvcfH35RGxqMZ/Ca8Viqap5zLY/gL0YTM/HYhRPE8iUutCWGN1SBDQm2yKuk84 -H1Jjj7zTOxIVW0yov1m36KCsF31s8ks1b3uo+yEOyLhGpJPUq8mYAVbl/13Tcwp3 -0zs7oflZxK65vICUreJM9P/BqXJ/Edfddj88uIqjW98XGgTCgCHpFBxKfRzNkmi+ -3TBswZm2yqQ3qcrFN6H7ieUNCgN2Bj6tWlNmB/RCV+stbbEWwXbDlv1M+JNoM/Jw -c1A++csAhx04BphNeKi01W0zEyRt6tlZWqolvhSUTom6xxWtjikqwfad6HMwn09V -8luh5Ote2WNE8JcCggEBAMRQUlYKCTSvHuIscDukLExW+9JVKaufeXZvCuTK94Bs -P0MtAgrHkgXdiRIBBFAViPjadMQitGkwKYd+zRPUmdzTuH9Lamqjl6e8dUpcO+Bf -0scM94Te0gD8Gtv2Uu4Kw75P2RTXHRk9e8VbFpgYvfl9H0Uc8BK8jzlgWPqHgFnB -WUXuMTAcKax4UwXdxWUjyv3xPmDygt7koPqpuX6FKvfxRq50a3TVIFku+8hRQtQR -N+U0ScYrTOClTuc/QE0Q6rmw06lWHmytK3QiVhgpKxdw1xvVyjw5rMbB2eQdz7IQ -2zn/o5+0gYdg/jvamo1HIxKjPCP1Lxm++manmy6kVD0CggEAeDwwm89yoJVEc6WZ -uAClKFRjQDzbqNsU+ytBczcJsCSe8mpw0EWQOdhrDF4wxhZt9M6PTxqcGvd0R7pf -8Hc2XCSNDGf/1/LGjTZ+I6wrVwJl1V8Kj+d3hH56ZscBDo3A5GDs7IH9acNtQ6Vw -KkNVt48Bcmzk2/YiTelR/UXZMipoW5+bKUgGErcZONs8Nq6KCBIgjy1f2B/9cfIC -4WhHkoFRr4aLwKdiwepf7BfFV5sSYxYtjuwCxF0UGln9PCjhBMuLlbZMmBSAFig1 -YhckBsgeNtVom+ULIaLBUkupYaWSOzs7SlmGx8eZGdr10CpTxYndEBiltjbGComx -+ImsCQKCAQBaaX+yHock225Gzh6WaUL5man6sbwyTY0cLYH/4zZfz/rGzmi9XDJ1 -PxVM1GkPFQvzSHE0j6M1Org1rgF3G5gNKvkyryIAoP1MhDAkohv1d0xU3jT03cYs -K++W1HhXJ2AFOzMINRYytK1XNF9QhzyfNa/8HZq3ll4EF8qC/3ruW2zpFw1SUfYj -d3sNHZk2vmhT0hJfhfEeBH/bUeWbTmt+q4FZAUcoFKwERu4w0LQNhSyQBCfh+7k5 -UQjo2amclKj2AmlI+N+kP5DeuJ2cHQG6lv6K1EiCujFHjKn0NIKeSMMekAzklbZ+ -Cf6sxD4fyN5vS/x7twUNP3aFZrXCom4lAoIBABSGp/CGw6xeQDPXvZZ7gpLr5m5P -mp+gS3S2MJZUv+6TEwXgHxnbxt5DMe25RAlqVHTuw9/TVccD6qcERvnhxUFMoscD -5Vg/Hj+D1nPEVxvFgq0xEPI2FcwS6xT8gicV5dsCWIlOY3ZxBzOzByW1iA2YNymj -pNTypqkARnheZZCK+4ortIeczll5XpbyGIXyxfIrKLYyv38gqLwCdb+MJsih91qc -SrijfhjqqK9sr5KyHlPJDI4Mlw//qkPwIiKv70TGsSgu775OyyZFEl3uS+Y4BUGa -dk5d+8OviBYIReMvvyl6U0jFCUK579lEDpEOPDF2C6VvDQ3FyHP7UXaOWrI= ------END RSA PRIVATE KEY----- === added file 'legalnotice.xml' --- legalnotice.xml 1970-01-01 00:00:00 +0000 +++ legalnotice.xml 2017-08-20 16:20:54 +0000 @@ -0,0 +1,28 @@ + + + + + This manual page is part of Mandos. + + + + Mandos 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. + + + + Mandos 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 Mandos. If not, see http://www.gnu.org/licenses/. + + === renamed file 'server.py' => 'mandos' --- server.py 2008-07-22 06:23:29 +0000 +++ mandos 2019-11-03 19:09:41 +0000 @@ -1,608 +1,2930 @@ -#!/usr/bin/python -# -*- mode: python; coding: utf-8 -*- -# +#!/usr/bin/python3 -bI +# -*- mode: python; after-save-hook: (lambda () (let ((command (if (fboundp 'file-local-name) (file-local-name (buffer-file-name)) (or (file-remote-p (buffer-file-name) 'localname) (buffer-file-name))))) (if (= (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (format "%s --check" (shell-quote-argument command)) nil "*Test*")) 0) (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w))) (progn (with-current-buffer "*Test*" (compilation-mode)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); coding: utf-8 -*- +# # Mandos server - give out binary blobs to connecting clients. -# +# # This program is partly derived from an example program for an Avahi # service publisher, downloaded from # . This includes the -# following functions: "add_service", "remove_service", -# "server_state_changed", "entry_group_state_changed", and some lines -# in "main". -# -# Everything else is Copyright © 2007-2008 Teddy Hogeborn and Björn -# Påhlsson. -# -# 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 +# methods "add", "remove", "server_state_changed", +# "entry_group_state_changed", "cleanup", and "activate" in the +# "AvahiService" class, and some lines in "main". +# +# Everything else is +# Copyright © 2008-2019 Teddy Hogeborn +# Copyright © 2008-2019 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos 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 +# Mandos 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 . -# -# Contact the authors at and -# . -# - -from __future__ import division - -import SocketServer +# along with Mandos. If not, see . +# +# Contact the authors at . +# + +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +try: + from future_builtins import * +except ImportError: + pass + +try: + import SocketServer as socketserver +except ImportError: + import socketserver import socket -import select -from optparse import OptionParser +import argparse import datetime import errno -import gnutls.crypto -import gnutls.connection -import gnutls.errors -import gnutls.library.functions -import gnutls.library.constants -import gnutls.library.types -import ConfigParser +try: + import ConfigParser as configparser +except ImportError: + import configparser import sys import re import os import signal -from sets import Set import subprocess import atexit import stat import logging import logging.handlers +import pwd +import contextlib +import struct +import fcntl +import functools +try: + import cPickle as pickle +except ImportError: + import pickle +import multiprocessing +import types +import binascii +import tempfile +import itertools +import collections +import codecs +import unittest import dbus -import gobject -import avahi +import dbus.service +import gi +from gi.repository import GLib from dbus.mainloop.glib import DBusGMainLoop import ctypes - -# Brief description of the operation of this program: -# -# This server announces itself as a Zeroconf service. Connecting -# clients use the TLS protocol, with the unusual quirk that this -# server program acts as a TLS "client" while the connecting clients -# acts as a TLS "server". The clients (acting as a TLS "server") must -# supply an OpenPGP certificate, and the fingerprint of this -# certificate is used by this server to look up (in a list read from a -# file at start time) which binary blob to give the client. No other -# authentication or authorization is done by this server. - - -logger = logging.Logger('mandos') -syslogger = logging.handlers.SysLogHandler\ - (facility = logging.handlers.SysLogHandler.LOG_DAEMON) -syslogger.setFormatter(logging.Formatter\ - ('%(levelname)s: %(message)s')) -logger.addHandler(syslogger) -del syslogger - -# This variable is used to optionally bind to a specified interface. -# It is a global variable to fit in with the other variables from the -# Avahi example code. -serviceInterface = avahi.IF_UNSPEC -# From the Avahi example code: -serviceName = None -serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html -servicePort = None # Not known at startup -serviceTXT = [] # TXT record for the service -domain = "" # Domain to publish on, default to .local -host = "" # Host to publish records for, default to localhost -group = None #our entry group -rename_count = 12 # Counter so we only rename after collisions a - # sensible number of times -# End of Avahi example code - - -class Client(object): +import ctypes.util +import xml.dom.minidom +import inspect + +if sys.version_info.major == 2: + __metaclass__ = type + +# Show warnings by default +if not sys.warnoptions: + import warnings + warnings.simplefilter("default") + +# Try to find the value of SO_BINDTODEVICE: +try: + # This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and + # newer, and it is also the most natural place for it: + SO_BINDTODEVICE = socket.SO_BINDTODEVICE +except AttributeError: + try: + # This is where SO_BINDTODEVICE was up to and including Python + # 2.6, and also 3.2: + from IN import SO_BINDTODEVICE + except ImportError: + # In Python 2.7 it seems to have been removed entirely. + # Try running the C preprocessor: + try: + cc = subprocess.Popen(["cc", "--language=c", "-E", + "/dev/stdin"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + stdout = cc.communicate( + "#include \nSO_BINDTODEVICE\n")[0] + SO_BINDTODEVICE = int(stdout.splitlines()[-1]) + except (OSError, ValueError, IndexError): + # No value found + SO_BINDTODEVICE = None + +if sys.version_info.major == 2: + str = unicode + +if sys.version_info < (3, 2): + configparser.Configparser = configparser.SafeConfigParser + +version = "1.8.9" +stored_state_file = "clients.pickle" + +logger = logging.getLogger() +logging.captureWarnings(True) # Show warnings via the logging system +syslogger = None + +try: + if_nametoindex = ctypes.cdll.LoadLibrary( + ctypes.util.find_library("c")).if_nametoindex +except (OSError, AttributeError): + + def if_nametoindex(interface): + "Get an interface index the hard way, i.e. using fcntl()" + SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h + with contextlib.closing(socket.socket()) as s: + ifreq = fcntl.ioctl(s, SIOCGIFINDEX, + struct.pack(b"16s16x", interface)) + interface_index = struct.unpack("I", ifreq[16:20])[0] + return interface_index + + +def copy_function(func): + """Make a copy of a function""" + if sys.version_info.major == 2: + return types.FunctionType(func.func_code, + func.func_globals, + func.func_name, + func.func_defaults, + func.func_closure) + else: + return types.FunctionType(func.__code__, + func.__globals__, + func.__name__, + func.__defaults__, + func.__closure__) + + +def initlogger(debug, level=logging.WARNING): + """init logger and add loglevel""" + + global syslogger + syslogger = (logging.handlers.SysLogHandler( + facility=logging.handlers.SysLogHandler.LOG_DAEMON, + address="/dev/log")) + syslogger.setFormatter(logging.Formatter + ('Mandos [%(process)d]: %(levelname)s:' + ' %(message)s')) + logger.addHandler(syslogger) + + if debug: + console = logging.StreamHandler() + console.setFormatter(logging.Formatter('%(asctime)s %(name)s' + ' [%(process)d]:' + ' %(levelname)s:' + ' %(message)s')) + logger.addHandler(console) + logger.setLevel(level) + + +class PGPError(Exception): + """Exception if encryption/decryption fails""" + pass + + +class PGPEngine: + """A simple class for OpenPGP symmetric encryption & decryption""" + + def __init__(self): + self.tempdir = tempfile.mkdtemp(prefix="mandos-") + self.gpg = "gpg" + try: + output = subprocess.check_output(["gpgconf"]) + for line in output.splitlines(): + name, text, path = line.split(b":") + if name == b"gpg": + self.gpg = path + break + except OSError as e: + if e.errno != errno.ENOENT: + raise + self.gnupgargs = ['--batch', + '--homedir', self.tempdir, + '--force-mdc', + '--quiet'] + # Only GPG version 1 has the --no-use-agent option. + if self.gpg == b"gpg" or self.gpg.endswith(b"/gpg"): + self.gnupgargs.append("--no-use-agent") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._cleanup() + return False + + def __del__(self): + self._cleanup() + + def _cleanup(self): + if self.tempdir is not None: + # Delete contents of tempdir + for root, dirs, files in os.walk(self.tempdir, + topdown=False): + for filename in files: + os.remove(os.path.join(root, filename)) + for dirname in dirs: + os.rmdir(os.path.join(root, dirname)) + # Remove tempdir + os.rmdir(self.tempdir) + self.tempdir = None + + def password_encode(self, password): + # Passphrase can not be empty and can not contain newlines or + # NUL bytes. So we prefix it and hex encode it. + encoded = b"mandos" + binascii.hexlify(password) + if len(encoded) > 2048: + # GnuPG can't handle long passwords, so encode differently + encoded = (b"mandos" + password.replace(b"\\", b"\\\\") + .replace(b"\n", b"\\n") + .replace(b"\0", b"\\x00")) + return encoded + + def encrypt(self, data, password): + passphrase = self.password_encode(password) + with tempfile.NamedTemporaryFile( + dir=self.tempdir) as passfile: + passfile.write(passphrase) + passfile.flush() + proc = subprocess.Popen([self.gpg, '--symmetric', + '--passphrase-file', + passfile.name] + + self.gnupgargs, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + ciphertext, err = proc.communicate(input=data) + if proc.returncode != 0: + raise PGPError(err) + return ciphertext + + def decrypt(self, data, password): + passphrase = self.password_encode(password) + with tempfile.NamedTemporaryFile( + dir=self.tempdir) as passfile: + passfile.write(passphrase) + passfile.flush() + proc = subprocess.Popen([self.gpg, '--decrypt', + '--passphrase-file', + passfile.name] + + self.gnupgargs, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + decrypted_plaintext, err = proc.communicate(input=data) + if proc.returncode != 0: + raise PGPError(err) + return decrypted_plaintext + + +# Pretend that we have an Avahi module +class avahi: + """This isn't so much a class as it is a module-like namespace.""" + IF_UNSPEC = -1 # avahi-common/address.h + PROTO_UNSPEC = -1 # avahi-common/address.h + PROTO_INET = 0 # avahi-common/address.h + PROTO_INET6 = 1 # avahi-common/address.h + DBUS_NAME = "org.freedesktop.Avahi" + DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup" + DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server" + DBUS_PATH_SERVER = "/" + + @staticmethod + def string_array_to_txt_array(t): + return dbus.Array((dbus.ByteArray(s.encode("utf-8")) + for s in t), signature="ay") + ENTRY_GROUP_ESTABLISHED = 2 # avahi-common/defs.h + ENTRY_GROUP_COLLISION = 3 # avahi-common/defs.h + ENTRY_GROUP_FAILURE = 4 # avahi-common/defs.h + SERVER_INVALID = 0 # avahi-common/defs.h + SERVER_REGISTERING = 1 # avahi-common/defs.h + SERVER_RUNNING = 2 # avahi-common/defs.h + SERVER_COLLISION = 3 # avahi-common/defs.h + SERVER_FAILURE = 4 # avahi-common/defs.h + + +class AvahiError(Exception): + def __init__(self, value, *args, **kwargs): + self.value = value + return super(AvahiError, self).__init__(value, *args, + **kwargs) + + +class AvahiServiceError(AvahiError): + pass + + +class AvahiGroupError(AvahiError): + pass + + +class AvahiService: + """An Avahi (Zeroconf) service. + + Attributes: + interface: integer; avahi.IF_UNSPEC or an interface index. + Used to optionally bind to the specified interface. + name: string; Example: 'Mandos' + type: string; Example: '_mandos._tcp'. + See + port: integer; what port to announce + TXT: list of strings; TXT record for the service + domain: string; Domain to publish on, default to .local if empty. + host: string; Host to publish records for, default is localhost + max_renames: integer; maximum number of renames + rename_count: integer; counter so we only rename after collisions + a sensible number of times + group: D-Bus Entry Group + server: D-Bus Server + bus: dbus.SystemBus() + """ + + def __init__(self, + interface=avahi.IF_UNSPEC, + name=None, + servicetype=None, + port=None, + TXT=None, + domain="", + host="", + max_renames=32768, + protocol=avahi.PROTO_UNSPEC, + bus=None): + self.interface = interface + self.name = name + self.type = servicetype + self.port = port + self.TXT = TXT if TXT is not None else [] + self.domain = domain + self.host = host + self.rename_count = 0 + self.max_renames = max_renames + self.protocol = protocol + self.group = None # our entry group + self.server = None + self.bus = bus + self.entry_group_state_changed_match = None + + def rename(self, remove=True): + """Derived from the Avahi example code""" + if self.rename_count >= self.max_renames: + logger.critical("No suitable Zeroconf service name found" + " after %i retries, exiting.", + self.rename_count) + raise AvahiServiceError("Too many renames") + self.name = str( + self.server.GetAlternativeServiceName(self.name)) + self.rename_count += 1 + logger.info("Changing Zeroconf service name to %r ...", + self.name) + if remove: + self.remove() + try: + self.add() + except dbus.exceptions.DBusException as error: + if (error.get_dbus_name() + == "org.freedesktop.Avahi.CollisionError"): + logger.info("Local Zeroconf service name collision.") + return self.rename(remove=False) + else: + logger.critical("D-Bus Exception", exc_info=error) + self.cleanup() + os._exit(1) + + def remove(self): + """Derived from the Avahi example code""" + if self.entry_group_state_changed_match is not None: + self.entry_group_state_changed_match.remove() + self.entry_group_state_changed_match = None + if self.group is not None: + self.group.Reset() + + def add(self): + """Derived from the Avahi example code""" + self.remove() + if self.group is None: + self.group = dbus.Interface( + self.bus.get_object(avahi.DBUS_NAME, + self.server.EntryGroupNew()), + avahi.DBUS_INTERFACE_ENTRY_GROUP) + self.entry_group_state_changed_match = ( + self.group.connect_to_signal( + 'StateChanged', self.entry_group_state_changed)) + logger.debug("Adding Zeroconf service '%s' of type '%s' ...", + self.name, self.type) + self.group.AddService( + self.interface, + self.protocol, + dbus.UInt32(0), # flags + self.name, self.type, + self.domain, self.host, + dbus.UInt16(self.port), + avahi.string_array_to_txt_array(self.TXT)) + self.group.Commit() + + def entry_group_state_changed(self, state, error): + """Derived from the Avahi example code""" + logger.debug("Avahi entry group state change: %i", state) + + if state == avahi.ENTRY_GROUP_ESTABLISHED: + logger.debug("Zeroconf service established.") + elif state == avahi.ENTRY_GROUP_COLLISION: + logger.info("Zeroconf service name collision.") + self.rename() + elif state == avahi.ENTRY_GROUP_FAILURE: + logger.critical("Avahi: Error in group state changed %s", + str(error)) + raise AvahiGroupError("State changed: {!s}".format(error)) + + def cleanup(self): + """Derived from the Avahi example code""" + if self.group is not None: + try: + self.group.Free() + except (dbus.exceptions.UnknownMethodException, + dbus.exceptions.DBusException): + pass + self.group = None + self.remove() + + def server_state_changed(self, state, error=None): + """Derived from the Avahi example code""" + logger.debug("Avahi server state change: %i", state) + bad_states = { + avahi.SERVER_INVALID: "Zeroconf server invalid", + avahi.SERVER_REGISTERING: None, + avahi.SERVER_COLLISION: "Zeroconf server name collision", + avahi.SERVER_FAILURE: "Zeroconf server failure", + } + if state in bad_states: + if bad_states[state] is not None: + if error is None: + logger.error(bad_states[state]) + else: + logger.error(bad_states[state] + ": %r", error) + self.cleanup() + elif state == avahi.SERVER_RUNNING: + try: + self.add() + except dbus.exceptions.DBusException as error: + if (error.get_dbus_name() + == "org.freedesktop.Avahi.CollisionError"): + logger.info("Local Zeroconf service name" + " collision.") + return self.rename(remove=False) + else: + logger.critical("D-Bus Exception", exc_info=error) + self.cleanup() + os._exit(1) + else: + if error is None: + logger.debug("Unknown state: %r", state) + else: + logger.debug("Unknown state: %r: %r", state, error) + + def activate(self): + """Derived from the Avahi example code""" + if self.server is None: + self.server = dbus.Interface( + self.bus.get_object(avahi.DBUS_NAME, + avahi.DBUS_PATH_SERVER, + follow_name_owner_changes=True), + avahi.DBUS_INTERFACE_SERVER) + self.server.connect_to_signal("StateChanged", + self.server_state_changed) + self.server_state_changed(self.server.GetState()) + + +class AvahiServiceToSyslog(AvahiService): + def rename(self, *args, **kwargs): + """Add the new name to the syslog messages""" + ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs) + syslogger.setFormatter(logging.Formatter( + 'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s' + .format(self.name))) + return ret + + +# Pretend that we have a GnuTLS module +class gnutls: + """This isn't so much a class as it is a module-like namespace.""" + + library = ctypes.util.find_library("gnutls") + if library is None: + library = ctypes.util.find_library("gnutls-deb0") + _library = ctypes.cdll.LoadLibrary(library) + del library + + # Unless otherwise indicated, the constants and types below are + # all from the gnutls/gnutls.h C header file. + + # Constants + E_SUCCESS = 0 + E_INTERRUPTED = -52 + E_AGAIN = -28 + CRT_OPENPGP = 2 + CRT_RAWPK = 3 + CLIENT = 2 + SHUT_RDWR = 0 + CRD_CERTIFICATE = 1 + E_NO_CERTIFICATE_FOUND = -49 + X509_FMT_DER = 0 + NO_TICKETS = 1<<10 + ENABLE_RAWPK = 1<<18 + CTYPE_PEERS = 3 + KEYID_USE_SHA256 = 1 # gnutls/x509.h + OPENPGP_FMT_RAW = 0 # gnutls/openpgp.h + + # Types + class session_int(ctypes.Structure): + _fields_ = [] + session_t = ctypes.POINTER(session_int) + + class certificate_credentials_st(ctypes.Structure): + _fields_ = [] + certificate_credentials_t = ctypes.POINTER( + certificate_credentials_st) + certificate_type_t = ctypes.c_int + + class datum_t(ctypes.Structure): + _fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)), + ('size', ctypes.c_uint)] + + class openpgp_crt_int(ctypes.Structure): + _fields_ = [] + openpgp_crt_t = ctypes.POINTER(openpgp_crt_int) + openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h + log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p) + credentials_type_t = ctypes.c_int + transport_ptr_t = ctypes.c_void_p + close_request_t = ctypes.c_int + + # Exceptions + class Error(Exception): + def __init__(self, message=None, code=None, args=()): + # Default usage is by a message string, but if a return + # code is passed, convert it to a string with + # gnutls.strerror() + self.code = code + if message is None and code is not None: + message = gnutls.strerror(code) + return super(gnutls.Error, self).__init__( + message, *args) + + class CertificateSecurityError(Error): + pass + + # Classes + class Credentials: + def __init__(self): + self._c_object = gnutls.certificate_credentials_t() + gnutls.certificate_allocate_credentials( + ctypes.byref(self._c_object)) + self.type = gnutls.CRD_CERTIFICATE + + def __del__(self): + gnutls.certificate_free_credentials(self._c_object) + + class ClientSession: + def __init__(self, socket, credentials=None): + self._c_object = gnutls.session_t() + gnutls_flags = gnutls.CLIENT + if gnutls.check_version(b"3.5.6"): + gnutls_flags |= gnutls.NO_TICKETS + if gnutls.has_rawpk: + gnutls_flags |= gnutls.ENABLE_RAWPK + gnutls.init(ctypes.byref(self._c_object), gnutls_flags) + del gnutls_flags + gnutls.set_default_priority(self._c_object) + gnutls.transport_set_ptr(self._c_object, socket.fileno()) + gnutls.handshake_set_private_extensions(self._c_object, + True) + self.socket = socket + if credentials is None: + credentials = gnutls.Credentials() + gnutls.credentials_set(self._c_object, credentials.type, + ctypes.cast(credentials._c_object, + ctypes.c_void_p)) + self.credentials = credentials + + def __del__(self): + gnutls.deinit(self._c_object) + + def handshake(self): + return gnutls.handshake(self._c_object) + + def send(self, data): + data = bytes(data) + data_len = len(data) + while data_len > 0: + data_len -= gnutls.record_send(self._c_object, + data[-data_len:], + data_len) + + def bye(self): + return gnutls.bye(self._c_object, gnutls.SHUT_RDWR) + + # Error handling functions + def _error_code(result): + """A function to raise exceptions on errors, suitable + for the 'restype' attribute on ctypes functions""" + if result >= 0: + return result + if result == gnutls.E_NO_CERTIFICATE_FOUND: + raise gnutls.CertificateSecurityError(code=result) + raise gnutls.Error(code=result) + + def _retry_on_error(result, func, arguments): + """A function to retry on some errors, suitable + for the 'errcheck' attribute on ctypes functions""" + while result < 0: + if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN): + return _error_code(result) + result = func(*arguments) + return result + + # Unless otherwise indicated, the function declarations below are + # all from the gnutls/gnutls.h C header file. + + # Functions + priority_set_direct = _library.gnutls_priority_set_direct + priority_set_direct.argtypes = [session_t, ctypes.c_char_p, + ctypes.POINTER(ctypes.c_char_p)] + priority_set_direct.restype = _error_code + + init = _library.gnutls_init + init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int] + init.restype = _error_code + + set_default_priority = _library.gnutls_set_default_priority + set_default_priority.argtypes = [session_t] + set_default_priority.restype = _error_code + + record_send = _library.gnutls_record_send + record_send.argtypes = [session_t, ctypes.c_void_p, + ctypes.c_size_t] + record_send.restype = ctypes.c_ssize_t + record_send.errcheck = _retry_on_error + + certificate_allocate_credentials = ( + _library.gnutls_certificate_allocate_credentials) + certificate_allocate_credentials.argtypes = [ + ctypes.POINTER(certificate_credentials_t)] + certificate_allocate_credentials.restype = _error_code + + certificate_free_credentials = ( + _library.gnutls_certificate_free_credentials) + certificate_free_credentials.argtypes = [ + certificate_credentials_t] + certificate_free_credentials.restype = None + + handshake_set_private_extensions = ( + _library.gnutls_handshake_set_private_extensions) + handshake_set_private_extensions.argtypes = [session_t, + ctypes.c_int] + handshake_set_private_extensions.restype = None + + credentials_set = _library.gnutls_credentials_set + credentials_set.argtypes = [session_t, credentials_type_t, + ctypes.c_void_p] + credentials_set.restype = _error_code + + strerror = _library.gnutls_strerror + strerror.argtypes = [ctypes.c_int] + strerror.restype = ctypes.c_char_p + + certificate_type_get = _library.gnutls_certificate_type_get + certificate_type_get.argtypes = [session_t] + certificate_type_get.restype = _error_code + + certificate_get_peers = _library.gnutls_certificate_get_peers + certificate_get_peers.argtypes = [session_t, + ctypes.POINTER(ctypes.c_uint)] + certificate_get_peers.restype = ctypes.POINTER(datum_t) + + global_set_log_level = _library.gnutls_global_set_log_level + global_set_log_level.argtypes = [ctypes.c_int] + global_set_log_level.restype = None + + global_set_log_function = _library.gnutls_global_set_log_function + global_set_log_function.argtypes = [log_func] + global_set_log_function.restype = None + + deinit = _library.gnutls_deinit + deinit.argtypes = [session_t] + deinit.restype = None + + handshake = _library.gnutls_handshake + handshake.argtypes = [session_t] + handshake.restype = _error_code + handshake.errcheck = _retry_on_error + + transport_set_ptr = _library.gnutls_transport_set_ptr + transport_set_ptr.argtypes = [session_t, transport_ptr_t] + transport_set_ptr.restype = None + + bye = _library.gnutls_bye + bye.argtypes = [session_t, close_request_t] + bye.restype = _error_code + bye.errcheck = _retry_on_error + + check_version = _library.gnutls_check_version + check_version.argtypes = [ctypes.c_char_p] + check_version.restype = ctypes.c_char_p + + _need_version = b"3.3.0" + if check_version(_need_version) is None: + raise self.Error("Needs GnuTLS {} or later" + .format(_need_version)) + + _tls_rawpk_version = b"3.6.6" + has_rawpk = bool(check_version(_tls_rawpk_version)) + + if has_rawpk: + # Types + class pubkey_st(ctypes.Structure): + _fields = [] + pubkey_t = ctypes.POINTER(pubkey_st) + + x509_crt_fmt_t = ctypes.c_int + + # All the function declarations below are from gnutls/abstract.h + pubkey_init = _library.gnutls_pubkey_init + pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)] + pubkey_init.restype = _error_code + + pubkey_import = _library.gnutls_pubkey_import + pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t), + x509_crt_fmt_t] + pubkey_import.restype = _error_code + + pubkey_get_key_id = _library.gnutls_pubkey_get_key_id + pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int, + ctypes.POINTER(ctypes.c_ubyte), + ctypes.POINTER(ctypes.c_size_t)] + pubkey_get_key_id.restype = _error_code + + pubkey_deinit = _library.gnutls_pubkey_deinit + pubkey_deinit.argtypes = [pubkey_t] + pubkey_deinit.restype = None + else: + # All the function declarations below are from gnutls/openpgp.h + + openpgp_crt_init = _library.gnutls_openpgp_crt_init + openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)] + openpgp_crt_init.restype = _error_code + + openpgp_crt_import = _library.gnutls_openpgp_crt_import + openpgp_crt_import.argtypes = [openpgp_crt_t, + ctypes.POINTER(datum_t), + openpgp_crt_fmt_t] + openpgp_crt_import.restype = _error_code + + openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self + openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint, + ctypes.POINTER(ctypes.c_uint)] + openpgp_crt_verify_self.restype = _error_code + + openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit + openpgp_crt_deinit.argtypes = [openpgp_crt_t] + openpgp_crt_deinit.restype = None + + openpgp_crt_get_fingerprint = ( + _library.gnutls_openpgp_crt_get_fingerprint) + openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t, + ctypes.c_void_p, + ctypes.POINTER( + ctypes.c_size_t)] + openpgp_crt_get_fingerprint.restype = _error_code + + if check_version(b"3.6.4"): + certificate_type_get2 = _library.gnutls_certificate_type_get2 + certificate_type_get2.argtypes = [session_t, ctypes.c_int] + certificate_type_get2.restype = _error_code + + # Remove non-public functions + del _error_code, _retry_on_error + + +def call_pipe(connection, # : multiprocessing.Connection + func, *args, **kwargs): + """This function is meant to be called by multiprocessing.Process + + This function runs func(*args, **kwargs), and writes the resulting + return value on the provided multiprocessing.Connection. + """ + connection.send(func(*args, **kwargs)) + connection.close() + + +class Client: """A representation of a client host served by this server. + Attributes: - name: string; from the config file, used in log messages - fingerprint: string (40 or 32 hexadecimal digits); used to - uniquely identify the client - secret: bytestring; sent verbatim (over TLS) to client - fqdn: string (FQDN); available for use by the checker command - created: datetime.datetime() - last_seen: datetime.datetime() or None if not yet seen - timeout: datetime.timedelta(); How long from last_seen until - this client is invalid - interval: datetime.timedelta(); How often to start a new checker - stop_hook: If set, called by stop() as stop_hook(self) - checker: subprocess.Popen(); a running checker process used - to see if the client lives. - Is None if no process is running. - checker_initiator_tag: a gobject event source tag, or None - stop_initiator_tag: - '' - - checker_callback_tag: - '' - - checker_command: string; External command which is run to check if - client lives. %()s expansions are done at + approved: bool(); 'None' if not yet approved/disapproved + approval_delay: datetime.timedelta(); Time to wait for approval + approval_duration: datetime.timedelta(); Duration of one approval + checker: multiprocessing.Process(); a running checker process used + to see if the client lives. 'None' if no process is + running. + checker_callback_tag: a GLib event source tag, or None + checker_command: string; External command which is run to check + if client lives. %() expansions are done at runtime with vars(self) as dict, so that for instance %(name)s can be used in the command. - Private attibutes: - _timeout: Real variable for 'timeout' - _interval: Real variable for 'interval' - _timeout_milliseconds: Used by gobject.timeout_add() - _interval_milliseconds: - '' - + checker_initiator_tag: a GLib event source tag, or None + created: datetime.datetime(); (UTC) object creation + client_structure: Object describing what attributes a client has + and is used for storing the client at exit + current_checker_command: string; current running checker_command + disable_initiator_tag: a GLib event source tag, or None + enabled: bool() + fingerprint: string (40 or 32 hexadecimal digits); used to + uniquely identify an OpenPGP client + key_id: string (64 hexadecimal digits); used to uniquely identify + a client using raw public keys + host: string; available for use by the checker command + interval: datetime.timedelta(); How often to start a new checker + last_approval_request: datetime.datetime(); (UTC) or None + last_checked_ok: datetime.datetime(); (UTC) or None + last_checker_status: integer between 0 and 255 reflecting exit + status of last checker. -1 reflects crashed + checker, -2 means no checker completed yet. + last_checker_signal: The signal which killed the last checker, if + last_checker_status is -1 + last_enabled: datetime.datetime(); (UTC) or None + name: string; from the config file, used in log messages and + D-Bus identifiers + secret: bytestring; sent verbatim (over TLS) to client + timeout: datetime.timedelta(); How long from last_checked_ok + until this client is disabled + extended_timeout: extra long timeout when secret has been sent + runtime_expansions: Allowed attributes for runtime expansion. + expires: datetime.datetime(); time (UTC) when a client will be + disabled, or None + server_settings: The server_settings dict from main() """ - def _set_timeout(self, timeout): - "Setter function for 'timeout' attribute" - self._timeout = timeout - self._timeout_milliseconds = ((self.timeout.days - * 24 * 60 * 60 * 1000) - + (self.timeout.seconds * 1000) - + (self.timeout.microseconds - // 1000)) - timeout = property(lambda self: self._timeout, - _set_timeout) - del _set_timeout - def _set_interval(self, interval): - "Setter function for 'interval' attribute" - self._interval = interval - self._interval_milliseconds = ((self.interval.days - * 24 * 60 * 60 * 1000) - + (self.interval.seconds - * 1000) - + (self.interval.microseconds - // 1000)) - interval = property(lambda self: self._interval, - _set_interval) - del _set_interval - def __init__(self, name=None, stop_hook=None, fingerprint=None, - secret=None, secfile=None, fqdn=None, timeout=None, - interval=-1, checker=None): - """Note: the 'checker' argument sets the 'checker_command' - attribute and not the 'checker' attribute..""" + + runtime_expansions = ("approval_delay", "approval_duration", + "created", "enabled", "expires", "key_id", + "fingerprint", "host", "interval", + "last_approval_request", "last_checked_ok", + "last_enabled", "name", "timeout") + client_defaults = { + "timeout": "PT5M", + "extended_timeout": "PT15M", + "interval": "PT2M", + "checker": "fping -q -- %%(host)s", + "host": "", + "approval_delay": "PT0S", + "approval_duration": "PT1S", + "approved_by_default": "True", + "enabled": "True", + } + + @staticmethod + def config_parser(config): + """Construct a new dict of client settings of this form: + { client_name: {setting_name: value, ...}, ...} + with exceptions for any special settings as defined above. + NOTE: Must be a pure function. Must return the same result + value given the same arguments. + """ + settings = {} + for client_name in config.sections(): + section = dict(config.items(client_name)) + client = settings[client_name] = {} + + client["host"] = section["host"] + # Reformat values from string types to Python types + client["approved_by_default"] = config.getboolean( + client_name, "approved_by_default") + client["enabled"] = config.getboolean(client_name, + "enabled") + + # Uppercase and remove spaces from key_id and fingerprint + # for later comparison purposes with return value from the + # key_id() and fingerprint() functions + client["key_id"] = (section.get("key_id", "").upper() + .replace(" ", "")) + client["fingerprint"] = (section["fingerprint"].upper() + .replace(" ", "")) + if "secret" in section: + client["secret"] = codecs.decode(section["secret"] + .encode("utf-8"), + "base64") + elif "secfile" in section: + with open(os.path.expanduser(os.path.expandvars + (section["secfile"])), + "rb") as secfile: + client["secret"] = secfile.read() + else: + raise TypeError("No secret or secfile for section {}" + .format(section)) + client["timeout"] = string_to_delta(section["timeout"]) + client["extended_timeout"] = string_to_delta( + section["extended_timeout"]) + client["interval"] = string_to_delta(section["interval"]) + client["approval_delay"] = string_to_delta( + section["approval_delay"]) + client["approval_duration"] = string_to_delta( + section["approval_duration"]) + client["checker_command"] = section["checker"] + client["last_approval_request"] = None + client["last_checked_ok"] = None + client["last_checker_status"] = -2 + + return settings + + def __init__(self, settings, name=None, server_settings=None): self.name = name - logger.debug(u"Creating client %r", self.name) - # Uppercase and remove spaces from fingerprint - # for later comparison purposes with return value of - # the fingerprint() function - self.fingerprint = fingerprint.upper().replace(u" ", u"") - logger.debug(u" Fingerprint: %s", self.fingerprint) - if secret: - self.secret = secret.decode(u"base64") - elif secfile: - sf = open(secfile) - self.secret = sf.read() - sf.close() + if server_settings is None: + server_settings = {} + self.server_settings = server_settings + # adding all client settings + for setting, value in settings.items(): + setattr(self, setting, value) + + if self.enabled: + if not hasattr(self, "last_enabled"): + self.last_enabled = datetime.datetime.utcnow() + if not hasattr(self, "expires"): + self.expires = (datetime.datetime.utcnow() + + self.timeout) else: - raise RuntimeError(u"No secret or secfile for client %s" - % self.name) - self.fqdn = fqdn # string - self.created = datetime.datetime.now() - self.last_seen = None - self.timeout = string_to_delta(timeout) - self.interval = string_to_delta(interval) - self.stop_hook = stop_hook + self.last_enabled = None + self.expires = None + + logger.debug("Creating client %r", self.name) + logger.debug(" Key ID: %s", self.key_id) + logger.debug(" Fingerprint: %s", self.fingerprint) + self.created = settings.get("created", + datetime.datetime.utcnow()) + + # attributes specific for this server instance self.checker = None self.checker_initiator_tag = None - self.stop_initiator_tag = None + self.disable_initiator_tag = None self.checker_callback_tag = None - self.check_command = checker - def start(self): + self.current_checker_command = None + self.approved = None + self.approvals_pending = 0 + self.changedstate = multiprocessing_manager.Condition( + multiprocessing_manager.Lock()) + self.client_structure = [attr + for attr in self.__dict__.keys() + if not attr.startswith("_")] + self.client_structure.append("client_structure") + + for name, t in inspect.getmembers( + type(self), lambda obj: isinstance(obj, property)): + if not name.startswith("_"): + self.client_structure.append(name) + + # Send notice to process children that client state has changed + def send_changedstate(self): + with self.changedstate: + self.changedstate.notify_all() + + def enable(self): """Start this client's checker and timeout hooks""" + if getattr(self, "enabled", False): + # Already enabled + return + self.expires = datetime.datetime.utcnow() + self.timeout + self.enabled = True + self.last_enabled = datetime.datetime.utcnow() + self.init_checker() + self.send_changedstate() + + def disable(self, quiet=True): + """Disable this client.""" + if not getattr(self, "enabled", False): + return False + if not quiet: + logger.info("Disabling client %s", self.name) + if getattr(self, "disable_initiator_tag", None) is not None: + GLib.source_remove(self.disable_initiator_tag) + self.disable_initiator_tag = None + self.expires = None + if getattr(self, "checker_initiator_tag", None) is not None: + GLib.source_remove(self.checker_initiator_tag) + self.checker_initiator_tag = None + self.stop_checker() + self.enabled = False + if not quiet: + self.send_changedstate() + # Do not run this again if called by a GLib.timeout_add + return False + + def __del__(self): + self.disable() + + def init_checker(self): # Schedule a new checker to be started an 'interval' from now, # and every interval from then on. - self.checker_initiator_tag = gobject.timeout_add\ - (self._interval_milliseconds, - self.start_checker) + if self.checker_initiator_tag is not None: + GLib.source_remove(self.checker_initiator_tag) + self.checker_initiator_tag = GLib.timeout_add( + int(self.interval.total_seconds() * 1000), + self.start_checker) + # Schedule a disable() when 'timeout' has passed + if self.disable_initiator_tag is not None: + GLib.source_remove(self.disable_initiator_tag) + self.disable_initiator_tag = GLib.timeout_add( + int(self.timeout.total_seconds() * 1000), self.disable) # Also start a new checker *right now*. self.start_checker() - # Schedule a stop() when 'timeout' has passed - self.stop_initiator_tag = gobject.timeout_add\ - (self._timeout_milliseconds, - self.stop) - def stop(self): - """Stop this client. - The possibility that this client might be restarted is left - open, but not currently used.""" - # If this client doesn't have a secret, it is already stopped. - if self.secret: - logger.debug(u"Stopping client %s", self.name) - self.secret = None - else: - return False - if hasattr(self, "stop_initiator_tag") \ - and self.stop_initiator_tag: - gobject.source_remove(self.stop_initiator_tag) - self.stop_initiator_tag = None - if hasattr(self, "checker_initiator_tag") \ - and self.checker_initiator_tag: - gobject.source_remove(self.checker_initiator_tag) - self.checker_initiator_tag = None - self.stop_checker() - if self.stop_hook: - self.stop_hook(self) - # Do not run this again if called by a gobject.timeout_add - return False - def __del__(self): - self.stop_hook = None - self.stop() - def checker_callback(self, pid, condition): + + def checker_callback(self, source, condition, connection, + command): """The checker has completed, so take appropriate actions.""" - now = datetime.datetime.now() + # Read return code from connection (see call_pipe) + returncode = connection.recv() + connection.close() + if self.checker is not None: + self.checker.join() self.checker_callback_tag = None self.checker = None - if os.WIFEXITED(condition) \ - and (os.WEXITSTATUS(condition) == 0): - logger.debug(u"Checker for %(name)s succeeded", - vars(self)) - self.last_seen = now - gobject.source_remove(self.stop_initiator_tag) - self.stop_initiator_tag = gobject.timeout_add\ - (self._timeout_milliseconds, - self.stop) - elif not os.WIFEXITED(condition): - logger.warning(u"Checker for %(name)s crashed?", + + if returncode >= 0: + self.last_checker_status = returncode + self.last_checker_signal = None + if self.last_checker_status == 0: + logger.info("Checker for %(name)s succeeded", + vars(self)) + self.checked_ok() + else: + logger.info("Checker for %(name)s failed", vars(self)) + else: + self.last_checker_status = -1 + self.last_checker_signal = -returncode + logger.warning("Checker for %(name)s crashed?", vars(self)) - else: - logger.debug(u"Checker for %(name)s failed", - vars(self)) + return False + + def checked_ok(self): + """Assert that the client has been seen, alive and well.""" + self.last_checked_ok = datetime.datetime.utcnow() + self.last_checker_status = 0 + self.last_checker_signal = None + self.bump_timeout() + + def bump_timeout(self, timeout=None): + """Bump up the timeout for this client.""" + if timeout is None: + timeout = self.timeout + if self.disable_initiator_tag is not None: + GLib.source_remove(self.disable_initiator_tag) + self.disable_initiator_tag = None + if getattr(self, "enabled", False): + self.disable_initiator_tag = GLib.timeout_add( + int(timeout.total_seconds() * 1000), self.disable) + self.expires = datetime.datetime.utcnow() + timeout + + def need_approval(self): + self.last_approval_request = datetime.datetime.utcnow() + def start_checker(self): """Start a new checker subprocess if one is not running. + If a checker already exists, leave it running and do nothing.""" # The reason for not killing a running checker is that if we - # did that, then if a checker (for some reason) started - # running slowly and taking more than 'interval' time, the - # client would inevitably timeout, since no checker would get - # a chance to run to completion. If we instead leave running + # did that, and if a checker (for some reason) started running + # slowly and taking more than 'interval' time, then the client + # would inevitably timeout, since no checker would get a + # chance to run to completion. If we instead leave running # checkers alone, the checker would have to take more time - # than 'timeout' for the client to be declared invalid, which - # is as it should be. + # than 'timeout' for the client to be disabled, which is as it + # should be. + + if self.checker is not None and not self.checker.is_alive(): + logger.warning("Checker was not alive; joining") + self.checker.join() + self.checker = None + # Start a new checker if needed if self.checker is None: - try: - command = self.check_command % self.fqdn - except TypeError: - escaped_attrs = dict((key, re.escape(str(val))) - for key, val in - vars(self).iteritems()) - try: - command = self.check_command % escaped_attrs - except TypeError, error: - logger.critical(u'Could not format string "%s":' - u' %s', self.check_command, error) - return True # Try again later - try: - logger.debug(u"Starting checker %r for %s", - command, self.name) - self.checker = subprocess.\ - Popen(command, - close_fds=True, shell=True, - cwd="/") - self.checker_callback_tag = gobject.child_watch_add\ - (self.checker.pid, - self.checker_callback) - except subprocess.OSError, error: - logger.error(u"Failed to start subprocess: %s", - error) - # Re-run this periodically if run by gobject.timeout_add + # Escape attributes for the shell + escaped_attrs = { + attr: re.escape(str(getattr(self, attr))) + for attr in self.runtime_expansions} + try: + command = self.checker_command % escaped_attrs + except TypeError as error: + logger.error('Could not format string "%s"', + self.checker_command, + exc_info=error) + return True # Try again later + self.current_checker_command = command + logger.info("Starting checker %r for %s", command, + self.name) + # We don't need to redirect stdout and stderr, since + # in normal mode, that is already done by daemon(), + # and in debug mode we don't want to. (Stdin is + # always replaced by /dev/null.) + # The exception is when not debugging but nevertheless + # running in the foreground; use the previously + # created wnull. + popen_args = {"close_fds": True, + "shell": True, + "cwd": "/"} + if (not self.server_settings["debug"] + and self.server_settings["foreground"]): + popen_args.update({"stdout": wnull, + "stderr": wnull}) + pipe = multiprocessing.Pipe(duplex=False) + self.checker = multiprocessing.Process( + target=call_pipe, + args=(pipe[1], subprocess.call, command), + kwargs=popen_args) + self.checker.start() + self.checker_callback_tag = GLib.io_add_watch( + GLib.IOChannel.unix_new(pipe[0].fileno()), + GLib.PRIORITY_DEFAULT, GLib.IO_IN, + self.checker_callback, pipe[0], command) + # Re-run this periodically if run by GLib.timeout_add return True + def stop_checker(self): """Force the checker process, if any, to stop.""" if self.checker_callback_tag: - gobject.source_remove(self.checker_callback_tag) + GLib.source_remove(self.checker_callback_tag) self.checker_callback_tag = None - if not hasattr(self, "checker") or self.checker is None: + if getattr(self, "checker", None) is None: return logger.debug("Stopping checker for %(name)s", vars(self)) - try: - os.kill(self.checker.pid, signal.SIGTERM) - #os.sleep(0.5) - #if self.checker.poll() is None: - # os.kill(self.checker.pid, signal.SIGKILL) - except OSError, error: - if error.errno != errno.ESRCH: - raise + self.checker.terminate() self.checker = None - def still_valid(self, now=None): - """Has the timeout not yet passed for this client?""" - if now is None: - now = datetime.datetime.now() - if self.last_seen is None: - return now < (self.created + self.timeout) - else: - return now < (self.last_seen + self.timeout) - - -def peer_certificate(session): - "Return the peer's OpenPGP certificate as a bytestring" - # If not an OpenPGP certificate... - if gnutls.library.functions.gnutls_certificate_type_get\ - (session._c_object) \ - != gnutls.library.constants.GNUTLS_CRT_OPENPGP: - # ...do the normal thing - return session.peer_certificate - list_size = ctypes.c_uint() - cert_list = gnutls.library.functions.gnutls_certificate_get_peers\ - (session._c_object, ctypes.byref(list_size)) - if list_size.value == 0: - return None - cert = cert_list[0] - return ctypes.string_at(cert.data, cert.size) - - -def fingerprint(openpgp): - "Convert an OpenPGP bytestring to a hexdigit fingerprint string" - # New empty GnuTLS certificate - crt = gnutls.library.types.gnutls_openpgp_crt_t() - gnutls.library.functions.gnutls_openpgp_crt_init\ - (ctypes.byref(crt)) - # New GnuTLS "datum" with the OpenPGP public key - datum = gnutls.library.types.gnutls_datum_t\ - (ctypes.cast(ctypes.c_char_p(openpgp), - ctypes.POINTER(ctypes.c_ubyte)), - ctypes.c_uint(len(openpgp))) - # Import the OpenPGP public key into the certificate - ret = gnutls.library.functions.gnutls_openpgp_crt_import\ - (crt, - ctypes.byref(datum), - gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW) - # New buffer for the fingerprint - buffer = ctypes.create_string_buffer(20) - buffer_length = ctypes.c_size_t() - # Get the fingerprint from the certificate into the buffer - gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\ - (crt, ctypes.byref(buffer), ctypes.byref(buffer_length)) - # Deinit the certificate - gnutls.library.functions.gnutls_openpgp_crt_deinit(crt) - # Convert the buffer to a Python bytestring - fpr = ctypes.string_at(buffer, buffer_length.value) - # Convert the bytestring to hexadecimal notation - hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr) - return hex_fpr - - -class tcp_handler(SocketServer.BaseRequestHandler, object): - """A TCP request handler class. - Instantiated by IPv6_TCPServer for each request to handle it. + + +def dbus_service_property(dbus_interface, + signature="v", + access="readwrite", + byte_arrays=False): + """Decorators for marking methods of a DBusObjectWithProperties to + become properties on the D-Bus. + + The decorated method will be called with no arguments by "Get" + and with one argument by "Set". + + The parameters, where they are supported, are the same as + dbus.service.method, except there is only "signature", since the + type from Get() and the type sent to Set() is the same. + """ + # Encoding deeply encoded byte arrays is not supported yet by the + # "Set" method, so we fail early here: + if byte_arrays and signature != "ay": + raise ValueError("Byte arrays not supported for non-'ay'" + " signature {!r}".format(signature)) + + def decorator(func): + func._dbus_is_property = True + func._dbus_interface = dbus_interface + func._dbus_signature = signature + func._dbus_access = access + func._dbus_name = func.__name__ + if func._dbus_name.endswith("_dbus_property"): + func._dbus_name = func._dbus_name[:-14] + func._dbus_get_args_options = {'byte_arrays': byte_arrays} + return func + + return decorator + + +def dbus_interface_annotations(dbus_interface): + """Decorator for marking functions returning interface annotations + + Usage: + + @dbus_interface_annotations("org.example.Interface") + def _foo(self): # Function name does not matter + return {"org.freedesktop.DBus.Deprecated": "true", + "org.freedesktop.DBus.Property.EmitsChangedSignal": + "false"} + """ + + def decorator(func): + func._dbus_is_interface = True + func._dbus_interface = dbus_interface + func._dbus_name = dbus_interface + return func + + return decorator + + +def dbus_annotations(annotations): + """Decorator to annotate D-Bus methods, signals or properties + Usage: + + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true", + "org.freedesktop.DBus.Property." + "EmitsChangedSignal": "false"}) + @dbus_service_property("org.example.Interface", signature="b", + access="r") + def Property_dbus_property(self): + return dbus.Boolean(False) + + See also the DBusObjectWithAnnotations class. + """ + + def decorator(func): + func._dbus_annotations = annotations + return func + + return decorator + + +class DBusPropertyException(dbus.exceptions.DBusException): + """A base class for D-Bus property-related exceptions + """ + pass + + +class DBusPropertyAccessException(DBusPropertyException): + """A property's access permissions disallows an operation. + """ + pass + + +class DBusPropertyNotFound(DBusPropertyException): + """An attempt was made to access a non-existing property. + """ + pass + + +class DBusObjectWithAnnotations(dbus.service.Object): + """A D-Bus object with annotations. + + Classes inheriting from this can use the dbus_annotations + decorator to add annotations to methods or signals. + """ + + @staticmethod + def _is_dbus_thing(thing): + """Returns a function testing if an attribute is a D-Bus thing + + If called like _is_dbus_thing("method") it returns a function + suitable for use as predicate to inspect.getmembers(). + """ + return lambda obj: getattr(obj, "_dbus_is_{}".format(thing), + False) + + def _get_all_dbus_things(self, thing): + """Returns a generator of (name, attribute) pairs + """ + return ((getattr(athing.__get__(self), "_dbus_name", name), + athing.__get__(self)) + for cls in self.__class__.__mro__ + for name, athing in + inspect.getmembers(cls, self._is_dbus_thing(thing))) + + @dbus.service.method(dbus.INTROSPECTABLE_IFACE, + out_signature="s", + path_keyword='object_path', + connection_keyword='connection') + def Introspect(self, object_path, connection): + """Overloading of standard D-Bus method. + + Inserts annotation tags on methods and signals. + """ + xmlstring = dbus.service.Object.Introspect(self, object_path, + connection) + try: + document = xml.dom.minidom.parseString(xmlstring) + + for if_tag in document.getElementsByTagName("interface"): + # Add annotation tags + for typ in ("method", "signal"): + for tag in if_tag.getElementsByTagName(typ): + annots = dict() + for name, prop in (self. + _get_all_dbus_things(typ)): + if (name == tag.getAttribute("name") + and prop._dbus_interface + == if_tag.getAttribute("name")): + annots.update(getattr( + prop, "_dbus_annotations", {})) + for name, value in annots.items(): + ann_tag = document.createElement( + "annotation") + ann_tag.setAttribute("name", name) + ann_tag.setAttribute("value", value) + tag.appendChild(ann_tag) + # Add interface annotation tags + for annotation, value in dict( + itertools.chain.from_iterable( + annotations().items() + for name, annotations + in self._get_all_dbus_things("interface") + if name == if_tag.getAttribute("name") + )).items(): + ann_tag = document.createElement("annotation") + ann_tag.setAttribute("name", annotation) + ann_tag.setAttribute("value", value) + if_tag.appendChild(ann_tag) + # Fix argument name for the Introspect method itself + if (if_tag.getAttribute("name") + == dbus.INTROSPECTABLE_IFACE): + for cn in if_tag.getElementsByTagName("method"): + if cn.getAttribute("name") == "Introspect": + for arg in cn.getElementsByTagName("arg"): + if (arg.getAttribute("direction") + == "out"): + arg.setAttribute("name", + "xml_data") + xmlstring = document.toxml("utf-8") + document.unlink() + except (AttributeError, xml.dom.DOMException, + xml.parsers.expat.ExpatError) as error: + logger.error("Failed to override Introspection method", + exc_info=error) + return xmlstring + + +class DBusObjectWithProperties(DBusObjectWithAnnotations): + """A D-Bus object with properties. + + Classes inheriting from this can use the dbus_service_property + decorator to expose methods as D-Bus properties. It exposes the + standard Get(), Set(), and GetAll() methods on the D-Bus. + """ + + def _get_dbus_property(self, interface_name, property_name): + """Returns a bound method if one exists which is a D-Bus + property with the specified name and interface. + """ + for cls in self.__class__.__mro__: + for name, value in inspect.getmembers( + cls, self._is_dbus_thing("property")): + if (value._dbus_name == property_name + and value._dbus_interface == interface_name): + return value.__get__(self) + + # No such property + raise DBusPropertyNotFound("{}:{}.{}".format( + self.dbus_object_path, interface_name, property_name)) + + @classmethod + def _get_all_interface_names(cls): + """Get a sequence of all interfaces supported by an object""" + return (name for name in set(getattr(getattr(x, attr), + "_dbus_interface", None) + for x in (inspect.getmro(cls)) + for attr in dir(x)) + if name is not None) + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature="ss", + out_signature="v") + def Get(self, interface_name, property_name): + """Standard D-Bus property Get() method, see D-Bus standard. + """ + prop = self._get_dbus_property(interface_name, property_name) + if prop._dbus_access == "write": + raise DBusPropertyAccessException(property_name) + value = prop() + if not hasattr(value, "variant_level"): + return value + return type(value)(value, variant_level=value.variant_level+1) + + @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ssv") + def Set(self, interface_name, property_name, value): + """Standard D-Bus property Set() method, see D-Bus standard. + """ + prop = self._get_dbus_property(interface_name, property_name) + if prop._dbus_access == "read": + raise DBusPropertyAccessException(property_name) + if prop._dbus_get_args_options["byte_arrays"]: + # The byte_arrays option is not supported yet on + # signatures other than "ay". + if prop._dbus_signature != "ay": + raise ValueError("Byte arrays not supported for non-" + "'ay' signature {!r}" + .format(prop._dbus_signature)) + value = dbus.ByteArray(b''.join(chr(byte) + for byte in value)) + prop(value) + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature="s", + out_signature="a{sv}") + def GetAll(self, interface_name): + """Standard D-Bus property GetAll() method, see D-Bus + standard. + + Note: Will not include properties with access="write". + """ + properties = {} + for name, prop in self._get_all_dbus_things("property"): + if (interface_name + and interface_name != prop._dbus_interface): + # Interface non-empty but did not match + continue + # Ignore write-only properties + if prop._dbus_access == "write": + continue + value = prop() + if not hasattr(value, "variant_level"): + properties[name] = value + continue + properties[name] = type(value)( + value, variant_level=value.variant_level + 1) + return dbus.Dictionary(properties, signature="sv") + + @dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as") + def PropertiesChanged(self, interface_name, changed_properties, + invalidated_properties): + """Standard D-Bus PropertiesChanged() signal, see D-Bus + standard. + """ + pass + + @dbus.service.method(dbus.INTROSPECTABLE_IFACE, + out_signature="s", + path_keyword='object_path', + connection_keyword='connection') + def Introspect(self, object_path, connection): + """Overloading of standard D-Bus method. + + Inserts property tags and interface annotation tags. + """ + xmlstring = DBusObjectWithAnnotations.Introspect(self, + object_path, + connection) + try: + document = xml.dom.minidom.parseString(xmlstring) + + def make_tag(document, name, prop): + e = document.createElement("property") + e.setAttribute("name", name) + e.setAttribute("type", prop._dbus_signature) + e.setAttribute("access", prop._dbus_access) + return e + + for if_tag in document.getElementsByTagName("interface"): + # Add property tags + for tag in (make_tag(document, name, prop) + for name, prop + in self._get_all_dbus_things("property") + if prop._dbus_interface + == if_tag.getAttribute("name")): + if_tag.appendChild(tag) + # Add annotation tags for properties + for tag in if_tag.getElementsByTagName("property"): + annots = dict() + for name, prop in self._get_all_dbus_things( + "property"): + if (name == tag.getAttribute("name") + and prop._dbus_interface + == if_tag.getAttribute("name")): + annots.update(getattr( + prop, "_dbus_annotations", {})) + for name, value in annots.items(): + ann_tag = document.createElement( + "annotation") + ann_tag.setAttribute("name", name) + ann_tag.setAttribute("value", value) + tag.appendChild(ann_tag) + # Add the names to the return values for the + # "org.freedesktop.DBus.Properties" methods + if (if_tag.getAttribute("name") + == "org.freedesktop.DBus.Properties"): + for cn in if_tag.getElementsByTagName("method"): + if cn.getAttribute("name") == "Get": + for arg in cn.getElementsByTagName("arg"): + if (arg.getAttribute("direction") + == "out"): + arg.setAttribute("name", "value") + elif cn.getAttribute("name") == "GetAll": + for arg in cn.getElementsByTagName("arg"): + if (arg.getAttribute("direction") + == "out"): + arg.setAttribute("name", "props") + xmlstring = document.toxml("utf-8") + document.unlink() + except (AttributeError, xml.dom.DOMException, + xml.parsers.expat.ExpatError) as error: + logger.error("Failed to override Introspection method", + exc_info=error) + return xmlstring + + +try: + dbus.OBJECT_MANAGER_IFACE +except AttributeError: + dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager" + + +class DBusObjectWithObjectManager(DBusObjectWithAnnotations): + """A D-Bus object with an ObjectManager. + + Classes inheriting from this exposes the standard + GetManagedObjects call and the InterfacesAdded and + InterfacesRemoved signals on the standard + "org.freedesktop.DBus.ObjectManager" interface. + + Note: No signals are sent automatically; they must be sent + manually. + """ + @dbus.service.method(dbus.OBJECT_MANAGER_IFACE, + out_signature="a{oa{sa{sv}}}") + def GetManagedObjects(self): + """This function must be overridden""" + raise NotImplementedError() + + @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, + signature="oa{sa{sv}}") + def InterfacesAdded(self, object_path, interfaces_and_properties): + pass + + @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature="oas") + def InterfacesRemoved(self, object_path, interfaces): + pass + + @dbus.service.method(dbus.INTROSPECTABLE_IFACE, + out_signature="s", + path_keyword='object_path', + connection_keyword='connection') + def Introspect(self, object_path, connection): + """Overloading of standard D-Bus method. + + Override return argument name of GetManagedObjects to be + "objpath_interfaces_and_properties" + """ + xmlstring = DBusObjectWithAnnotations.Introspect(self, + object_path, + connection) + try: + document = xml.dom.minidom.parseString(xmlstring) + + for if_tag in document.getElementsByTagName("interface"): + # Fix argument name for the GetManagedObjects method + if (if_tag.getAttribute("name") + == dbus.OBJECT_MANAGER_IFACE): + for cn in if_tag.getElementsByTagName("method"): + if (cn.getAttribute("name") + == "GetManagedObjects"): + for arg in cn.getElementsByTagName("arg"): + if (arg.getAttribute("direction") + == "out"): + arg.setAttribute( + "name", + "objpath_interfaces" + "_and_properties") + xmlstring = document.toxml("utf-8") + document.unlink() + except (AttributeError, xml.dom.DOMException, + xml.parsers.expat.ExpatError) as error: + logger.error("Failed to override Introspection method", + exc_info=error) + return xmlstring + + +def datetime_to_dbus(dt, variant_level=0): + """Convert a UTC datetime.datetime() to a D-Bus type.""" + if dt is None: + return dbus.String("", variant_level=variant_level) + return dbus.String(dt.isoformat(), variant_level=variant_level) + + +def alternate_dbus_interfaces(alt_interface_names, deprecate=True): + """A class decorator; applied to a subclass of + dbus.service.Object, it will add alternate D-Bus attributes with + interface names according to the "alt_interface_names" mapping. + Usage: + + @alternate_dbus_interfaces({"org.example.Interface": + "net.example.AlternateInterface"}) + class SampleDBusObject(dbus.service.Object): + @dbus.service.method("org.example.Interface") + def SampleDBusMethod(): + pass + + The above "SampleDBusMethod" on "SampleDBusObject" will be + reachable via two interfaces: "org.example.Interface" and + "net.example.AlternateInterface", the latter of which will have + its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to + "true", unless "deprecate" is passed with a False value. + + This works for methods and signals, and also for D-Bus properties + (from DBusObjectWithProperties) and interfaces (from the + dbus_interface_annotations decorator). + """ + + def wrapper(cls): + for orig_interface_name, alt_interface_name in ( + alt_interface_names.items()): + attr = {} + interface_names = set() + # Go though all attributes of the class + for attrname, attribute in inspect.getmembers(cls): + # Ignore non-D-Bus attributes, and D-Bus attributes + # with the wrong interface name + if (not hasattr(attribute, "_dbus_interface") + or not attribute._dbus_interface.startswith( + orig_interface_name)): + continue + # Create an alternate D-Bus interface name based on + # the current name + alt_interface = attribute._dbus_interface.replace( + orig_interface_name, alt_interface_name) + interface_names.add(alt_interface) + # Is this a D-Bus signal? + if getattr(attribute, "_dbus_is_signal", False): + # Extract the original non-method undecorated + # function by black magic + if sys.version_info.major == 2: + nonmethod_func = (dict( + zip(attribute.func_code.co_freevars, + attribute.__closure__)) + ["func"].cell_contents) + else: + nonmethod_func = (dict( + zip(attribute.__code__.co_freevars, + attribute.__closure__)) + ["func"].cell_contents) + # Create a new, but exactly alike, function + # object, and decorate it to be a new D-Bus signal + # with the alternate D-Bus interface name + new_function = copy_function(nonmethod_func) + new_function = (dbus.service.signal( + alt_interface, + attribute._dbus_signature)(new_function)) + # Copy annotations, if any + try: + new_function._dbus_annotations = dict( + attribute._dbus_annotations) + except AttributeError: + pass + + # Define a creator of a function to call both the + # original and alternate functions, so both the + # original and alternate signals gets sent when + # the function is called + def fixscope(func1, func2): + """This function is a scope container to pass + func1 and func2 to the "call_both" function + outside of its arguments""" + + @functools.wraps(func2) + def call_both(*args, **kwargs): + """This function will emit two D-Bus + signals by calling func1 and func2""" + func1(*args, **kwargs) + func2(*args, **kwargs) + # Make wrapper function look like a D-Bus + # signal + for name, attr in inspect.getmembers(func2): + if name.startswith("_dbus_"): + setattr(call_both, name, attr) + + return call_both + # Create the "call_both" function and add it to + # the class + attr[attrname] = fixscope(attribute, new_function) + # Is this a D-Bus method? + elif getattr(attribute, "_dbus_is_method", False): + # Create a new, but exactly alike, function + # object. Decorate it to be a new D-Bus method + # with the alternate D-Bus interface name. Add it + # to the class. + attr[attrname] = ( + dbus.service.method( + alt_interface, + attribute._dbus_in_signature, + attribute._dbus_out_signature) + (copy_function(attribute))) + # Copy annotations, if any + try: + attr[attrname]._dbus_annotations = dict( + attribute._dbus_annotations) + except AttributeError: + pass + # Is this a D-Bus property? + elif getattr(attribute, "_dbus_is_property", False): + # Create a new, but exactly alike, function + # object, and decorate it to be a new D-Bus + # property with the alternate D-Bus interface + # name. Add it to the class. + attr[attrname] = (dbus_service_property( + alt_interface, attribute._dbus_signature, + attribute._dbus_access, + attribute._dbus_get_args_options + ["byte_arrays"]) + (copy_function(attribute))) + # Copy annotations, if any + try: + attr[attrname]._dbus_annotations = dict( + attribute._dbus_annotations) + except AttributeError: + pass + # Is this a D-Bus interface? + elif getattr(attribute, "_dbus_is_interface", False): + # Create a new, but exactly alike, function + # object. Decorate it to be a new D-Bus interface + # with the alternate D-Bus interface name. Add it + # to the class. + attr[attrname] = ( + dbus_interface_annotations(alt_interface) + (copy_function(attribute))) + if deprecate: + # Deprecate all alternate interfaces + iname = "_AlternateDBusNames_interface_annotation{}" + for interface_name in interface_names: + + @dbus_interface_annotations(interface_name) + def func(self): + return {"org.freedesktop.DBus.Deprecated": + "true"} + # Find an unused name + for aname in (iname.format(i) + for i in itertools.count()): + if aname not in attr: + attr[aname] = func + break + if interface_names: + # Replace the class with a new subclass of it with + # methods, signals, etc. as created above. + if sys.version_info.major == 2: + cls = type(b"{}Alternate".format(cls.__name__), + (cls, ), attr) + else: + cls = type("{}Alternate".format(cls.__name__), + (cls, ), attr) + return cls + + return wrapper + + +@alternate_dbus_interfaces({"se.recompile.Mandos": + "se.bsnet.fukt.Mandos"}) +class ClientDBus(Client, DBusObjectWithProperties): + """A Client class using D-Bus + + Attributes: + dbus_object_path: dbus.ObjectPath + bus: dbus.SystemBus() + """ + + runtime_expansions = (Client.runtime_expansions + + ("dbus_object_path", )) + + _interface = "se.recompile.Mandos.Client" + + # dbus.service.Object doesn't use super(), so we can't either. + + def __init__(self, bus=None, *args, **kwargs): + self.bus = bus + Client.__init__(self, *args, **kwargs) + # Only now, when this client is initialized, can it show up on + # the D-Bus + client_object_name = str(self.name).translate( + {ord("."): ord("_"), + ord("-"): ord("_")}) + self.dbus_object_path = dbus.ObjectPath( + "/clients/" + client_object_name) + DBusObjectWithProperties.__init__(self, self.bus, + self.dbus_object_path) + + def notifychangeproperty(transform_func, dbus_name, + type_func=lambda x: x, + variant_level=1, + invalidate_only=False, + _interface=_interface): + """ Modify a variable so that it's a property which announces + its changes to DBus. + + transform_fun: Function that takes a value and a variant_level + and transforms it to a D-Bus type. + dbus_name: D-Bus name of the variable + type_func: Function that transform the value before sending it + to the D-Bus. Default: no transform + variant_level: D-Bus variant level. Default: 1 + """ + attrname = "_{}".format(dbus_name) + + def setter(self, value): + if hasattr(self, "dbus_object_path"): + if (not hasattr(self, attrname) or + type_func(getattr(self, attrname, None)) + != type_func(value)): + if invalidate_only: + self.PropertiesChanged( + _interface, dbus.Dictionary(), + dbus.Array((dbus_name, ))) + else: + dbus_value = transform_func( + type_func(value), + variant_level=variant_level) + self.PropertyChanged(dbus.String(dbus_name), + dbus_value) + self.PropertiesChanged( + _interface, + dbus.Dictionary({dbus.String(dbus_name): + dbus_value}), + dbus.Array()) + setattr(self, attrname, value) + + return property(lambda self: getattr(self, attrname), setter) + + expires = notifychangeproperty(datetime_to_dbus, "Expires") + approvals_pending = notifychangeproperty(dbus.Boolean, + "ApprovalPending", + type_func=bool) + enabled = notifychangeproperty(dbus.Boolean, "Enabled") + last_enabled = notifychangeproperty(datetime_to_dbus, + "LastEnabled") + checker = notifychangeproperty( + dbus.Boolean, "CheckerRunning", + type_func=lambda checker: checker is not None) + last_checked_ok = notifychangeproperty(datetime_to_dbus, + "LastCheckedOK") + last_checker_status = notifychangeproperty(dbus.Int16, + "LastCheckerStatus") + last_approval_request = notifychangeproperty( + datetime_to_dbus, "LastApprovalRequest") + approved_by_default = notifychangeproperty(dbus.Boolean, + "ApprovedByDefault") + approval_delay = notifychangeproperty( + dbus.UInt64, "ApprovalDelay", + type_func=lambda td: td.total_seconds() * 1000) + approval_duration = notifychangeproperty( + dbus.UInt64, "ApprovalDuration", + type_func=lambda td: td.total_seconds() * 1000) + host = notifychangeproperty(dbus.String, "Host") + timeout = notifychangeproperty( + dbus.UInt64, "Timeout", + type_func=lambda td: td.total_seconds() * 1000) + extended_timeout = notifychangeproperty( + dbus.UInt64, "ExtendedTimeout", + type_func=lambda td: td.total_seconds() * 1000) + interval = notifychangeproperty( + dbus.UInt64, "Interval", + type_func=lambda td: td.total_seconds() * 1000) + checker_command = notifychangeproperty(dbus.String, "Checker") + secret = notifychangeproperty(dbus.ByteArray, "Secret", + invalidate_only=True) + + del notifychangeproperty + + def __del__(self, *args, **kwargs): + try: + self.remove_from_connection() + except LookupError: + pass + if hasattr(DBusObjectWithProperties, "__del__"): + DBusObjectWithProperties.__del__(self, *args, **kwargs) + Client.__del__(self, *args, **kwargs) + + def checker_callback(self, source, condition, + connection, command, *args, **kwargs): + ret = Client.checker_callback(self, source, condition, + connection, command, *args, + **kwargs) + exitstatus = self.last_checker_status + if exitstatus >= 0: + # Emit D-Bus signal + self.CheckerCompleted(dbus.Int16(exitstatus), + # This is specific to GNU libC + dbus.Int64(exitstatus << 8), + dbus.String(command)) + else: + # Emit D-Bus signal + self.CheckerCompleted(dbus.Int16(-1), + dbus.Int64( + # This is specific to GNU libC + (exitstatus << 8) + | self.last_checker_signal), + dbus.String(command)) + return ret + + def start_checker(self, *args, **kwargs): + old_checker_pid = getattr(self.checker, "pid", None) + r = Client.start_checker(self, *args, **kwargs) + # Only if new checker process was started + if (self.checker is not None + and old_checker_pid != self.checker.pid): + # Emit D-Bus signal + self.CheckerStarted(self.current_checker_command) + return r + + def _reset_approved(self): + self.approved = None + return False + + def approve(self, value=True): + self.approved = value + GLib.timeout_add(int(self.approval_duration.total_seconds() + * 1000), self._reset_approved) + self.send_changedstate() + + # D-Bus methods, signals & properties + + # Interfaces + + # Signals + + # CheckerCompleted - signal + @dbus.service.signal(_interface, signature="nxs") + def CheckerCompleted(self, exitcode, waitstatus, command): + "D-Bus signal" + pass + + # CheckerStarted - signal + @dbus.service.signal(_interface, signature="s") + def CheckerStarted(self, command): + "D-Bus signal" + pass + + # PropertyChanged - signal + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) + @dbus.service.signal(_interface, signature="sv") + def PropertyChanged(self, property, value): + "D-Bus signal" + pass + + # GotSecret - signal + @dbus.service.signal(_interface) + def GotSecret(self): + """D-Bus signal + Is sent after a successful transfer of secret from the Mandos + server to mandos-client + """ + pass + + # Rejected - signal + @dbus.service.signal(_interface, signature="s") + def Rejected(self, reason): + "D-Bus signal" + pass + + # NeedApproval - signal + @dbus.service.signal(_interface, signature="tb") + def NeedApproval(self, timeout, default): + "D-Bus signal" + return self.need_approval() + + # Methods + + # Approve - method + @dbus.service.method(_interface, in_signature="b") + def Approve(self, value): + self.approve(value) + + # CheckedOK - method + @dbus.service.method(_interface) + def CheckedOK(self): + self.checked_ok() + + # Enable - method + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) + @dbus.service.method(_interface) + def Enable(self): + "D-Bus method" + self.enable() + + # StartChecker - method + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) + @dbus.service.method(_interface) + def StartChecker(self): + "D-Bus method" + self.start_checker() + + # Disable - method + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) + @dbus.service.method(_interface) + def Disable(self): + "D-Bus method" + self.disable() + + # StopChecker - method + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) + @dbus.service.method(_interface) + def StopChecker(self): + self.stop_checker() + + # Properties + + # ApprovalPending - property + @dbus_service_property(_interface, signature="b", access="read") + def ApprovalPending_dbus_property(self): + return dbus.Boolean(bool(self.approvals_pending)) + + # ApprovedByDefault - property + @dbus_service_property(_interface, + signature="b", + access="readwrite") + def ApprovedByDefault_dbus_property(self, value=None): + if value is None: # get + return dbus.Boolean(self.approved_by_default) + self.approved_by_default = bool(value) + + # ApprovalDelay - property + @dbus_service_property(_interface, + signature="t", + access="readwrite") + def ApprovalDelay_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.approval_delay.total_seconds() + * 1000) + self.approval_delay = datetime.timedelta(0, 0, 0, value) + + # ApprovalDuration - property + @dbus_service_property(_interface, + signature="t", + access="readwrite") + def ApprovalDuration_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.approval_duration.total_seconds() + * 1000) + self.approval_duration = datetime.timedelta(0, 0, 0, value) + + # Name - property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"}) + @dbus_service_property(_interface, signature="s", access="read") + def Name_dbus_property(self): + return dbus.String(self.name) + + # KeyID - property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"}) + @dbus_service_property(_interface, signature="s", access="read") + def KeyID_dbus_property(self): + return dbus.String(self.key_id) + + # Fingerprint - property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"}) + @dbus_service_property(_interface, signature="s", access="read") + def Fingerprint_dbus_property(self): + return dbus.String(self.fingerprint) + + # Host - property + @dbus_service_property(_interface, + signature="s", + access="readwrite") + def Host_dbus_property(self, value=None): + if value is None: # get + return dbus.String(self.host) + self.host = str(value) + + # Created - property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"}) + @dbus_service_property(_interface, signature="s", access="read") + def Created_dbus_property(self): + return datetime_to_dbus(self.created) + + # LastEnabled - property + @dbus_service_property(_interface, signature="s", access="read") + def LastEnabled_dbus_property(self): + return datetime_to_dbus(self.last_enabled) + + # Enabled - property + @dbus_service_property(_interface, + signature="b", + access="readwrite") + def Enabled_dbus_property(self, value=None): + if value is None: # get + return dbus.Boolean(self.enabled) + if value: + self.enable() + else: + self.disable() + + # LastCheckedOK - property + @dbus_service_property(_interface, + signature="s", + access="readwrite") + def LastCheckedOK_dbus_property(self, value=None): + if value is not None: + self.checked_ok() + return + return datetime_to_dbus(self.last_checked_ok) + + # LastCheckerStatus - property + @dbus_service_property(_interface, signature="n", access="read") + def LastCheckerStatus_dbus_property(self): + return dbus.Int16(self.last_checker_status) + + # Expires - property + @dbus_service_property(_interface, signature="s", access="read") + def Expires_dbus_property(self): + return datetime_to_dbus(self.expires) + + # LastApprovalRequest - property + @dbus_service_property(_interface, signature="s", access="read") + def LastApprovalRequest_dbus_property(self): + return datetime_to_dbus(self.last_approval_request) + + # Timeout - property + @dbus_service_property(_interface, + signature="t", + access="readwrite") + def Timeout_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.timeout.total_seconds() * 1000) + old_timeout = self.timeout + self.timeout = datetime.timedelta(0, 0, 0, value) + # Reschedule disabling + if self.enabled: + now = datetime.datetime.utcnow() + self.expires += self.timeout - old_timeout + if self.expires <= now: + # The timeout has passed + self.disable() + else: + if (getattr(self, "disable_initiator_tag", None) + is None): + return + GLib.source_remove(self.disable_initiator_tag) + self.disable_initiator_tag = GLib.timeout_add( + int((self.expires - now).total_seconds() * 1000), + self.disable) + + # ExtendedTimeout - property + @dbus_service_property(_interface, + signature="t", + access="readwrite") + def ExtendedTimeout_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.extended_timeout.total_seconds() + * 1000) + self.extended_timeout = datetime.timedelta(0, 0, 0, value) + + # Interval - property + @dbus_service_property(_interface, + signature="t", + access="readwrite") + def Interval_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.interval.total_seconds() * 1000) + self.interval = datetime.timedelta(0, 0, 0, value) + if getattr(self, "checker_initiator_tag", None) is None: + return + if self.enabled: + # Reschedule checker run + GLib.source_remove(self.checker_initiator_tag) + self.checker_initiator_tag = GLib.timeout_add( + value, self.start_checker) + self.start_checker() # Start one now, too + + # Checker - property + @dbus_service_property(_interface, + signature="s", + access="readwrite") + def Checker_dbus_property(self, value=None): + if value is None: # get + return dbus.String(self.checker_command) + self.checker_command = str(value) + + # CheckerRunning - property + @dbus_service_property(_interface, + signature="b", + access="readwrite") + def CheckerRunning_dbus_property(self, value=None): + if value is None: # get + return dbus.Boolean(self.checker is not None) + if value: + self.start_checker() + else: + self.stop_checker() + + # ObjectPath - property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const", + "org.freedesktop.DBus.Deprecated": "true"}) + @dbus_service_property(_interface, signature="o", access="read") + def ObjectPath_dbus_property(self): + return self.dbus_object_path # is already a dbus.ObjectPath + + # Secret = property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": + "invalidates"}) + @dbus_service_property(_interface, + signature="ay", + access="write", + byte_arrays=True) + def Secret_dbus_property(self, value): + self.secret = bytes(value) + + del _interface + + +class ProxyClient: + def __init__(self, child_pipe, key_id, fpr, address): + self._pipe = child_pipe + self._pipe.send(('init', key_id, fpr, address)) + if not self._pipe.recv(): + raise KeyError(key_id or fpr) + + def __getattribute__(self, name): + if name == '_pipe': + return super(ProxyClient, self).__getattribute__(name) + self._pipe.send(('getattr', name)) + data = self._pipe.recv() + if data[0] == 'data': + return data[1] + if data[0] == 'function': + + def func(*args, **kwargs): + self._pipe.send(('funcall', name, args, kwargs)) + return self._pipe.recv()[1] + + return func + + def __setattr__(self, name, value): + if name == '_pipe': + return super(ProxyClient, self).__setattr__(name, value) + self._pipe.send(('setattr', name, value)) + + +class ClientHandler(socketserver.BaseRequestHandler, object): + """A class to handle client connections. + + Instantiated once for each connection to handle it. Note: This will run in its own forked process.""" - + def handle(self): - logger.debug(u"TCP connection from: %s", - unicode(self.client_address)) - session = gnutls.connection.ClientSession(self.request, - gnutls.connection.\ - X509Credentials()) - - #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC", - # "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP", - # "+DHE-DSS")) - priority = "NORMAL" - if self.server.options.priority: - priority = self.server.options.priority - gnutls.library.functions.gnutls_priority_set_direct\ - (session._c_object, priority, None); - - try: - session.handshake() - except gnutls.errors.GNUTLSError, error: - logger.debug(u"Handshake failed: %s", error) - # Do not run session.bye() here: the session is not - # established. Just abandon the request. - return - try: - fpr = fingerprint(peer_certificate(session)) - except (TypeError, gnutls.errors.GNUTLSError), error: - logger.debug(u"Bad certificate: %s", error) - session.bye() - return - logger.debug(u"Fingerprint: %s", fpr) - client = None - for c in self.server.clients: - if c.fingerprint == fpr: - client = c - break - # Have to check if client.still_valid(), since it is possible - # that the client timed out while establishing the GnuTLS - # session. - if (not client) or (not client.still_valid()): - if client: - logger.debug(u"Client %(name)s is invalid", - vars(client)) - else: - logger.debug(u"Client not found for fingerprint: %s", - fpr) - session.bye() - return - sent_size = 0 - while sent_size < len(client.secret): - sent = session.send(client.secret[sent_size:]) - logger.debug(u"Sent: %d, remaining: %d", - sent, len(client.secret) - - (sent_size + sent)) - sent_size += sent - session.bye() - - -class IPv6_TCPServer(SocketServer.ForkingTCPServer, object): - """IPv6 TCP server. Accepts 'None' as address and/or port. + with contextlib.closing(self.server.child_pipe) as child_pipe: + logger.info("TCP connection from: %s", + str(self.client_address)) + logger.debug("Pipe FD: %d", + self.server.child_pipe.fileno()) + + session = gnutls.ClientSession(self.request) + + # priority = ':'.join(("NONE", "+VERS-TLS1.1", + # "+AES-256-CBC", "+SHA1", + # "+COMP-NULL", "+CTYPE-OPENPGP", + # "+DHE-DSS")) + # Use a fallback default, since this MUST be set. + priority = self.server.gnutls_priority + if priority is None: + priority = "NORMAL" + gnutls.priority_set_direct(session._c_object, + priority.encode("utf-8"), + None) + + # Start communication using the Mandos protocol + # Get protocol number + line = self.request.makefile().readline() + logger.debug("Protocol version: %r", line) + try: + if int(line.strip().split()[0]) > 1: + raise RuntimeError(line) + except (ValueError, IndexError, RuntimeError) as error: + logger.error("Unknown protocol version: %s", error) + return + + # Start GnuTLS connection + try: + session.handshake() + except gnutls.Error as error: + logger.warning("Handshake failed: %s", error) + # Do not run session.bye() here: the session is not + # established. Just abandon the request. + return + logger.debug("Handshake succeeded") + + approval_required = False + try: + if gnutls.has_rawpk: + fpr = b"" + try: + key_id = self.key_id( + self.peer_certificate(session)) + except (TypeError, gnutls.Error) as error: + logger.warning("Bad certificate: %s", error) + return + logger.debug("Key ID: %s", key_id) + + else: + key_id = b"" + try: + fpr = self.fingerprint( + self.peer_certificate(session)) + except (TypeError, gnutls.Error) as error: + logger.warning("Bad certificate: %s", error) + return + logger.debug("Fingerprint: %s", fpr) + + try: + client = ProxyClient(child_pipe, key_id, fpr, + self.client_address) + except KeyError: + return + + if client.approval_delay: + delay = client.approval_delay + client.approvals_pending += 1 + approval_required = True + + while True: + if not client.enabled: + logger.info("Client %s is disabled", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Disabled") + return + + if client.approved or not client.approval_delay: + # We are approved or approval is disabled + break + elif client.approved is None: + logger.info("Client %s needs approval", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.NeedApproval( + client.approval_delay.total_seconds() + * 1000, client.approved_by_default) + else: + logger.warning("Client %s was not approved", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Denied") + return + + # wait until timeout or approved + time = datetime.datetime.now() + client.changedstate.acquire() + client.changedstate.wait(delay.total_seconds()) + client.changedstate.release() + time2 = datetime.datetime.now() + if (time2 - time) >= delay: + if not client.approved_by_default: + logger.warning("Client %s timed out while" + " waiting for approval", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Approval timed out") + return + else: + break + else: + delay -= time2 - time + + try: + session.send(client.secret) + except gnutls.Error as error: + logger.warning("gnutls send failed", + exc_info=error) + return + + logger.info("Sending secret to %s", client.name) + # bump the timeout using extended_timeout + client.bump_timeout(client.extended_timeout) + if self.server.use_dbus: + # Emit D-Bus signal + client.GotSecret() + + finally: + if approval_required: + client.approvals_pending -= 1 + try: + session.bye() + except gnutls.Error as error: + logger.warning("GnuTLS bye failed", + exc_info=error) + + @staticmethod + def peer_certificate(session): + "Return the peer's certificate as a bytestring" + try: + cert_type = gnutls.certificate_type_get2(session._c_object, + gnutls.CTYPE_PEERS) + except AttributeError: + cert_type = gnutls.certificate_type_get(session._c_object) + if gnutls.has_rawpk: + valid_cert_types = frozenset((gnutls.CRT_RAWPK,)) + else: + valid_cert_types = frozenset((gnutls.CRT_OPENPGP,)) + # If not a valid certificate type... + if cert_type not in valid_cert_types: + logger.info("Cert type %r not in %r", cert_type, + valid_cert_types) + # ...return invalid data + return b"" + list_size = ctypes.c_uint(1) + cert_list = (gnutls.certificate_get_peers + (session._c_object, ctypes.byref(list_size))) + if not bool(cert_list) and list_size.value != 0: + raise gnutls.Error("error getting peer certificate") + if list_size.value == 0: + return None + cert = cert_list[0] + return ctypes.string_at(cert.data, cert.size) + + @staticmethod + def key_id(certificate): + "Convert a certificate bytestring to a hexdigit key ID" + # New GnuTLS "datum" with the public key + datum = gnutls.datum_t( + ctypes.cast(ctypes.c_char_p(certificate), + ctypes.POINTER(ctypes.c_ubyte)), + ctypes.c_uint(len(certificate))) + # XXX all these need to be created in the gnutls "module" + # New empty GnuTLS certificate + pubkey = gnutls.pubkey_t() + gnutls.pubkey_init(ctypes.byref(pubkey)) + # Import the raw public key into the certificate + gnutls.pubkey_import(pubkey, + ctypes.byref(datum), + gnutls.X509_FMT_DER) + # New buffer for the key ID + buf = ctypes.create_string_buffer(32) + buf_len = ctypes.c_size_t(len(buf)) + # Get the key ID from the raw public key into the buffer + gnutls.pubkey_get_key_id(pubkey, + gnutls.KEYID_USE_SHA256, + ctypes.cast(ctypes.byref(buf), + ctypes.POINTER(ctypes.c_ubyte)), + ctypes.byref(buf_len)) + # Deinit the certificate + gnutls.pubkey_deinit(pubkey) + + # Convert the buffer to a Python bytestring + key_id = ctypes.string_at(buf, buf_len.value) + # Convert the bytestring to hexadecimal notation + hex_key_id = binascii.hexlify(key_id).upper() + return hex_key_id + + @staticmethod + def fingerprint(openpgp): + "Convert an OpenPGP bytestring to a hexdigit fingerprint" + # New GnuTLS "datum" with the OpenPGP public key + datum = gnutls.datum_t( + ctypes.cast(ctypes.c_char_p(openpgp), + ctypes.POINTER(ctypes.c_ubyte)), + ctypes.c_uint(len(openpgp))) + # New empty GnuTLS certificate + crt = gnutls.openpgp_crt_t() + gnutls.openpgp_crt_init(ctypes.byref(crt)) + # Import the OpenPGP public key into the certificate + gnutls.openpgp_crt_import(crt, ctypes.byref(datum), + gnutls.OPENPGP_FMT_RAW) + # Verify the self signature in the key + crtverify = ctypes.c_uint() + gnutls.openpgp_crt_verify_self(crt, 0, + ctypes.byref(crtverify)) + if crtverify.value != 0: + gnutls.openpgp_crt_deinit(crt) + raise gnutls.CertificateSecurityError(code + =crtverify.value) + # New buffer for the fingerprint + buf = ctypes.create_string_buffer(20) + buf_len = ctypes.c_size_t() + # Get the fingerprint from the certificate into the buffer + gnutls.openpgp_crt_get_fingerprint(crt, ctypes.byref(buf), + ctypes.byref(buf_len)) + # Deinit the certificate + gnutls.openpgp_crt_deinit(crt) + # Convert the buffer to a Python bytestring + fpr = ctypes.string_at(buf, buf_len.value) + # Convert the bytestring to hexadecimal notation + hex_fpr = binascii.hexlify(fpr).upper() + return hex_fpr + + +class MultiprocessingMixIn: + """Like socketserver.ThreadingMixIn, but with multiprocessing""" + + def sub_process_main(self, request, address): + try: + self.finish_request(request, address) + except Exception: + self.handle_error(request, address) + self.close_request(request) + + def process_request(self, request, address): + """Start a new process to process the request.""" + proc = multiprocessing.Process(target=self.sub_process_main, + args=(request, address)) + proc.start() + return proc + + +class MultiprocessingMixInWithPipe(MultiprocessingMixIn): + """ adds a pipe to the MixIn """ + + def process_request(self, request, client_address): + """Overrides and wraps the original process_request(). + + This function creates a new pipe in self.pipe + """ + parent_pipe, self.child_pipe = multiprocessing.Pipe() + + proc = MultiprocessingMixIn.process_request(self, request, + client_address) + self.child_pipe.close() + self.add_pipe(parent_pipe, proc) + + def add_pipe(self, parent_pipe, proc): + """Dummy function; override as necessary""" + raise NotImplementedError() + + +class IPv6_TCPServer(MultiprocessingMixInWithPipe, + socketserver.TCPServer): + """IPv6-capable TCP server. Accepts 'None' as address and/or port + Attributes: - options: Command line options - clients: Set() of Client objects + enabled: Boolean; whether this server is activated yet + interface: None or a network interface name (string) + use_ipv6: Boolean; to use IPv6 or not """ - address_family = socket.AF_INET6 - def __init__(self, *args, **kwargs): - if "options" in kwargs: - self.options = kwargs["options"] - del kwargs["options"] - if "clients" in kwargs: - self.clients = kwargs["clients"] - del kwargs["clients"] - return super(type(self), self).__init__(*args, **kwargs) + + def __init__(self, server_address, RequestHandlerClass, + interface=None, + use_ipv6=True, + socketfd=None): + """If socketfd is set, use that file descriptor instead of + creating a new one with socket.socket(). + """ + self.interface = interface + if use_ipv6: + self.address_family = socket.AF_INET6 + if socketfd is not None: + # Save the file descriptor + self.socketfd = socketfd + # Save the original socket.socket() function + self.socket_socket = socket.socket + + # To implement --socket, we monkey patch socket.socket. + # + # (When socketserver.TCPServer is a new-style class, we + # could make self.socket into a property instead of monkey + # patching socket.socket.) + # + # Create a one-time-only replacement for socket.socket() + @functools.wraps(socket.socket) + def socket_wrapper(*args, **kwargs): + # Restore original function so subsequent calls are + # not affected. + socket.socket = self.socket_socket + del self.socket_socket + # This time only, return a new socket object from the + # saved file descriptor. + return socket.fromfd(self.socketfd, *args, **kwargs) + # Replace socket.socket() function with wrapper + socket.socket = socket_wrapper + # The socketserver.TCPServer.__init__ will call + # socket.socket(), which might be our replacement, + # socket_wrapper(), if socketfd was set. + socketserver.TCPServer.__init__(self, server_address, + RequestHandlerClass) + def server_bind(self): """This overrides the normal server_bind() function to bind to an interface if one was specified, and also NOT to bind to an address or port if they were not specified.""" - if self.options.interface: - if not hasattr(socket, "SO_BINDTODEVICE"): - # From /usr/include/asm-i486/socket.h - socket.SO_BINDTODEVICE = 25 + global SO_BINDTODEVICE + if self.interface is not None: + if SO_BINDTODEVICE is None: + # Fall back to a hard-coded value which seems to be + # common enough. + logger.warning("SO_BINDTODEVICE not found, trying 25") + SO_BINDTODEVICE = 25 try: - self.socket.setsockopt(socket.SOL_SOCKET, - socket.SO_BINDTODEVICE, - self.options.interface) - except socket.error, error: - if error[0] == errno.EPERM: - logger.warning(u"No permission to" - u" bind to interface %s", - self.options.interface) + self.socket.setsockopt( + socket.SOL_SOCKET, SO_BINDTODEVICE, + (self.interface + "\0").encode("utf-8")) + except socket.error as error: + if error.errno == errno.EPERM: + logger.error("No permission to bind to" + " interface %s", self.interface) + elif error.errno == errno.ENOPROTOOPT: + logger.error("SO_BINDTODEVICE not available;" + " cannot bind to interface %s", + self.interface) + elif error.errno == errno.ENODEV: + logger.error("Interface %s does not exist," + " cannot bind", self.interface) else: - raise error + raise # Only bind(2) the socket if we really need to. if self.server_address[0] or self.server_address[1]: + if self.server_address[1]: + self.allow_reuse_address = True if not self.server_address[0]: - in6addr_any = "::" - self.server_address = (in6addr_any, + if self.address_family == socket.AF_INET6: + any_address = "::" # in6addr_any + else: + any_address = "0.0.0.0" # INADDR_ANY + self.server_address = (any_address, self.server_address[1]) - elif self.server_address[1] is None: - self.server_address = (self.server_address[0], - 0) - return super(type(self), self).server_bind() + elif not self.server_address[1]: + self.server_address = (self.server_address[0], 0) +# if self.interface: +# self.server_address = (self.server_address[0], +# 0, # port +# 0, # flowinfo +# if_nametoindex +# (self.interface)) + return socketserver.TCPServer.server_bind(self) + + +class MandosServer(IPv6_TCPServer): + """Mandos server. + + Attributes: + clients: set of Client objects + gnutls_priority GnuTLS priority string + use_dbus: Boolean; to emit D-Bus signals or not + + Assumes a GLib.MainLoop event loop. + """ + + def __init__(self, server_address, RequestHandlerClass, + interface=None, + use_ipv6=True, + clients=None, + gnutls_priority=None, + use_dbus=True, + socketfd=None): + self.enabled = False + self.clients = clients + if self.clients is None: + self.clients = {} + self.use_dbus = use_dbus + self.gnutls_priority = gnutls_priority + IPv6_TCPServer.__init__(self, server_address, + RequestHandlerClass, + interface=interface, + use_ipv6=use_ipv6, + socketfd=socketfd) + + def server_activate(self): + if self.enabled: + return socketserver.TCPServer.server_activate(self) + + def enable(self): + self.enabled = True + + def add_pipe(self, parent_pipe, proc): + # Call "handle_ipc" for both data and EOF events + GLib.io_add_watch( + GLib.IOChannel.unix_new(parent_pipe.fileno()), + GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP, + functools.partial(self.handle_ipc, + parent_pipe=parent_pipe, + proc=proc)) + + def handle_ipc(self, source, condition, + parent_pipe=None, + proc=None, + client_object=None): + # error, or the other end of multiprocessing.Pipe has closed + if condition & (GLib.IO_ERR | GLib.IO_HUP): + # Wait for other process to exit + proc.join() + return False + + # Read a request from the child + request = parent_pipe.recv() + command = request[0] + + if command == 'init': + key_id = request[1].decode("ascii") + fpr = request[2].decode("ascii") + address = request[3] + + for c in self.clients.values(): + if key_id == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855": + continue + if key_id and c.key_id == key_id: + client = c + break + if fpr and c.fingerprint == fpr: + client = c + break + else: + logger.info("Client not found for key ID: %s, address" + ": %s", key_id or fpr, address) + if self.use_dbus: + # Emit D-Bus signal + mandos_dbus_service.ClientNotFound(key_id or fpr, + address[0]) + parent_pipe.send(False) + return False + + GLib.io_add_watch( + GLib.IOChannel.unix_new(parent_pipe.fileno()), + GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP, + functools.partial(self.handle_ipc, + parent_pipe=parent_pipe, + proc=proc, + client_object=client)) + parent_pipe.send(True) + # remove the old hook in favor of the new above hook on + # same fileno + return False + if command == 'funcall': + funcname = request[1] + args = request[2] + kwargs = request[3] + + parent_pipe.send(('data', getattr(client_object, + funcname)(*args, + **kwargs))) + + if command == 'getattr': + attrname = request[1] + if isinstance(client_object.__getattribute__(attrname), + collections.Callable): + parent_pipe.send(('function', )) + else: + parent_pipe.send(( + 'data', client_object.__getattribute__(attrname))) + + if command == 'setattr': + attrname = request[1] + value = request[2] + setattr(client_object, attrname, value) + + return True + + +def rfc3339_duration_to_delta(duration): + """Parse an RFC 3339 "duration" and return a datetime.timedelta + + >>> rfc3339_duration_to_delta("P7D") == datetime.timedelta(7) + True + >>> rfc3339_duration_to_delta("PT60S") == datetime.timedelta(0, 60) + True + >>> rfc3339_duration_to_delta("PT60M") == datetime.timedelta(0, 3600) + True + >>> rfc3339_duration_to_delta("PT24H") == datetime.timedelta(1) + True + >>> rfc3339_duration_to_delta("P1W") == datetime.timedelta(7) + True + >>> rfc3339_duration_to_delta("PT5M30S") == datetime.timedelta(0, 330) + True + >>> rfc3339_duration_to_delta("P1DT3M20S") == datetime.timedelta(1, 200) + True + """ + + # Parsing an RFC 3339 duration with regular expressions is not + # possible - there would have to be multiple places for the same + # values, like seconds. The current code, while more esoteric, is + # cleaner without depending on a parsing library. If Python had a + # built-in library for parsing we would use it, but we'd like to + # avoid excessive use of external libraries. + + # New type for defining tokens, syntax, and semantics all-in-one + Token = collections.namedtuple("Token", ( + "regexp", # To match token; if "value" is not None, must have + # a "group" containing digits + "value", # datetime.timedelta or None + "followers")) # Tokens valid after this token + # RFC 3339 "duration" tokens, syntax, and semantics; taken from + # the "duration" ABNF definition in RFC 3339, Appendix A. + token_end = Token(re.compile(r"$"), None, frozenset()) + token_second = Token(re.compile(r"(\d+)S"), + datetime.timedelta(seconds=1), + frozenset((token_end, ))) + token_minute = Token(re.compile(r"(\d+)M"), + datetime.timedelta(minutes=1), + frozenset((token_second, token_end))) + token_hour = Token(re.compile(r"(\d+)H"), + datetime.timedelta(hours=1), + frozenset((token_minute, token_end))) + token_time = Token(re.compile(r"T"), + None, + frozenset((token_hour, token_minute, + token_second))) + token_day = Token(re.compile(r"(\d+)D"), + datetime.timedelta(days=1), + frozenset((token_time, token_end))) + token_month = Token(re.compile(r"(\d+)M"), + datetime.timedelta(weeks=4), + frozenset((token_day, token_end))) + token_year = Token(re.compile(r"(\d+)Y"), + datetime.timedelta(weeks=52), + frozenset((token_month, token_end))) + token_week = Token(re.compile(r"(\d+)W"), + datetime.timedelta(weeks=1), + frozenset((token_end, ))) + token_duration = Token(re.compile(r"P"), None, + frozenset((token_year, token_month, + token_day, token_time, + token_week))) + # Define starting values: + # Value so far + value = datetime.timedelta() + found_token = None + # Following valid tokens + followers = frozenset((token_duration, )) + # String left to parse + s = duration + # Loop until end token is found + while found_token is not token_end: + # Search for any currently valid tokens + for token in followers: + match = token.regexp.match(s) + if match is not None: + # Token found + if token.value is not None: + # Value found, parse digits + factor = int(match.group(1), 10) + # Add to value so far + value += factor * token.value + # Strip token from string + s = token.regexp.sub("", s, 1) + # Go to found token + found_token = token + # Set valid next tokens + followers = found_token.followers + break + else: + # No currently valid tokens were found + raise ValueError("Invalid RFC 3339 duration: {!r}" + .format(duration)) + # End token found + return value def string_to_delta(interval): """Parse a string and return a datetime.timedelta - >>> string_to_delta('7d') - datetime.timedelta(7) - >>> string_to_delta('60s') - datetime.timedelta(0, 60) - >>> string_to_delta('60m') - datetime.timedelta(0, 3600) - >>> string_to_delta('24h') - datetime.timedelta(1) - >>> string_to_delta(u'1w') - datetime.timedelta(7) + >>> string_to_delta('7d') == datetime.timedelta(7) + True + >>> string_to_delta('60s') == datetime.timedelta(0, 60) + True + >>> string_to_delta('60m') == datetime.timedelta(0, 3600) + True + >>> string_to_delta('24h') == datetime.timedelta(1) + True + >>> string_to_delta('1w') == datetime.timedelta(7) + True + >>> string_to_delta('5m 30s') == datetime.timedelta(0, 330) + True """ - try: - suffix=unicode(interval[-1]) - value=int(interval[:-1]) - if suffix == u"d": - delta = datetime.timedelta(value) - elif suffix == u"s": - delta = datetime.timedelta(0, value) - elif suffix == u"m": - delta = datetime.timedelta(0, 0, 0, 0, value) - elif suffix == u"h": - delta = datetime.timedelta(0, 0, 0, 0, 0, value) - elif suffix == u"w": - delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) - else: - raise ValueError - except (ValueError, IndexError): - raise ValueError - return delta - - -def add_service(): - """Derived from the Avahi example code""" - global group, serviceName, serviceType, servicePort, serviceTXT, \ - domain, host - if group is None: - group = dbus.Interface( - bus.get_object( avahi.DBUS_NAME, - server.EntryGroupNew()), - avahi.DBUS_INTERFACE_ENTRY_GROUP) - group.connect_to_signal('StateChanged', - entry_group_state_changed) - logger.debug(u"Adding service '%s' of type '%s' ...", - serviceName, serviceType) - - group.AddService( - serviceInterface, # interface - avahi.PROTO_INET6, # protocol - dbus.UInt32(0), # flags - serviceName, serviceType, - domain, host, - dbus.UInt16(servicePort), - avahi.string_array_to_txt_array(serviceTXT)) - group.Commit() - - -def remove_service(): - """From the Avahi example code""" - global group - - if not group is None: - group.Reset() - - -def server_state_changed(state): - """Derived from the Avahi example code""" - if state == avahi.SERVER_COLLISION: - logger.warning(u"Server name collision") - remove_service() - elif state == avahi.SERVER_RUNNING: - add_service() - - -def entry_group_state_changed(state, error): - """Derived from the Avahi example code""" - global serviceName, server, rename_count - - logger.debug(u"state change: %i", state) - - if state == avahi.ENTRY_GROUP_ESTABLISHED: - logger.debug(u"Service established.") - elif state == avahi.ENTRY_GROUP_COLLISION: - - rename_count = rename_count - 1 - if rename_count > 0: - name = server.GetAlternativeServiceName(name) - logger.warning(u"Service name collision, " - u"changing name to '%s' ...", name) - remove_service() - add_service() - - else: - logger.error(u"No suitable service name found after %i" - u" retries, exiting.", n_rename) - killme(1) - elif state == avahi.ENTRY_GROUP_FAILURE: - logger.error(u"Error in group state changed %s", - unicode(error)) - killme(1) - - -def if_nametoindex(interface): - """Call the C function if_nametoindex()""" - try: - libc = ctypes.cdll.LoadLibrary("libc.so.6") - return libc.if_nametoindex(interface) - except (OSError, AttributeError): - if "struct" not in sys.modules: - import struct - if "fcntl" not in sys.modules: - import fcntl - SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h - s = socket.socket() - ifreq = fcntl.ioctl(s, SIOCGIFINDEX, - struct.pack("16s16x", interface)) - s.close() - interface_index = struct.unpack("I", ifreq[16:20])[0] - return interface_index - - -def daemon(nochdir, noclose): + + try: + return rfc3339_duration_to_delta(interval) + except ValueError: + pass + + timevalue = datetime.timedelta(0) + for s in interval.split(): + try: + suffix = s[-1] + value = int(s[:-1]) + if suffix == "d": + delta = datetime.timedelta(value) + elif suffix == "s": + delta = datetime.timedelta(0, value) + elif suffix == "m": + delta = datetime.timedelta(0, 0, 0, 0, value) + elif suffix == "h": + delta = datetime.timedelta(0, 0, 0, 0, 0, value) + elif suffix == "w": + delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) + else: + raise ValueError("Unknown suffix {!r}".format(suffix)) + except IndexError as e: + raise ValueError(*(e.args)) + timevalue += delta + return timevalue + + +def daemon(nochdir=False, noclose=False): """See daemon(3). Standard BSD Unix function. + This should really exist as os.daemon, but it doesn't (yet).""" if os.fork(): sys.exit() os.setsid() if not nochdir: os.chdir("/") + if os.fork(): + sys.exit() if not noclose: # Close all standard open file descriptors - null = os.open("/dev/null", os.O_NOCTTY | os.O_RDWR) + null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR) if not stat.S_ISCHR(os.fstat(null).st_mode): raise OSError(errno.ENODEV, - "/dev/null not a character device") + "{} not a character device" + .format(os.devnull)) os.dup2(null, sys.stdin.fileno()) os.dup2(null, sys.stdout.fileno()) os.dup2(null, sys.stderr.fileno()) @@ -610,155 +2932,722 @@ os.close(null) -def killme(status = 0): - logger.debug("Stopping server with exit status %d", status) - exitstatus = status - if main_loop_started: - main_loop.quit() - else: - sys.exit(status) - - def main(): - global exitstatus - exitstatus = 0 - global main_loop_started - main_loop_started = False - - parser = OptionParser() - parser.add_option("-i", "--interface", type="string", - default=None, metavar="IF", - help="Bind to interface IF") - parser.add_option("-a", "--address", type="string", default=None, - help="Address to listen for requests on") - parser.add_option("-p", "--port", type="int", default=None, - help="Port number to receive requests on") - parser.add_option("--check", action="store_true", default=False, - help="Run self-test") - parser.add_option("--debug", action="store_true", default=False, - help="Debug mode") - parser.add_option("--priority", type="string", - default="SECURE256", - help="GnuTLS priority string" - " (see GnuTLS documentation)") - parser.add_option("--servicename", type="string", - default="Mandos", help="Zeroconf service name") - (options, args) = parser.parse_args() - - if options.check: - import doctest - doctest.testmod() - sys.exit() - - # Parse config file - defaults = { "timeout": "1h", - "interval": "5m", - "checker": "fping -q -- %%(fqdn)s", - } - client_config = ConfigParser.SafeConfigParser(defaults) - #client_config.readfp(open("global.conf"), "global.conf") - client_config.read("mandos-clients.conf") - - global serviceName - serviceName = options.servicename; - + + ################################################################## + # Parsing of options, both command line and config file + + parser = argparse.ArgumentParser() + parser.add_argument("-v", "--version", action="version", + version="%(prog)s {}".format(version), + help="show version number and exit") + parser.add_argument("-i", "--interface", metavar="IF", + help="Bind to interface IF") + parser.add_argument("-a", "--address", + help="Address to listen for requests on") + parser.add_argument("-p", "--port", type=int, + help="Port number to receive requests on") + parser.add_argument("--check", action="store_true", + help="Run self-test") + parser.add_argument("--debug", action="store_true", + help="Debug mode; run in foreground and log" + " to terminal", default=None) + parser.add_argument("--debuglevel", metavar="LEVEL", + help="Debug level for stdout output") + parser.add_argument("--priority", help="GnuTLS" + " priority string (see GnuTLS documentation)") + parser.add_argument("--servicename", + metavar="NAME", help="Zeroconf service name") + parser.add_argument("--configdir", + default="/etc/mandos", metavar="DIR", + help="Directory to search for configuration" + " files") + parser.add_argument("--no-dbus", action="store_false", + dest="use_dbus", help="Do not provide D-Bus" + " system bus interface", default=None) + parser.add_argument("--no-ipv6", action="store_false", + dest="use_ipv6", help="Do not use IPv6", + default=None) + parser.add_argument("--no-restore", action="store_false", + dest="restore", help="Do not restore stored" + " state", default=None) + parser.add_argument("--socket", type=int, + help="Specify a file descriptor to a network" + " socket to use instead of creating one") + parser.add_argument("--statedir", metavar="DIR", + help="Directory to save/restore state in") + parser.add_argument("--foreground", action="store_true", + help="Run in foreground", default=None) + parser.add_argument("--no-zeroconf", action="store_false", + dest="zeroconf", help="Do not use Zeroconf", + default=None) + + options = parser.parse_args() + + # Default values for config file for server-global settings + if gnutls.has_rawpk: + priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA" + ":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA") + else: + priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA" + ":+SIGN-DSA-SHA256") + server_defaults = {"interface": "", + "address": "", + "port": "", + "debug": "False", + "priority": priority, + "servicename": "Mandos", + "use_dbus": "True", + "use_ipv6": "True", + "debuglevel": "", + "restore": "True", + "socket": "", + "statedir": "/var/lib/mandos", + "foreground": "False", + "zeroconf": "True", + } + del priority + + # Parse config file for server-global settings + server_config = configparser.ConfigParser(server_defaults) + del server_defaults + server_config.read(os.path.join(options.configdir, "mandos.conf")) + # Convert the ConfigParser object to a dict + server_settings = server_config.defaults() + # Use the appropriate methods on the non-string config options + for option in ("debug", "use_dbus", "use_ipv6", "restore", + "foreground", "zeroconf"): + server_settings[option] = server_config.getboolean("DEFAULT", + option) + if server_settings["port"]: + server_settings["port"] = server_config.getint("DEFAULT", + "port") + if server_settings["socket"]: + server_settings["socket"] = server_config.getint("DEFAULT", + "socket") + # Later, stdin will, and stdout and stderr might, be dup'ed + # over with an opened os.devnull. But we don't want this to + # happen with a supplied network socket. + if 0 <= server_settings["socket"] <= 2: + server_settings["socket"] = os.dup(server_settings + ["socket"]) + del server_config + + # Override the settings from the config file with command line + # options, if set. + for option in ("interface", "address", "port", "debug", + "priority", "servicename", "configdir", "use_dbus", + "use_ipv6", "debuglevel", "restore", "statedir", + "socket", "foreground", "zeroconf"): + value = getattr(options, option) + if value is not None: + server_settings[option] = value + del options + # Force all strings to be unicode + for option in server_settings.keys(): + if isinstance(server_settings[option], bytes): + server_settings[option] = (server_settings[option] + .decode("utf-8")) + # Force all boolean options to be boolean + for option in ("debug", "use_dbus", "use_ipv6", "restore", + "foreground", "zeroconf"): + server_settings[option] = bool(server_settings[option]) + # Debug implies foreground + if server_settings["debug"]: + server_settings["foreground"] = True + # Now we have our good server settings in "server_settings" + + ################################################################## + + if (not server_settings["zeroconf"] + and not (server_settings["port"] + or server_settings["socket"] != "")): + parser.error("Needs port or socket to work without Zeroconf") + + # For convenience + debug = server_settings["debug"] + debuglevel = server_settings["debuglevel"] + use_dbus = server_settings["use_dbus"] + use_ipv6 = server_settings["use_ipv6"] + stored_state_path = os.path.join(server_settings["statedir"], + stored_state_file) + foreground = server_settings["foreground"] + zeroconf = server_settings["zeroconf"] + + if debug: + initlogger(debug, logging.DEBUG) + else: + if not debuglevel: + initlogger(debug) + else: + level = getattr(logging, debuglevel.upper()) + initlogger(debug, level) + + if server_settings["servicename"] != "Mandos": + syslogger.setFormatter( + logging.Formatter('Mandos ({}) [%(process)d]:' + ' %(levelname)s: %(message)s'.format( + server_settings["servicename"]))) + + # Parse config file with clients + client_config = configparser.ConfigParser(Client.client_defaults) + client_config.read(os.path.join(server_settings["configdir"], + "clients.conf")) + + global mandos_dbus_service + mandos_dbus_service = None + + socketfd = None + if server_settings["socket"] != "": + socketfd = server_settings["socket"] + tcp_server = MandosServer( + (server_settings["address"], server_settings["port"]), + ClientHandler, + interface=(server_settings["interface"] or None), + use_ipv6=use_ipv6, + gnutls_priority=server_settings["priority"], + use_dbus=use_dbus, + socketfd=socketfd) + if not foreground: + pidfilename = "/run/mandos.pid" + if not os.path.isdir("/run/."): + pidfilename = "/var/run/mandos.pid" + pidfile = None + try: + pidfile = codecs.open(pidfilename, "w", encoding="utf-8") + except IOError as e: + logger.error("Could not open file %r", pidfilename, + exc_info=e) + + for name, group in (("_mandos", "_mandos"), + ("mandos", "mandos"), + ("nobody", "nogroup")): + try: + uid = pwd.getpwnam(name).pw_uid + gid = pwd.getpwnam(group).pw_gid + break + except KeyError: + continue + else: + uid = 65534 + gid = 65534 + try: + os.setgid(gid) + os.setuid(uid) + if debug: + logger.debug("Did setuid/setgid to {}:{}".format(uid, + gid)) + except OSError as error: + logger.warning("Failed to setuid/setgid to {}:{}: {}" + .format(uid, gid, os.strerror(error.errno))) + if error.errno != errno.EPERM: + raise + + if debug: + # Enable all possible GnuTLS debugging + + # "Use a log level over 10 to enable all debugging options." + # - GnuTLS manual + gnutls.global_set_log_level(11) + + @gnutls.log_func + def debug_gnutls(level, string): + logger.debug("GnuTLS: %s", string[:-1]) + + gnutls.global_set_log_function(debug_gnutls) + + # Redirect stdin so all checkers get /dev/null + null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR) + os.dup2(null, sys.stdin.fileno()) + if null > 2: + os.close(null) + + # Need to fork before connecting to D-Bus + if not foreground: + # Close all input and output, do double fork, etc. + daemon() + + if gi.version_info < (3, 10, 2): + # multiprocessing will use threads, so before we use GLib we + # need to inform GLib that threads will be used. + GLib.threads_init() + global main_loop - global bus - global server # From the Avahi example code - DBusGMainLoop(set_as_default=True ) - main_loop = gobject.MainLoop() + DBusGMainLoop(set_as_default=True) + main_loop = GLib.MainLoop() bus = dbus.SystemBus() - server = dbus.Interface( - bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ), - avahi.DBUS_INTERFACE_SERVER ) # End of Avahi example code - - debug = options.debug - - if debug: - console = logging.StreamHandler() - # console.setLevel(logging.DEBUG) - console.setFormatter(logging.Formatter\ - ('%(levelname)s: %(message)s')) - logger.addHandler(console) - del console - - clients = Set() - def remove_from_clients(client): - clients.remove(client) - if not clients: - logger.debug(u"No clients left, exiting") - killme() - - clients.update(Set(Client(name=section, - stop_hook = remove_from_clients, - **(dict(client_config\ - .items(section)))) - for section in client_config.sections())) - - if not debug: - daemon(False, False) - + if use_dbus: + try: + bus_name = dbus.service.BusName("se.recompile.Mandos", + bus, + do_not_queue=True) + old_bus_name = dbus.service.BusName( + "se.bsnet.fukt.Mandos", bus, + do_not_queue=True) + except dbus.exceptions.DBusException as e: + logger.error("Disabling D-Bus:", exc_info=e) + use_dbus = False + server_settings["use_dbus"] = False + tcp_server.use_dbus = False + if zeroconf: + protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET + service = AvahiServiceToSyslog( + name=server_settings["servicename"], + servicetype="_mandos._tcp", + protocol=protocol, + bus=bus) + if server_settings["interface"]: + service.interface = if_nametoindex( + server_settings["interface"].encode("utf-8")) + + global multiprocessing_manager + multiprocessing_manager = multiprocessing.Manager() + + client_class = Client + if use_dbus: + client_class = functools.partial(ClientDBus, bus=bus) + + client_settings = Client.config_parser(client_config) + old_client_settings = {} + clients_data = {} + + # This is used to redirect stdout and stderr for checker processes + global wnull + wnull = open(os.devnull, "w") # A writable /dev/null + # Only used if server is running in foreground but not in debug + # mode + if debug or not foreground: + wnull.close() + + # Get client data and settings from last running state. + if server_settings["restore"]: + try: + with open(stored_state_path, "rb") as stored_state: + if sys.version_info.major == 2: + clients_data, old_client_settings = pickle.load( + stored_state) + else: + bytes_clients_data, bytes_old_client_settings = ( + pickle.load(stored_state, encoding="bytes")) + # Fix bytes to strings + # clients_data + # .keys() + clients_data = {(key.decode("utf-8") + if isinstance(key, bytes) + else key): value + for key, value in + bytes_clients_data.items()} + del bytes_clients_data + for key in clients_data: + value = {(k.decode("utf-8") + if isinstance(k, bytes) else k): v + for k, v in + clients_data[key].items()} + clients_data[key] = value + # .client_structure + value["client_structure"] = [ + (s.decode("utf-8") + if isinstance(s, bytes) + else s) for s in + value["client_structure"]] + # .name, .host, and .checker_command + for k in ("name", "host", "checker_command"): + if isinstance(value[k], bytes): + value[k] = value[k].decode("utf-8") + if "key_id" not in value: + value["key_id"] = "" + elif "fingerprint" not in value: + value["fingerprint"] = "" + # old_client_settings + # .keys() + old_client_settings = { + (key.decode("utf-8") + if isinstance(key, bytes) + else key): value + for key, value in + bytes_old_client_settings.items()} + del bytes_old_client_settings + # .host and .checker_command + for value in old_client_settings.values(): + for attribute in ("host", "checker_command"): + if isinstance(value[attribute], bytes): + value[attribute] = (value[attribute] + .decode("utf-8")) + os.remove(stored_state_path) + except IOError as e: + if e.errno == errno.ENOENT: + logger.warning("Could not load persistent state:" + " {}".format(os.strerror(e.errno))) + else: + logger.critical("Could not load persistent state:", + exc_info=e) + raise + except EOFError as e: + logger.warning("Could not load persistent state: " + "EOFError:", + exc_info=e) + + with PGPEngine() as pgp: + for client_name, client in clients_data.items(): + # Skip removed clients + if client_name not in client_settings: + continue + + # Decide which value to use after restoring saved state. + # We have three different values: Old config file, + # new config file, and saved state. + # New config value takes precedence if it differs from old + # config value, otherwise use saved state. + for name, value in client_settings[client_name].items(): + try: + # For each value in new config, check if it + # differs from the old config value (Except for + # the "secret" attribute) + if (name != "secret" + and (value != + old_client_settings[client_name][name])): + client[name] = value + except KeyError: + pass + + # Clients who has passed its expire date can still be + # enabled if its last checker was successful. A Client + # whose checker succeeded before we stored its state is + # assumed to have successfully run all checkers during + # downtime. + if client["enabled"]: + if datetime.datetime.utcnow() >= client["expires"]: + if not client["last_checked_ok"]: + logger.warning( + "disabling client {} - Client never " + "performed a successful checker".format( + client_name)) + client["enabled"] = False + elif client["last_checker_status"] != 0: + logger.warning( + "disabling client {} - Client last" + " checker failed with error code" + " {}".format( + client_name, + client["last_checker_status"])) + client["enabled"] = False + else: + client["expires"] = ( + datetime.datetime.utcnow() + + client["timeout"]) + logger.debug("Last checker succeeded," + " keeping {} enabled".format( + client_name)) + try: + client["secret"] = pgp.decrypt( + client["encrypted_secret"], + client_settings[client_name]["secret"]) + except PGPError: + # If decryption fails, we use secret from new settings + logger.debug("Failed to decrypt {} old secret".format( + client_name)) + client["secret"] = (client_settings[client_name] + ["secret"]) + + # Add/remove clients based on new changes made to config + for client_name in (set(old_client_settings) + - set(client_settings)): + del clients_data[client_name] + for client_name in (set(client_settings) + - set(old_client_settings)): + clients_data[client_name] = client_settings[client_name] + + # Create all client objects + for client_name, client in clients_data.items(): + tcp_server.clients[client_name] = client_class( + name=client_name, + settings=client, + server_settings=server_settings) + + if not tcp_server.clients: + logger.warning("No clients defined") + + if not foreground: + if pidfile is not None: + pid = os.getpid() + try: + with pidfile: + print(pid, file=pidfile) + except IOError: + logger.error("Could not write to file %r with PID %d", + pidfilename, pid) + del pidfile + del pidfilename + + for termsig in (signal.SIGHUP, signal.SIGTERM): + GLib.unix_signal_add(GLib.PRIORITY_HIGH, termsig, + lambda: main_loop.quit() and False) + + if use_dbus: + + @alternate_dbus_interfaces( + {"se.recompile.Mandos": "se.bsnet.fukt.Mandos"}) + class MandosDBusService(DBusObjectWithObjectManager): + """A D-Bus proxy object""" + + def __init__(self): + dbus.service.Object.__init__(self, bus, "/") + + _interface = "se.recompile.Mandos" + + @dbus.service.signal(_interface, signature="o") + def ClientAdded(self, objpath): + "D-Bus signal" + pass + + @dbus.service.signal(_interface, signature="ss") + def ClientNotFound(self, key_id, address): + "D-Bus signal" + pass + + @dbus_annotations({"org.freedesktop.DBus.Deprecated": + "true"}) + @dbus.service.signal(_interface, signature="os") + def ClientRemoved(self, objpath, name): + "D-Bus signal" + pass + + @dbus_annotations({"org.freedesktop.DBus.Deprecated": + "true"}) + @dbus.service.method(_interface, out_signature="ao") + def GetAllClients(self): + "D-Bus method" + return dbus.Array(c.dbus_object_path for c in + tcp_server.clients.values()) + + @dbus_annotations({"org.freedesktop.DBus.Deprecated": + "true"}) + @dbus.service.method(_interface, + out_signature="a{oa{sv}}") + def GetAllClientsWithProperties(self): + "D-Bus method" + return dbus.Dictionary( + {c.dbus_object_path: c.GetAll( + "se.recompile.Mandos.Client") + for c in tcp_server.clients.values()}, + signature="oa{sv}") + + @dbus.service.method(_interface, in_signature="o") + def RemoveClient(self, object_path): + "D-Bus method" + for c in tcp_server.clients.values(): + if c.dbus_object_path == object_path: + del tcp_server.clients[c.name] + c.remove_from_connection() + # Don't signal the disabling + c.disable(quiet=True) + # Emit D-Bus signal for removal + self.client_removed_signal(c) + return + raise KeyError(object_path) + + del _interface + + @dbus.service.method(dbus.OBJECT_MANAGER_IFACE, + out_signature="a{oa{sa{sv}}}") + def GetManagedObjects(self): + """D-Bus method""" + return dbus.Dictionary( + {client.dbus_object_path: + dbus.Dictionary( + {interface: client.GetAll(interface) + for interface in + client._get_all_interface_names()}) + for client in tcp_server.clients.values()}) + + def client_added_signal(self, client): + """Send the new standard signal and the old signal""" + if use_dbus: + # New standard signal + self.InterfacesAdded( + client.dbus_object_path, + dbus.Dictionary( + {interface: client.GetAll(interface) + for interface in + client._get_all_interface_names()})) + # Old signal + self.ClientAdded(client.dbus_object_path) + + def client_removed_signal(self, client): + """Send the new standard signal and the old signal""" + if use_dbus: + # New standard signal + self.InterfacesRemoved( + client.dbus_object_path, + client._get_all_interface_names()) + # Old signal + self.ClientRemoved(client.dbus_object_path, + client.name) + + mandos_dbus_service = MandosDBusService() + + # Save modules to variables to exempt the modules from being + # unloaded before the function registered with atexit() is run. + mp = multiprocessing + wn = wnull + def cleanup(): "Cleanup function; run on exit" - global group - # From the Avahi example code - if not group is None: - group.Free() - group = None - # End of Avahi example code - - while clients: - client = clients.pop() - client.stop_hook = None - client.stop() - + if zeroconf: + service.cleanup() + + mp.active_children() + wn.close() + if not (tcp_server.clients or client_settings): + return + + # Store client before exiting. Secrets are encrypted with key + # based on what config file has. If config file is + # removed/edited, old secret will thus be unrecovable. + clients = {} + with PGPEngine() as pgp: + for client in tcp_server.clients.values(): + key = client_settings[client.name]["secret"] + client.encrypted_secret = pgp.encrypt(client.secret, + key) + client_dict = {} + + # A list of attributes that can not be pickled + # + secret. + exclude = {"bus", "changedstate", "secret", + "checker", "server_settings"} + for name, typ in inspect.getmembers(dbus.service + .Object): + exclude.add(name) + + client_dict["encrypted_secret"] = (client + .encrypted_secret) + for attr in client.client_structure: + if attr not in exclude: + client_dict[attr] = getattr(client, attr) + + clients[client.name] = client_dict + del client_settings[client.name]["secret"] + + try: + with tempfile.NamedTemporaryFile( + mode='wb', + suffix=".pickle", + prefix='clients-', + dir=os.path.dirname(stored_state_path), + delete=False) as stored_state: + pickle.dump((clients, client_settings), stored_state, + protocol=2) + tempname = stored_state.name + os.rename(tempname, stored_state_path) + except (IOError, OSError) as e: + if not debug: + try: + os.remove(tempname) + except NameError: + pass + if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST): + logger.warning("Could not save persistent state: {}" + .format(os.strerror(e.errno))) + else: + logger.warning("Could not save persistent state:", + exc_info=e) + raise + + # Delete all clients, and settings from config + while tcp_server.clients: + name, client = tcp_server.clients.popitem() + if use_dbus: + client.remove_from_connection() + # Don't signal the disabling + client.disable(quiet=True) + # Emit D-Bus signal for removal + if use_dbus: + mandos_dbus_service.client_removed_signal(client) + client_settings.clear() + atexit.register(cleanup) - - if not debug: - signal.signal(signal.SIGINT, signal.SIG_IGN) - signal.signal(signal.SIGHUP, lambda signum, frame: killme()) - signal.signal(signal.SIGTERM, lambda signum, frame: killme()) - - for client in clients: - client.start() - - tcp_server = IPv6_TCPServer((options.address, options.port), - tcp_handler, - options=options, - clients=clients) - # Find out what random port we got - global servicePort - servicePort = tcp_server.socket.getsockname()[1] - logger.debug(u"Now listening on port %d", servicePort) - - if options.interface is not None: - global serviceInterface - serviceInterface = if_nametoindex(options.interface) - - # From the Avahi example code - server.connect_to_signal("StateChanged", server_state_changed) - try: - server_state_changed(server.GetState()) - except dbus.exceptions.DBusException, error: - logger.critical(u"DBusException: %s", error) - killme(1) - # End of Avahi example code - - gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN, - lambda *args, **kwargs: - tcp_server.handle_request(*args[2:], - **kwargs) or True) - try: + + for client in tcp_server.clients.values(): + if use_dbus: + # Emit D-Bus signal for adding + mandos_dbus_service.client_added_signal(client) + # Need to initiate checking of clients + if client.enabled: + client.init_checker() + + tcp_server.enable() + tcp_server.server_activate() + + # Find out what port we got + if zeroconf: + service.port = tcp_server.socket.getsockname()[1] + if use_ipv6: + logger.info("Now listening on address %r, port %d," + " flowinfo %d, scope_id %d", + *tcp_server.socket.getsockname()) + else: # IPv4 + logger.info("Now listening on address %r, port %d", + *tcp_server.socket.getsockname()) + + # service.interface = tcp_server.socket.getsockname()[3] + + try: + if zeroconf: + # From the Avahi example code + try: + service.activate() + except dbus.exceptions.DBusException as error: + logger.critical("D-Bus Exception", exc_info=error) + cleanup() + sys.exit(1) + # End of Avahi example code + + GLib.io_add_watch( + GLib.IOChannel.unix_new(tcp_server.fileno()), + GLib.PRIORITY_DEFAULT, GLib.IO_IN, + lambda *args, **kwargs: (tcp_server.handle_request + (*args[2:], **kwargs) or True)) + logger.debug("Starting main loop") - main_loop_started = True main_loop.run() + except AvahiError as error: + logger.critical("Avahi Error", exc_info=error) + cleanup() + sys.exit(1) except KeyboardInterrupt: if debug: - print - - sys.exit(exitstatus) + print("", file=sys.stderr) + logger.debug("Server received KeyboardInterrupt") + logger.debug("Server exiting") + # Must run before the D-Bus bus name gets deregistered + cleanup() + + +def should_only_run_tests(): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--check", action='store_true') + args, unknown_args = parser.parse_known_args() + run_tests = args.check + if run_tests: + # Remove --check argument from sys.argv + sys.argv[1:] = unknown_args + return run_tests + +# Add all tests from doctest strings +def load_tests(loader, tests, none): + import doctest + tests.addTests(doctest.DocTestSuite()) + return tests if __name__ == '__main__': - main() + try: + if should_only_run_tests(): + # Call using ./mandos --check [--verbose] + unittest.main() + else: + main() + finally: + logging.shutdown() === added file 'mandos-clients.conf.xml' --- mandos-clients.conf.xml 1970-01-01 00:00:00 +0000 +++ mandos-clients.conf.xml 2019-02-10 04:20:26 +0000 @@ -0,0 +1,567 @@ + + +/etc/mandos/clients.conf"> + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &CONFNAME; + 5 + + + + &CONFNAME; + + Configuration file for the Mandos server + + + + + &CONFPATH; + + + + DESCRIPTION + + The file &CONFPATH; is a configuration file for mandos + 8, read by it at startup. + The file needs to list all clients that should be able to use + the service. The settings in this file can be overridden by + runtime changes to the server, which it saves across restarts. + (See the section called PERSISTENT STATE in + mandos8.) However, any changes to this file (including adding and removing + clients) will, at startup, override changes done during runtime. + + + The format starts with a [section + header] which is either + [DEFAULT] or [client + name]. The client + name can be anything, and is not tied to a host + name. Following the section header is any number of + option=value entries, + with continuations in the style of RFC 822. option: value is also accepted. Note that + leading whitespace is removed from values. Values can contain + format strings which refer to other values in the same section, + or values in the DEFAULT section (see ). Lines beginning with # + or ; are ignored and may be used to provide + comments. + + + + + OPTIONS + + Note: all option values are subject to + start time expansion, see . + + + Unknown options are ignored. The used options are as follows: + + + + + + + + + This option is optional. + + + How long to wait for external approval before resorting to + use the value. The + default is PT0S, i.e. not to wait. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + This option is optional. + + + How long an external approval lasts. The default is 1 + second. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + Whether to approve a client by default after + the . The default + is True. + + + + + + + + + This option is optional. + + + This option overrides the default shell command that the + server will use to check if the client is still up. Any + output of the command will be ignored, only the exit code + is checked: If the exit code of the command is zero, the + client is considered up. The command will be run using + /bin/sh + , so + PATH will be searched. The default + value for the checker command is fping %%(host)s. Note that + mandos-keygen, when generating output + to be inserted into this file, normally looks for an SSH + server on the Mandos client, and, if it finds one, outputs + a option to check for the + client’s SSH key fingerprint – this is more secure against + spoofing. + + + In addition to normal start time expansion, this option + will also be subject to runtime expansion; see . + + + + + + + + + This option is optional. + + + Extended timeout is an added timeout that is given once + after a password has been sent successfully to a client. + The timeout is by default longer than the normal timeout, + and is used for handling the extra long downtime while a + machine is booting up. Time to take into consideration + when changing this value is file system checks and quota + checks. The default value is 15 minutes. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + This option is required. + + + This option sets the OpenPGP fingerprint that identifies + the public key that clients authenticate themselves with + through TLS. The string needs to be in hexadecimal form, + but spaces or upper/lower case are not significant. + + + + + + + + + This option is optional. + + + This option sets the certificate key ID that identifies + the public key that clients authenticate themselves with + through TLS. The string needs to be in hexadecimal form, + but spaces or upper/lower case are not significant. + + + + + + + + + This option is optional, but highly + recommended unless the + option is modified to a + non-standard value without %%(host)s in it. + + + Host name for this client. This is not used by the server + directly, but can be, and is by default, used by the + checker. See the option. + + + + + + + + + This option is optional. + + + How often to run the checker to confirm that a client is + still up. Note: a new checker will + not be started if an old one is still running. The server + will wait for a checker to complete until the below + timeout occurs, at which + time the client will be disabled, and any running checker + killed. The default interval is 2 minutes. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + This option is only used if is not + specified, in which case this option is + required. + + + Similar to the , except the secret + data is in an external file. The contents of the file + should not be base64-encoded, but + will be sent to clients verbatim. + + + File names of the form ~user/foo/bar + and $ENVVAR/foo/bar + are supported. + + + + + + + + + If this option is not specified, the option is required + to be present. + + + If present, this option must be set to a string of + base64-encoded binary data. It will be decoded and sent + to the client matching the above + or . This should, of course, + be OpenPGP encrypted data, decryptable only by the client. + The program mandos-keygen8 can, using its + option, be used to generate + this, if desired. + + + Note: this value of this option will probably be very + long. A useful feature to avoid having unreadably-long + lines is that a line beginning with white space adds to + the value of the previous line, RFC 822-style. + + + + + + + + + This option is optional. + + + The timeout is how long the server will wait, after a + successful checker run, until a client is disabled and not + allowed to get the data this server holds. By default + Mandos will use 5 minutes. See also the + option. + + + The TIME is specified as an RFC + 3339 duration; for example + P1Y2M3DT4H5M6S meaning + one year, two months, three days, four hours, five + minutes, and six seconds. Some values can be omitted, see + RFC 3339 Appendix A for details. + + + + + + + + + Whether this client should be enabled by default. The + default is true. + + + + + + + + + EXPANSION + + There are two forms of expansion: Start time expansion and + runtime expansion. + + + START TIME EXPANSION + + Any string in an option value of the form + %(foo)s will be replaced by the value of the option + foo either in the same section, or, if it + does not exist there, the [DEFAULT] + section. This is done at start time, when the configuration + file is read. + + + Note that this means that, in order to include an actual + percent character (%) in an option value, two + percent characters in a row (%%) must be + entered. + + + + RUNTIME EXPANSION + + This is currently only done for the checker + option. + + + Any string in an option value of the form + %%(foo)s will be replaced by the value of the attribute + foo of the internal + Client object in the + Mandos server. The currently allowed values for + foo are: + approval_delay, + approval_duration, + created, + enabled, + expires, + key_id, + fingerprint, + host, + interval, + last_approval_request, + last_checked_ok, + last_enabled, + name, + timeout, and, if using + D-Bus, dbus_object_path. + See the source code for details. Currently, none of these attributes + except host are guaranteed + to be valid in future versions. Therefore, please + let the authors know of any attributes that are useful so they + may be preserved to any new versions of this software. + + + Note that this means that, in order to include an actual + percent character (%) in a + checker option, four + percent characters in a row (%%%%) must be + entered. Also, a bad format here will lead to an immediate + but silent run-time fatal exit; debug + mode is needed to expose an error of this kind. + + + + + + + FILES + + The file described here is &CONFPATH; + + + + + BUGS + + The format for specifying times for timeout + and interval is not very good. + + + The difference between + %%(foo)s and + %(foo)s is + obscure. + + + + + + EXAMPLE + + +[DEFAULT] +timeout = PT5M +interval = PT2M +checker = fping -q -- %%(host)s + +# Client "foo" +[foo] +key_id = 788cd77115cd0bb7b2d5e0ae8496f6b48149d5e712c652076b1fd2d957ef7c1f +fingerprint = 7788 2722 5BA7 DE53 9C5A 7CFA 59CF F7CD BD9A 5920 +secret = + hQIOA6QdEjBs2L/HEAf/TCyrDe5Xnm9esa+Pb/vWF9CUqfn4srzVgSu234 + REJMVv7lBSrPE2132Lmd2gqF1HeLKDJRSVxJpt6xoWOChGHg+TMyXDxK+N + Xl89vGvdU1XfhKkVm9MDLOgT5ECDPysDGHFPDhqHOSu3Kaw2DWMV/iH9vz + 3Z20erVNbdcvyBnuojcoWO/6yfB5EQO0BXp7kcyy00USA3CjD5FGZdoQGI + Tb8A/ar0tVA5crSQmaSotm6KmNLhrFnZ5BxX+TiE+eTUTqSloWRY6VAvqW + QHC7OASxK5E6RXPBuFH5IohUA2Qbk5AHt99pYvsIPX88j2rWauOokoiKZo + t/9leJ8VxO5l3wf/U64IH8bkPIoWmWZfd/nqh4uwGNbCgKMyT+AnvH7kMJ + 3i7DivfWl2mKLV0PyPHUNva0VQxX6yYjcOhj1R6fCr/at8/NSLe2OhLchz + dC+Ls9h+kvJXgF8Sisv+Wk/1RadPLFmraRlqvJwt6Ww21LpiXqXHV2mIgq + WnR98YgSvUi3TJHrUQiNc9YyBzuRo0AjgG2C9qiE3FM+Y28+iQ/sR3+bFs + zYuZKVTObqiIslwXu7imO0cvvFRgJF/6u3HNFQ4LUTGhiM3FQmC6NNlF3/ + vJM2hwRDMcJqDd54Twx90Wh+tYz0z7QMsK4ANXWHHWHR0JchnLWmenzbtW + 5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm + 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O + QlnHIvPzEArRQLo= +host = foo.example.org +interval = PT1M + +# Client "bar" +[bar] +key_id = F90C7A81D72D1EA69A51031A91FF8885F36C8B46D155C8C58709A4C99AE9E361 +fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 +secfile = /etc/mandos/bar-secret +timeout = PT15M +approved_by_default = False +approval_delay = PT30S + + + + + + SEE ALSO + + intro + 8mandos, + mandos-keygen + 8, + mandos.conf + 5, + mandos + 8, + fping + 8 + + + + + RFC 3339: Date and Time on the Internet: + Timestamps + + + + The time intervals are in the "duration" format, as + specified in ABNF in Appendix A of RFC 3339. + + + + + +
+ + + + + === added file 'mandos-ctl' --- mandos-ctl 1970-01-01 00:00:00 +0000 +++ mandos-ctl 2019-11-03 19:09:41 +0000 @@ -0,0 +1,2535 @@ +#!/usr/bin/python3 -bbI +# -*- after-save-hook: (lambda () (let ((command (if (fboundp 'file-local-name) (file-local-name (buffer-file-name)) (or (file-remote-p (buffer-file-name) 'localname) (buffer-file-name))))) (if (= (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (format "%s --check" (shell-quote-argument command)) nil "*Test*")) 0) (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w))) (progn (with-current-buffer "*Test*" (compilation-mode)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); coding: utf-8 -*- +# +# Mandos Monitor - Control and monitor the Mandos server +# +# Copyright © 2008-2019 Teddy Hogeborn +# Copyright © 2008-2019 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos 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. +# +# Mandos 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 Mandos. If not, see . +# +# Contact the authors at . +# + +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +try: + from future_builtins import * +except ImportError: + pass + +import sys +import argparse +import locale +import datetime +import re +import os +import collections +import json +import unittest +import logging +import io +import tempfile +import contextlib + +if sys.version_info.major == 2: + __metaclass__ = type + +try: + import pydbus + import gi + dbus_python = None +except ImportError: + import dbus as dbus_python + pydbus = None + class gi: + """Dummy gi module, for the tests""" + class repository: + class GLib: + class Error(Exception): + pass + +# Show warnings by default +if not sys.warnoptions: + import warnings + warnings.simplefilter("default") + +log = logging.getLogger(sys.argv[0]) +logging.basicConfig(level="INFO", # Show info level messages + format="%(message)s") # Show basic log messages + +logging.captureWarnings(True) # Show warnings via the logging system + +if sys.version_info.major == 2: + str = unicode + import StringIO + io.StringIO = StringIO.StringIO + +locale.setlocale(locale.LC_ALL, "") + +version = "1.8.9" + + +def main(): + parser = argparse.ArgumentParser() + add_command_line_options(parser) + + options = parser.parse_args() + check_option_syntax(parser, options) + + clientnames = options.client + + if options.debug: + log.setLevel(logging.DEBUG) + + if pydbus is not None: + bus = pydbus_adapter.CachingBus(pydbus) + else: + bus = dbus_python_adapter.CachingBus(dbus_python) + + try: + all_clients = bus.get_clients_and_properties() + except dbus.ConnectFailed as e: + log.critical("Could not connect to Mandos server: %s", e) + sys.exit(1) + except dbus.Error as e: + log.critical( + "Failed to access Mandos server through D-Bus:\n%s", e) + sys.exit(1) + + # Compile dict of (clientpath: properties) to process + if not clientnames: + clients = all_clients + else: + clients = {} + for name in clientnames: + for objpath, properties in all_clients.items(): + if properties["Name"] == name: + clients[objpath] = properties + break + else: + log.critical("Client not found on server: %r", name) + sys.exit(1) + + commands = commands_from_options(options) + + for command in commands: + command.run(clients, bus) + + +def add_command_line_options(parser): + parser.add_argument("--version", action="version", + version="%(prog)s {}".format(version), + help="show version number and exit") + parser.add_argument("-a", "--all", action="store_true", + help="Select all clients") + parser.add_argument("-v", "--verbose", action="store_true", + help="Print all fields") + parser.add_argument("-j", "--dump-json", dest="commands", + action="append_const", default=[], + const=command.DumpJSON(), + help="Dump client data in JSON format") + enable_disable = parser.add_mutually_exclusive_group() + enable_disable.add_argument("-e", "--enable", dest="commands", + action="append_const", default=[], + const=command.Enable(), + help="Enable client") + enable_disable.add_argument("-d", "--disable", dest="commands", + action="append_const", default=[], + const=command.Disable(), + help="disable client") + parser.add_argument("-b", "--bump-timeout", dest="commands", + action="append_const", default=[], + const=command.BumpTimeout(), + help="Bump timeout for client") + start_stop_checker = parser.add_mutually_exclusive_group() + start_stop_checker.add_argument("--start-checker", + dest="commands", + action="append_const", default=[], + const=command.StartChecker(), + help="Start checker for client") + start_stop_checker.add_argument("--stop-checker", dest="commands", + action="append_const", default=[], + const=command.StopChecker(), + help="Stop checker for client") + parser.add_argument("-V", "--is-enabled", dest="commands", + action="append_const", default=[], + const=command.IsEnabled(), + help="Check if client is enabled") + parser.add_argument("-r", "--remove", dest="commands", + action="append_const", default=[], + const=command.Remove(), + help="Remove client") + parser.add_argument("-c", "--checker", dest="commands", + action="append", default=[], + metavar="COMMAND", type=command.SetChecker, + help="Set checker command for client") + parser.add_argument( + "-t", "--timeout", dest="commands", action="append", + default=[], metavar="TIME", + type=command.SetTimeout.argparse(string_to_delta), + help="Set timeout for client") + parser.add_argument( + "--extended-timeout", dest="commands", action="append", + default=[], metavar="TIME", + type=command.SetExtendedTimeout.argparse(string_to_delta), + help="Set extended timeout for client") + parser.add_argument( + "-i", "--interval", dest="commands", action="append", + default=[], metavar="TIME", + type=command.SetInterval.argparse(string_to_delta), + help="Set checker interval for client") + approve_deny_default = parser.add_mutually_exclusive_group() + approve_deny_default.add_argument( + "--approve-by-default", dest="commands", + action="append_const", default=[], + const=command.ApproveByDefault(), + help="Set client to be approved by default") + approve_deny_default.add_argument( + "--deny-by-default", dest="commands", + action="append_const", default=[], + const=command.DenyByDefault(), + help="Set client to be denied by default") + parser.add_argument( + "--approval-delay", dest="commands", action="append", + default=[], metavar="TIME", + type=command.SetApprovalDelay.argparse(string_to_delta), + help="Set delay before client approve/deny") + parser.add_argument( + "--approval-duration", dest="commands", action="append", + default=[], metavar="TIME", + type=command.SetApprovalDuration.argparse(string_to_delta), + help="Set duration of one client approval") + parser.add_argument("-H", "--host", dest="commands", + action="append", default=[], metavar="STRING", + type=command.SetHost, + help="Set host for client") + parser.add_argument( + "-s", "--secret", dest="commands", action="append", + default=[], metavar="FILENAME", + type=command.SetSecret.argparse(argparse.FileType(mode="rb")), + help="Set password blob (file) for client") + approve_deny = parser.add_mutually_exclusive_group() + approve_deny.add_argument( + "-A", "--approve", dest="commands", action="append_const", + default=[], const=command.Approve(), + help="Approve any current client request") + approve_deny.add_argument("-D", "--deny", dest="commands", + action="append_const", default=[], + const=command.Deny(), + help="Deny any current client request") + parser.add_argument("--debug", action="store_true", + help="Debug mode (show D-Bus commands)") + parser.add_argument("--check", action="store_true", + help="Run self-test") + parser.add_argument("client", nargs="*", help="Client name") + + +def string_to_delta(interval): + """Parse a string and return a datetime.timedelta""" + + try: + return rfc3339_duration_to_delta(interval) + except ValueError as e: + log.warning("%s - Parsing as pre-1.6.1 interval instead", + ' '.join(e.args)) + return parse_pre_1_6_1_interval(interval) + + +def rfc3339_duration_to_delta(duration): + """Parse an RFC 3339 "duration" and return a datetime.timedelta + + >>> rfc3339_duration_to_delta("P7D") == datetime.timedelta(7) + True + >>> rfc3339_duration_to_delta("PT60S") == datetime.timedelta(0, 60) + True + >>> rfc3339_duration_to_delta("PT60M") == datetime.timedelta(hours=1) + True + >>> # 60 months + >>> rfc3339_duration_to_delta("P60M") == datetime.timedelta(1680) + True + >>> rfc3339_duration_to_delta("PT24H") == datetime.timedelta(1) + True + >>> rfc3339_duration_to_delta("P1W") == datetime.timedelta(7) + True + >>> rfc3339_duration_to_delta("PT5M30S") == datetime.timedelta(0, 330) + True + >>> rfc3339_duration_to_delta("P1DT3M20S") == datetime.timedelta(1, 200) + True + >>> # Can not be empty: + >>> rfc3339_duration_to_delta("") + Traceback (most recent call last): + ... + ValueError: Invalid RFC 3339 duration: "" + >>> # Must start with "P": + >>> rfc3339_duration_to_delta("1D") + Traceback (most recent call last): + ... + ValueError: Invalid RFC 3339 duration: "1D" + >>> # Must use correct order + >>> rfc3339_duration_to_delta("PT1S2M") + Traceback (most recent call last): + ... + ValueError: Invalid RFC 3339 duration: "PT1S2M" + >>> # Time needs time marker + >>> rfc3339_duration_to_delta("P1H2S") + Traceback (most recent call last): + ... + ValueError: Invalid RFC 3339 duration: "P1H2S" + >>> # Weeks can not be combined with anything else + >>> rfc3339_duration_to_delta("P1D2W") + Traceback (most recent call last): + ... + ValueError: Invalid RFC 3339 duration: "P1D2W" + >>> rfc3339_duration_to_delta("P2W2H") + Traceback (most recent call last): + ... + ValueError: Invalid RFC 3339 duration: "P2W2H" + """ + + # Parsing an RFC 3339 duration with regular expressions is not + # possible - there would have to be multiple places for the same + # values, like seconds. The current code, while more esoteric, is + # cleaner without depending on a parsing library. If Python had a + # built-in library for parsing we would use it, but we'd like to + # avoid excessive use of external libraries. + + # New type for defining tokens, syntax, and semantics all-in-one + Token = collections.namedtuple("Token", ( + "regexp", # To match token; if "value" is not None, must have + # a "group" containing digits + "value", # datetime.timedelta or None + "followers")) # Tokens valid after this token + # RFC 3339 "duration" tokens, syntax, and semantics; taken from + # the "duration" ABNF definition in RFC 3339, Appendix A. + token_end = Token(re.compile(r"$"), None, frozenset()) + token_second = Token(re.compile(r"(\d+)S"), + datetime.timedelta(seconds=1), + frozenset((token_end, ))) + token_minute = Token(re.compile(r"(\d+)M"), + datetime.timedelta(minutes=1), + frozenset((token_second, token_end))) + token_hour = Token(re.compile(r"(\d+)H"), + datetime.timedelta(hours=1), + frozenset((token_minute, token_end))) + token_time = Token(re.compile(r"T"), + None, + frozenset((token_hour, token_minute, + token_second))) + token_day = Token(re.compile(r"(\d+)D"), + datetime.timedelta(days=1), + frozenset((token_time, token_end))) + token_month = Token(re.compile(r"(\d+)M"), + datetime.timedelta(weeks=4), + frozenset((token_day, token_end))) + token_year = Token(re.compile(r"(\d+)Y"), + datetime.timedelta(weeks=52), + frozenset((token_month, token_end))) + token_week = Token(re.compile(r"(\d+)W"), + datetime.timedelta(weeks=1), + frozenset((token_end, ))) + token_duration = Token(re.compile(r"P"), None, + frozenset((token_year, token_month, + token_day, token_time, + token_week))) + # Define starting values: + # Value so far + value = datetime.timedelta() + found_token = None + # Following valid tokens + followers = frozenset((token_duration, )) + # String left to parse + s = duration + # Loop until end token is found + while found_token is not token_end: + # Search for any currently valid tokens + for token in followers: + match = token.regexp.match(s) + if match is not None: + # Token found + if token.value is not None: + # Value found, parse digits + factor = int(match.group(1), 10) + # Add to value so far + value += factor * token.value + # Strip token from string + s = token.regexp.sub("", s, 1) + # Go to found token + found_token = token + # Set valid next tokens + followers = found_token.followers + break + else: + # No currently valid tokens were found + raise ValueError("Invalid RFC 3339 duration: \"{}\"" + .format(duration)) + # End token found + return value + + +def parse_pre_1_6_1_interval(interval): + """Parse an interval string as documented by Mandos before 1.6.1, + and return a datetime.timedelta + + >>> parse_pre_1_6_1_interval('7d') == datetime.timedelta(days=7) + True + >>> parse_pre_1_6_1_interval('60s') == datetime.timedelta(0, 60) + True + >>> parse_pre_1_6_1_interval('60m') == datetime.timedelta(hours=1) + True + >>> parse_pre_1_6_1_interval('24h') == datetime.timedelta(days=1) + True + >>> parse_pre_1_6_1_interval('1w') == datetime.timedelta(days=7) + True + >>> parse_pre_1_6_1_interval('5m 30s') == datetime.timedelta(0, 330) + True + >>> parse_pre_1_6_1_interval('') == datetime.timedelta(0) + True + >>> # Ignore unknown characters, allow any order and repetitions + >>> parse_pre_1_6_1_interval('2dxy7zz11y3m5m') == datetime.timedelta(2, 480, 18000) + True + + """ + + value = datetime.timedelta(0) + regexp = re.compile(r"(\d+)([dsmhw]?)") + + for num, suffix in regexp.findall(interval): + if suffix == "d": + value += datetime.timedelta(int(num)) + elif suffix == "s": + value += datetime.timedelta(0, int(num)) + elif suffix == "m": + value += datetime.timedelta(0, 0, 0, 0, int(num)) + elif suffix == "h": + value += datetime.timedelta(0, 0, 0, 0, 0, int(num)) + elif suffix == "w": + value += datetime.timedelta(0, 0, 0, 0, 0, 0, int(num)) + elif suffix == "": + value += datetime.timedelta(0, 0, 0, int(num)) + return value + + +def check_option_syntax(parser, options): + """Apply additional restrictions on options, not expressible in +argparse""" + + def has_commands(options, commands=None): + if commands is None: + commands = (command.Enable, + command.Disable, + command.BumpTimeout, + command.StartChecker, + command.StopChecker, + command.IsEnabled, + command.Remove, + command.SetChecker, + command.SetTimeout, + command.SetExtendedTimeout, + command.SetInterval, + command.ApproveByDefault, + command.DenyByDefault, + command.SetApprovalDelay, + command.SetApprovalDuration, + command.SetHost, + command.SetSecret, + command.Approve, + command.Deny) + return any(isinstance(cmd, commands) + for cmd in options.commands) + + if has_commands(options) and not (options.client or options.all): + parser.error("Options require clients names or --all.") + if options.verbose and has_commands(options): + parser.error("--verbose can only be used alone.") + if (has_commands(options, (command.DumpJSON,)) + and (options.verbose or len(options.commands) > 1)): + parser.error("--dump-json can only be used alone.") + if options.all and not has_commands(options): + parser.error("--all requires an action.") + if (has_commands(options, (command.IsEnabled,)) + and len(options.client) > 1): + parser.error("--is-enabled requires exactly one client") + if (len(options.commands) > 1 + and has_commands(options, (command.Remove,)) + and not has_commands(options, (command.Deny,))): + parser.error("--remove can only be combined with --deny") + + +class dbus: + + class SystemBus: + + object_manager_iface = "org.freedesktop.DBus.ObjectManager" + def get_managed_objects(self, busname, objectpath): + return self.call_method("GetManagedObjects", busname, + objectpath, + self.object_manager_iface) + + properties_iface = "org.freedesktop.DBus.Properties" + def set_property(self, busname, objectpath, interface, key, + value): + self.call_method("Set", busname, objectpath, + self.properties_iface, interface, key, + value) + + + class MandosBus(SystemBus): + busname_domain = "se.recompile" + busname = busname_domain + ".Mandos" + server_path = "/" + server_interface = busname_domain + ".Mandos" + client_interface = busname_domain + ".Mandos.Client" + del busname_domain + + def get_clients_and_properties(self): + managed_objects = self.get_managed_objects( + self.busname, self.server_path) + return {objpath: properties[self.client_interface] + for objpath, properties in managed_objects.items() + if self.client_interface in properties} + + def set_client_property(self, objectpath, key, value): + return self.set_property(self.busname, objectpath, + self.client_interface, key, + value) + + def call_client_method(self, objectpath, method, *args): + return self.call_method(method, self.busname, objectpath, + self.client_interface, *args) + + def call_server_method(self, method, *args): + return self.call_method(method, self.busname, + self.server_path, + self.server_interface, *args) + + class Error(Exception): + pass + + class ConnectFailed(Error): + pass + + +class dbus_python_adapter: + + class SystemBus(dbus.MandosBus): + """Use dbus-python""" + + def __init__(self, module=dbus_python): + self.dbus_python = module + self.bus = self.dbus_python.SystemBus() + + @contextlib.contextmanager + def convert_exception(self, exception_class=dbus.Error): + try: + yield + except self.dbus_python.exceptions.DBusException as e: + # This does what "raise from" would do + exc = exception_class(*e.args) + exc.__cause__ = e + raise exc + + def call_method(self, methodname, busname, objectpath, + interface, *args): + proxy_object = self.get_object(busname, objectpath) + log.debug("D-Bus: %s:%s:%s.%s(%s)", busname, objectpath, + interface, methodname, + ", ".join(repr(a) for a in args)) + method = getattr(proxy_object, methodname) + with self.convert_exception(): + with dbus_python_adapter.SilenceLogger( + "dbus.proxies"): + value = method(*args, dbus_interface=interface) + return self.type_filter(value) + + def get_object(self, busname, objectpath): + log.debug("D-Bus: Connect to: (busname=%r, path=%r)", + busname, objectpath) + with self.convert_exception(dbus.ConnectFailed): + return self.bus.get_object(busname, objectpath) + + def type_filter(self, value): + """Convert the most bothersome types to Python types""" + if isinstance(value, self.dbus_python.Boolean): + return bool(value) + if isinstance(value, self.dbus_python.ObjectPath): + return str(value) + # Also recurse into dictionaries + if isinstance(value, self.dbus_python.Dictionary): + return {self.type_filter(key): + self.type_filter(subval) + for key, subval in value.items()} + return value + + def set_client_property(self, objectpath, key, value): + if key == "Secret": + if not isinstance(value, bytes): + value = value.encode("utf-8") + value = self.dbus_python.ByteArray(value) + return self.set_property(self.busname, objectpath, + self.client_interface, key, + value) + + class SilenceLogger: + "Simple context manager to silence a particular logger" + def __init__(self, loggername): + self.logger = logging.getLogger(loggername) + + def __enter__(self): + self.logger.addFilter(self.nullfilter) + + class NullFilter(logging.Filter): + def filter(self, record): + return False + + nullfilter = NullFilter() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.logger.removeFilter(self.nullfilter) + + + class CachingBus(SystemBus): + """A caching layer for dbus_python_adapter.SystemBus""" + def __init__(self, *args, **kwargs): + self.object_cache = {} + super(dbus_python_adapter.CachingBus, + self).__init__(*args, **kwargs) + def get_object(self, busname, objectpath): + try: + return self.object_cache[(busname, objectpath)] + except KeyError: + new_object = super( + dbus_python_adapter.CachingBus, + self).get_object(busname, objectpath) + self.object_cache[(busname, objectpath)] = new_object + return new_object + + +class pydbus_adapter: + class SystemBus(dbus.MandosBus): + def __init__(self, module=pydbus): + self.pydbus = module + self.bus = self.pydbus.SystemBus() + + @contextlib.contextmanager + def convert_exception(self, exception_class=dbus.Error): + try: + yield + except gi.repository.GLib.Error as e: + # This does what "raise from" would do + exc = exception_class(*e.args) + exc.__cause__ = e + raise exc + + def call_method(self, methodname, busname, objectpath, + interface, *args): + proxy_object = self.get(busname, objectpath) + log.debug("D-Bus: %s:%s:%s.%s(%s)", busname, objectpath, + interface, methodname, + ", ".join(repr(a) for a in args)) + method = getattr(proxy_object[interface], methodname) + with self.convert_exception(): + return method(*args) + + def get(self, busname, objectpath): + log.debug("D-Bus: Connect to: (busname=%r, path=%r)", + busname, objectpath) + with self.convert_exception(dbus.ConnectFailed): + if sys.version_info.major <= 2: + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", "", DeprecationWarning, + r"^xml\.etree\.ElementTree$") + return self.bus.get(busname, objectpath) + else: + return self.bus.get(busname, objectpath) + + def set_property(self, busname, objectpath, interface, key, + value): + proxy_object = self.get(busname, objectpath) + log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", busname, + objectpath, self.properties_iface, interface, + key, value) + setattr(proxy_object[interface], key, value) + + class CachingBus(SystemBus): + """A caching layer for pydbus_adapter.SystemBus""" + def __init__(self, *args, **kwargs): + self.object_cache = {} + super(pydbus_adapter.CachingBus, + self).__init__(*args, **kwargs) + def get(self, busname, objectpath): + try: + return self.object_cache[(busname, objectpath)] + except KeyError: + new_object = (super(pydbus_adapter.CachingBus, self) + .get(busname, objectpath)) + self.object_cache[(busname, objectpath)] = new_object + return new_object + + +def commands_from_options(options): + + commands = list(options.commands) + + def find_cmd(cmd, commands): + i = 0 + for i, c in enumerate(commands): + if isinstance(c, cmd): + return i + return i+1 + + # If command.Remove is present, move any instances of command.Deny + # to occur ahead of command.Remove. + index_of_remove = find_cmd(command.Remove, commands) + before_remove = commands[:index_of_remove] + after_remove = commands[index_of_remove:] + cleaned_after = [] + for cmd in after_remove: + if isinstance(cmd, command.Deny): + before_remove.append(cmd) + else: + cleaned_after.append(cmd) + if cleaned_after != after_remove: + commands = before_remove + cleaned_after + + # If no command option has been given, show table of clients, + # optionally verbosely + if not commands: + commands.append(command.PrintTable(verbose=options.verbose)) + + return commands + + +class command: + """A namespace for command classes""" + + class Base: + """Abstract base class for commands""" + def run(self, clients, bus=None): + """Normal commands should implement run_on_one_client(), +but commands which want to operate on all clients at the same time can +override this run() method instead. +""" + self.bus = bus + for client, properties in clients.items(): + self.run_on_one_client(client, properties) + + + class IsEnabled(Base): + def run(self, clients, bus=None): + properties = next(iter(clients.values())) + if properties["Enabled"]: + sys.exit(0) + sys.exit(1) + + + class Approve(Base): + def run_on_one_client(self, client, properties): + self.bus.call_client_method(client, "Approve", True) + + + class Deny(Base): + def run_on_one_client(self, client, properties): + self.bus.call_client_method(client, "Approve", False) + + + class Remove(Base): + def run(self, clients, bus): + for clientpath in frozenset(clients.keys()): + bus.call_server_method("RemoveClient", clientpath) + + + class Output(Base): + """Abstract class for commands outputting client details""" + all_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK", + "Created", "Interval", "Host", "KeyID", + "Fingerprint", "CheckerRunning", + "LastEnabled", "ApprovalPending", + "ApprovedByDefault", "LastApprovalRequest", + "ApprovalDelay", "ApprovalDuration", + "Checker", "ExtendedTimeout", "Expires", + "LastCheckerStatus") + + + class DumpJSON(Output): + def run(self, clients, bus=None): + data = {properties["Name"]: + {key: properties[key] + for key in self.all_keywords} + for properties in clients.values()} + print(json.dumps(data, indent=4, separators=(',', ': '))) + + + class PrintTable(Output): + def __init__(self, verbose=False): + self.verbose = verbose + + def run(self, clients, bus=None): + default_keywords = ("Name", "Enabled", "Timeout", + "LastCheckedOK") + keywords = default_keywords + if self.verbose: + keywords = self.all_keywords + print(self.TableOfClients(clients.values(), keywords)) + + class TableOfClients: + tableheaders = { + "Name": "Name", + "Enabled": "Enabled", + "Timeout": "Timeout", + "LastCheckedOK": "Last Successful Check", + "LastApprovalRequest": "Last Approval Request", + "Created": "Created", + "Interval": "Interval", + "Host": "Host", + "Fingerprint": "Fingerprint", + "KeyID": "Key ID", + "CheckerRunning": "Check Is Running", + "LastEnabled": "Last Enabled", + "ApprovalPending": "Approval Is Pending", + "ApprovedByDefault": "Approved By Default", + "ApprovalDelay": "Approval Delay", + "ApprovalDuration": "Approval Duration", + "Checker": "Checker", + "ExtendedTimeout": "Extended Timeout", + "Expires": "Expires", + "LastCheckerStatus": "Last Checker Status", + } + + def __init__(self, clients, keywords): + self.clients = clients + self.keywords = keywords + + def __str__(self): + return "\n".join(self.rows()) + + if sys.version_info.major == 2: + __unicode__ = __str__ + def __str__(self): + return str(self).encode( + locale.getpreferredencoding()) + + def rows(self): + format_string = self.row_formatting_string() + rows = [self.header_line(format_string)] + rows.extend(self.client_line(client, format_string) + for client in self.clients) + return rows + + def row_formatting_string(self): + "Format string used to format table rows" + return " ".join("{{{key}:{width}}}".format( + width=max(len(self.tableheaders[key]), + *(len(self.string_from_client(client, + key)) + for client in self.clients)), + key=key) + for key in self.keywords) + + def string_from_client(self, client, key): + return self.valuetostring(client[key], key) + + @classmethod + def valuetostring(cls, value, keyword): + if isinstance(value, bool): + return "Yes" if value else "No" + if keyword in ("Timeout", "Interval", "ApprovalDelay", + "ApprovalDuration", "ExtendedTimeout"): + return cls.milliseconds_to_string(value) + return str(value) + + def header_line(self, format_string): + return format_string.format(**self.tableheaders) + + def client_line(self, client, format_string): + return format_string.format( + **{key: self.string_from_client(client, key) + for key in self.keywords}) + + @staticmethod + def milliseconds_to_string(ms): + td = datetime.timedelta(0, 0, 0, ms) + return ("{days}{hours:02}:{minutes:02}:{seconds:02}" + .format(days="{}T".format(td.days) + if td.days else "", + hours=td.seconds // 3600, + minutes=(td.seconds % 3600) // 60, + seconds=td.seconds % 60)) + + + class PropertySetter(Base): + "Abstract class for Actions for setting one client property" + + def run_on_one_client(self, client, properties=None): + """Set the Client's D-Bus property""" + self.bus.set_client_property(client, self.propname, + self.value_to_set) + + @property + def propname(self): + raise NotImplementedError() + + + class Enable(PropertySetter): + propname = "Enabled" + value_to_set = True + + + class Disable(PropertySetter): + propname = "Enabled" + value_to_set = False + + + class BumpTimeout(PropertySetter): + propname = "LastCheckedOK" + value_to_set = "" + + + class StartChecker(PropertySetter): + propname = "CheckerRunning" + value_to_set = True + + + class StopChecker(PropertySetter): + propname = "CheckerRunning" + value_to_set = False + + + class ApproveByDefault(PropertySetter): + propname = "ApprovedByDefault" + value_to_set = True + + + class DenyByDefault(PropertySetter): + propname = "ApprovedByDefault" + value_to_set = False + + + class PropertySetterValue(PropertySetter): + """Abstract class for PropertySetter recieving a value as +constructor argument instead of a class attribute.""" + def __init__(self, value): + self.value_to_set = value + + @classmethod + def argparse(cls, argtype): + def cmdtype(arg): + return cls(argtype(arg)) + return cmdtype + + class SetChecker(PropertySetterValue): + propname = "Checker" + + + class SetHost(PropertySetterValue): + propname = "Host" + + + class SetSecret(PropertySetterValue): + propname = "Secret" + + @property + def value_to_set(self): + return self._vts + + @value_to_set.setter + def value_to_set(self, value): + """When setting, read data from supplied file object""" + self._vts = value.read() + value.close() + + + class PropertySetterValueMilliseconds(PropertySetterValue): + """Abstract class for PropertySetterValue taking a value +argument as a datetime.timedelta() but should store it as +milliseconds.""" + + @property + def value_to_set(self): + return self._vts + + @value_to_set.setter + def value_to_set(self, value): + "When setting, convert value from a datetime.timedelta" + self._vts = int(round(value.total_seconds() * 1000)) + + + class SetTimeout(PropertySetterValueMilliseconds): + propname = "Timeout" + + + class SetExtendedTimeout(PropertySetterValueMilliseconds): + propname = "ExtendedTimeout" + + + class SetInterval(PropertySetterValueMilliseconds): + propname = "Interval" + + + class SetApprovalDelay(PropertySetterValueMilliseconds): + propname = "ApprovalDelay" + + + class SetApprovalDuration(PropertySetterValueMilliseconds): + propname = "ApprovalDuration" + + + +class TestCaseWithAssertLogs(unittest.TestCase): + """unittest.TestCase.assertLogs only exists in Python 3.4""" + + if not hasattr(unittest.TestCase, "assertLogs"): + @contextlib.contextmanager + def assertLogs(self, logger, level=logging.INFO): + capturing_handler = self.CapturingLevelHandler(level) + old_level = logger.level + old_propagate = logger.propagate + logger.addHandler(capturing_handler) + logger.setLevel(level) + logger.propagate = False + try: + yield capturing_handler.watcher + finally: + logger.propagate = old_propagate + logger.removeHandler(capturing_handler) + logger.setLevel(old_level) + self.assertGreater(len(capturing_handler.watcher.records), + 0) + + class CapturingLevelHandler(logging.Handler): + def __init__(self, level, *args, **kwargs): + logging.Handler.__init__(self, *args, **kwargs) + self.watcher = self.LoggingWatcher([], []) + def emit(self, record): + self.watcher.records.append(record) + self.watcher.output.append(self.format(record)) + + LoggingWatcher = collections.namedtuple("LoggingWatcher", + ("records", + "output")) + + +class Unique: + """Class for objects which exist only to be unique objects, since +unittest.mock.sentinel only exists in Python 3.3""" + + +class Test_string_to_delta(TestCaseWithAssertLogs): + # Just test basic RFC 3339 functionality here, the doc string for + # rfc3339_duration_to_delta() already has more comprehensive + # tests, which are run by doctest. + + def test_rfc3339_zero_seconds(self): + self.assertEqual(datetime.timedelta(), + string_to_delta("PT0S")) + + def test_rfc3339_zero_days(self): + self.assertEqual(datetime.timedelta(), string_to_delta("P0D")) + + def test_rfc3339_one_second(self): + self.assertEqual(datetime.timedelta(0, 1), + string_to_delta("PT1S")) + + def test_rfc3339_two_hours(self): + self.assertEqual(datetime.timedelta(0, 7200), + string_to_delta("PT2H")) + + def test_falls_back_to_pre_1_6_1_with_warning(self): + with self.assertLogs(log, logging.WARNING): + value = string_to_delta("2h") + self.assertEqual(datetime.timedelta(0, 7200), value) + + +class Test_check_option_syntax(unittest.TestCase): + def setUp(self): + self.parser = argparse.ArgumentParser() + add_command_line_options(self.parser) + + def test_actions_requires_client_or_all(self): + for action, value in self.actions.items(): + args = self.actionargs(action, value) + with self.assertParseError(): + self.parse_args(args) + + # This mostly corresponds to the definition from has_commands() in + # check_option_syntax() + actions = { + "--enable": None, + "--disable": None, + "--bump-timeout": None, + "--start-checker": None, + "--stop-checker": None, + "--is-enabled": None, + "--remove": None, + "--checker": "x", + "--timeout": "PT0S", + "--extended-timeout": "PT0S", + "--interval": "PT0S", + "--approve-by-default": None, + "--deny-by-default": None, + "--approval-delay": "PT0S", + "--approval-duration": "PT0S", + "--host": "hostname", + "--secret": "/dev/null", + "--approve": None, + "--deny": None, + } + + @staticmethod + def actionargs(action, value, *args): + if value is not None: + return [action, value] + list(args) + else: + return [action] + list(args) + + @contextlib.contextmanager + def assertParseError(self): + with self.assertRaises(SystemExit) as e: + with self.redirect_stderr_to_devnull(): + yield + # Exit code from argparse is guaranteed to be "2". Reference: + # https://docs.python.org/3/library + # /argparse.html#exiting-methods + self.assertEqual(2, e.exception.code) + + def parse_args(self, args): + options = self.parser.parse_args(args) + check_option_syntax(self.parser, options) + + @staticmethod + @contextlib.contextmanager + def redirect_stderr_to_devnull(): + old_stderr = sys.stderr + with contextlib.closing(open(os.devnull, "w")) as null: + sys.stderr = null + try: + yield + finally: + sys.stderr = old_stderr + + def check_option_syntax(self, options): + check_option_syntax(self.parser, options) + + def test_actions_all_conflicts_with_verbose(self): + for action, value in self.actions.items(): + args = self.actionargs(action, value, "--all", + "--verbose") + with self.assertParseError(): + self.parse_args(args) + + def test_actions_with_client_conflicts_with_verbose(self): + for action, value in self.actions.items(): + args = self.actionargs(action, value, "--verbose", + "client") + with self.assertParseError(): + self.parse_args(args) + + def test_dump_json_conflicts_with_verbose(self): + args = ["--dump-json", "--verbose"] + with self.assertParseError(): + self.parse_args(args) + + def test_dump_json_conflicts_with_action(self): + for action, value in self.actions.items(): + args = self.actionargs(action, value, "--dump-json") + with self.assertParseError(): + self.parse_args(args) + + def test_all_can_not_be_alone(self): + args = ["--all"] + with self.assertParseError(): + self.parse_args(args) + + def test_all_is_ok_with_any_action(self): + for action, value in self.actions.items(): + args = self.actionargs(action, value, "--all") + self.parse_args(args) + + def test_any_action_is_ok_with_one_client(self): + for action, value in self.actions.items(): + args = self.actionargs(action, value, "client") + self.parse_args(args) + + def test_one_client_with_all_actions_except_is_enabled(self): + for action, value in self.actions.items(): + if action == "--is-enabled": + continue + args = self.actionargs(action, value, "client") + self.parse_args(args) + + def test_two_clients_with_all_actions_except_is_enabled(self): + for action, value in self.actions.items(): + if action == "--is-enabled": + continue + args = self.actionargs(action, value, "client1", + "client2") + self.parse_args(args) + + def test_two_clients_are_ok_with_actions_except_is_enabled(self): + for action, value in self.actions.items(): + if action == "--is-enabled": + continue + args = self.actionargs(action, value, "client1", + "client2") + self.parse_args(args) + + def test_is_enabled_fails_without_client(self): + args = ["--is-enabled"] + with self.assertParseError(): + self.parse_args(args) + + def test_is_enabled_fails_with_two_clients(self): + args = ["--is-enabled", "client1", "client2"] + with self.assertParseError(): + self.parse_args(args) + + def test_remove_can_only_be_combined_with_action_deny(self): + for action, value in self.actions.items(): + if action in {"--remove", "--deny"}: + continue + args = self.actionargs(action, value, "--all", + "--remove") + with self.assertParseError(): + self.parse_args(args) + + +class Test_dbus_exceptions(unittest.TestCase): + + def test_dbus_ConnectFailed_is_Error(self): + with self.assertRaises(dbus.Error): + raise dbus.ConnectFailed() + + +class Test_dbus_MandosBus(unittest.TestCase): + + class MockMandosBus(dbus.MandosBus): + def __init__(self): + self._name = "se.recompile.Mandos" + self._server_path = "/" + self._server_interface = "se.recompile.Mandos" + self._client_interface = "se.recompile.Mandos.Client" + self.calls = [] + self.call_method_return = Unique() + + def call_method(self, methodname, busname, objectpath, + interface, *args): + self.calls.append((methodname, busname, objectpath, + interface, args)) + return self.call_method_return + + def setUp(self): + self.bus = self.MockMandosBus() + + def test_set_client_property(self): + self.bus.set_client_property("objectpath", "key", "value") + expected_call = ("Set", self.bus._name, "objectpath", + "org.freedesktop.DBus.Properties", + (self.bus._client_interface, "key", "value")) + self.assertIn(expected_call, self.bus.calls) + + def test_call_client_method(self): + ret = self.bus.call_client_method("objectpath", "methodname") + self.assertIs(self.bus.call_method_return, ret) + expected_call = ("methodname", self.bus._name, "objectpath", + self.bus._client_interface, ()) + self.assertIn(expected_call, self.bus.calls) + + def test_call_client_method_with_args(self): + args = (Unique(), Unique()) + ret = self.bus.call_client_method("objectpath", "methodname", + *args) + self.assertIs(self.bus.call_method_return, ret) + expected_call = ("methodname", self.bus._name, "objectpath", + self.bus._client_interface, + (args[0], args[1])) + self.assertIn(expected_call, self.bus.calls) + + def test_get_clients_and_properties(self): + managed_objects = { + "objectpath": { + self.bus._client_interface: { + "key": "value", + "bool": True, + }, + "irrelevant_interface": { + "key": "othervalue", + "bool": False, + }, + }, + "other_objectpath": { + "other_irrelevant_interface": { + "key": "value 3", + "bool": None, + }, + }, + } + expected_clients_and_properties = { + "objectpath": { + "key": "value", + "bool": True, + } + } + self.bus.call_method_return = managed_objects + ret = self.bus.get_clients_and_properties() + self.assertDictEqual(expected_clients_and_properties, ret) + expected_call = ("GetManagedObjects", self.bus._name, + self.bus._server_path, + "org.freedesktop.DBus.ObjectManager", ()) + self.assertIn(expected_call, self.bus.calls) + + def test_call_server_method(self): + ret = self.bus.call_server_method("methodname") + self.assertIs(self.bus.call_method_return, ret) + expected_call = ("methodname", self.bus._name, + self.bus._server_path, + self.bus._server_interface, ()) + self.assertIn(expected_call, self.bus.calls) + + def test_call_server_method_with_args(self): + args = (Unique(), Unique()) + ret = self.bus.call_server_method("methodname", *args) + self.assertIs(self.bus.call_method_return, ret) + expected_call = ("methodname", self.bus._name, + self.bus._server_path, + self.bus._server_interface, + (args[0], args[1])) + self.assertIn(expected_call, self.bus.calls) + + +class Test_dbus_python_adapter_SystemBus(TestCaseWithAssertLogs): + + def MockDBusPython_func(self, func): + class mock_dbus_python: + """mock dbus-python module""" + class exceptions: + """Pseudo-namespace""" + class DBusException(Exception): + pass + class SystemBus: + @staticmethod + def get_object(busname, objectpath): + DBusObject = collections.namedtuple( + "DBusObject", ("methodname", "Set")) + def method(*args, **kwargs): + self.assertEqual({"dbus_interface": + "interface"}, + kwargs) + return func(*args) + def set_property(interface, key, value, + dbus_interface=None): + self.assertEqual( + "org.freedesktop.DBus.Properties", + dbus_interface) + self.assertEqual("Secret", key) + return func(interface, key, value, + dbus_interface=dbus_interface) + return DBusObject(methodname=method, + Set=set_property) + class Boolean: + def __init__(self, value): + self.value = bool(value) + def __bool__(self): + return self.value + if sys.version_info.major == 2: + __nonzero__ = __bool__ + class ObjectPath(str): + pass + class Dictionary(dict): + pass + class ByteArray(bytes): + pass + return mock_dbus_python + + def call_method(self, bus, methodname, busname, objectpath, + interface, *args): + with self.assertLogs(log, logging.DEBUG): + return bus.call_method(methodname, busname, objectpath, + interface, *args) + + def test_call_method_returns(self): + expected_method_return = Unique() + method_args = (Unique(), Unique()) + def func(*args): + self.assertEqual(len(method_args), len(args)) + for marg, arg in zip(method_args, args): + self.assertIs(marg, arg) + return expected_method_return + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + ret = self.call_method(bus, "methodname", "busname", + "objectpath", "interface", + *method_args) + self.assertIs(ret, expected_method_return) + + def test_call_method_filters_bool_true(self): + def func(): + return method_return + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + method_return = mock_dbus_python.Boolean(True) + ret = self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + self.assertTrue(ret) + self.assertNotIsInstance(ret, mock_dbus_python.Boolean) + + def test_call_method_filters_bool_false(self): + def func(): + return method_return + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + method_return = mock_dbus_python.Boolean(False) + ret = self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + self.assertFalse(ret) + self.assertNotIsInstance(ret, mock_dbus_python.Boolean) + + def test_call_method_filters_objectpath(self): + def func(): + return method_return + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + method_return = mock_dbus_python.ObjectPath("objectpath") + ret = self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + self.assertEqual("objectpath", ret) + self.assertIsNot("objectpath", ret) + self.assertNotIsInstance(ret, mock_dbus_python.ObjectPath) + + def test_call_method_filters_booleans_in_dict(self): + def func(): + return method_return + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + method_return = mock_dbus_python.Dictionary( + {mock_dbus_python.Boolean(True): + mock_dbus_python.Boolean(False), + mock_dbus_python.Boolean(False): + mock_dbus_python.Boolean(True)}) + ret = self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + expected_method_return = {True: False, + False: True} + self.assertEqual(expected_method_return, ret) + self.assertNotIsInstance(ret, mock_dbus_python.Dictionary) + + def test_call_method_filters_objectpaths_in_dict(self): + def func(): + return method_return + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + method_return = mock_dbus_python.Dictionary( + {mock_dbus_python.ObjectPath("objectpath_key_1"): + mock_dbus_python.ObjectPath("objectpath_value_1"), + mock_dbus_python.ObjectPath("objectpath_key_2"): + mock_dbus_python.ObjectPath("objectpath_value_2")}) + ret = self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + expected_method_return = {str(key): str(value) + for key, value in + method_return.items()} + self.assertEqual(expected_method_return, ret) + self.assertIsInstance(ret, dict) + self.assertNotIsInstance(ret, mock_dbus_python.Dictionary) + + def test_call_method_filters_dict_in_dict(self): + def func(): + return method_return + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + method_return = mock_dbus_python.Dictionary( + {"key1": mock_dbus_python.Dictionary({"key11": "value11", + "key12": "value12"}), + "key2": mock_dbus_python.Dictionary({"key21": "value21", + "key22": "value22"})}) + ret = self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + expected_method_return = { + "key1": {"key11": "value11", + "key12": "value12"}, + "key2": {"key21": "value21", + "key22": "value22"}, + } + self.assertEqual(expected_method_return, ret) + self.assertIsInstance(ret, dict) + self.assertNotIsInstance(ret, mock_dbus_python.Dictionary) + for key, value in ret.items(): + self.assertIsInstance(value, dict) + self.assertEqual(expected_method_return[key], value) + self.assertNotIsInstance(value, + mock_dbus_python.Dictionary) + + def test_call_method_filters_dict_three_deep(self): + def func(): + return method_return + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + method_return = mock_dbus_python.Dictionary( + {"key1": + mock_dbus_python.Dictionary( + {"key2": + mock_dbus_python.Dictionary( + {"key3": + mock_dbus_python.Boolean(True), + }), + }), + }) + ret = self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + expected_method_return = {"key1": {"key2": {"key3": True}}} + self.assertEqual(expected_method_return, ret) + self.assertIsInstance(ret, dict) + self.assertNotIsInstance(ret, mock_dbus_python.Dictionary) + self.assertIsInstance(ret["key1"], dict) + self.assertNotIsInstance(ret["key1"], + mock_dbus_python.Dictionary) + self.assertIsInstance(ret["key1"]["key2"], dict) + self.assertNotIsInstance(ret["key1"]["key2"], + mock_dbus_python.Dictionary) + self.assertTrue(ret["key1"]["key2"]["key3"]) + self.assertNotIsInstance(ret["key1"]["key2"]["key3"], + mock_dbus_python.Boolean) + + def test_call_method_handles_exception(self): + dbus_logger = logging.getLogger("dbus.proxies") + + def func(): + dbus_logger.error("Test") + raise mock_dbus_python.exceptions.DBusException() + + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + + class CountingHandler(logging.Handler): + count = 0 + def emit(self, record): + self.count += 1 + + counting_handler = CountingHandler() + + dbus_logger.addHandler(counting_handler) + + try: + with self.assertRaises(dbus.Error) as e: + self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + finally: + dbus_logger.removeFilter(counting_handler) + + self.assertNotIsInstance(e, dbus.ConnectFailed) + + # Make sure the dbus logger was suppressed + self.assertEqual(0, counting_handler.count) + + def test_Set_Secret_sends_bytearray(self): + ret = [None] + def func(*args, **kwargs): + ret[0] = (args, kwargs) + mock_dbus_python = self.MockDBusPython_func(func) + bus = dbus_python_adapter.SystemBus(mock_dbus_python) + bus.set_client_property("objectpath", "Secret", "value") + expected_call = (("se.recompile.Mandos.Client", "Secret", + mock_dbus_python.ByteArray(b"value")), + {"dbus_interface": + "org.freedesktop.DBus.Properties"}) + self.assertEqual(expected_call, ret[0]) + if sys.version_info.major == 2: + self.assertIsInstance(ret[0][0][-1], + mock_dbus_python.ByteArray) + + def test_get_object_converts_to_correct_exception(self): + bus = dbus_python_adapter.SystemBus( + self.fake_dbus_python_raises_exception_on_connect) + with self.assertRaises(dbus.ConnectFailed): + self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + + class fake_dbus_python_raises_exception_on_connect: + """fake dbus-python module""" + class exceptions: + """Pseudo-namespace""" + class DBusException(Exception): + pass + + @classmethod + def SystemBus(cls): + def get_object(busname, objectpath): + raise cls.exceptions.DBusException() + Bus = collections.namedtuple("Bus", ["get_object"]) + return Bus(get_object=get_object) + + +class Test_dbus_python_adapter_CachingBus(unittest.TestCase): + class mock_dbus_python: + """mock dbus-python modules""" + class SystemBus: + @staticmethod + def get_object(busname, objectpath): + return Unique() + + def setUp(self): + self.bus = dbus_python_adapter.CachingBus( + self.mock_dbus_python) + + def test_returns_distinct_objectpaths(self): + obj1 = self.bus.get_object("busname", "objectpath1") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get_object("busname", "objectpath2") + self.assertIsInstance(obj2, Unique) + self.assertIsNot(obj1, obj2) + + def test_returns_distinct_busnames(self): + obj1 = self.bus.get_object("busname1", "objectpath") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get_object("busname2", "objectpath") + self.assertIsInstance(obj2, Unique) + self.assertIsNot(obj1, obj2) + + def test_returns_distinct_both(self): + obj1 = self.bus.get_object("busname1", "objectpath") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get_object("busname2", "objectpath") + self.assertIsInstance(obj2, Unique) + self.assertIsNot(obj1, obj2) + + def test_returns_same(self): + obj1 = self.bus.get_object("busname", "objectpath") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get_object("busname", "objectpath") + self.assertIsInstance(obj2, Unique) + self.assertIs(obj1, obj2) + + def test_returns_same_old(self): + obj1 = self.bus.get_object("busname1", "objectpath1") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get_object("busname2", "objectpath2") + self.assertIsInstance(obj2, Unique) + obj1b = self.bus.get_object("busname1", "objectpath1") + self.assertIsInstance(obj1b, Unique) + self.assertIsNot(obj1, obj2) + self.assertIsNot(obj2, obj1b) + self.assertIs(obj1, obj1b) + + +class Test_pydbus_adapter_SystemBus(TestCaseWithAssertLogs): + + def Stub_pydbus_func(self, func): + class stub_pydbus: + """stub pydbus module""" + class SystemBus: + @staticmethod + def get(busname, objectpath): + DBusObject = collections.namedtuple( + "DBusObject", ("methodname",)) + return {"interface": + DBusObject(methodname=func)} + return stub_pydbus + + def call_method(self, bus, methodname, busname, objectpath, + interface, *args): + with self.assertLogs(log, logging.DEBUG): + return bus.call_method(methodname, busname, objectpath, + interface, *args) + + def test_call_method_returns(self): + expected_method_return = Unique() + method_args = (Unique(), Unique()) + def func(*args): + self.assertEqual(len(method_args), len(args)) + for marg, arg in zip(method_args, args): + self.assertIs(marg, arg) + return expected_method_return + stub_pydbus = self.Stub_pydbus_func(func) + bus = pydbus_adapter.SystemBus(stub_pydbus) + ret = self.call_method(bus, "methodname", "busname", + "objectpath", "interface", + *method_args) + self.assertIs(ret, expected_method_return) + + def test_call_method_handles_exception(self): + dbus_logger = logging.getLogger("dbus.proxies") + + def func(): + raise gi.repository.GLib.Error() + + stub_pydbus = self.Stub_pydbus_func(func) + bus = pydbus_adapter.SystemBus(stub_pydbus) + + with self.assertRaises(dbus.Error) as e: + self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + + self.assertNotIsInstance(e, dbus.ConnectFailed) + + def test_get_converts_to_correct_exception(self): + bus = pydbus_adapter.SystemBus( + self.fake_pydbus_raises_exception_on_connect) + with self.assertRaises(dbus.ConnectFailed): + self.call_method(bus, "methodname", "busname", + "objectpath", "interface") + + class fake_pydbus_raises_exception_on_connect: + """fake dbus-python module""" + @classmethod + def SystemBus(cls): + def get(busname, objectpath): + raise gi.repository.GLib.Error() + Bus = collections.namedtuple("Bus", ["get"]) + return Bus(get=get) + + def test_set_property_uses_setattr(self): + class Object: + pass + obj = Object() + class pydbus_spy: + class SystemBus: + @staticmethod + def get(busname, objectpath): + return {"interface": obj} + bus = pydbus_adapter.SystemBus(pydbus_spy) + value = Unique() + bus.set_property("busname", "objectpath", "interface", "key", + value) + self.assertIs(value, obj.key) + + def test_get_suppresses_xml_deprecation_warning(self): + if sys.version_info.major >= 3: + return + class stub_pydbus_get: + class SystemBus: + @staticmethod + def get(busname, objectpath): + warnings.warn_explicit( + "deprecated", DeprecationWarning, + "xml.etree.ElementTree", 0) + bus = pydbus_adapter.SystemBus(stub_pydbus_get) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + bus.get("busname", "objectpath") + self.assertEqual(0, len(w)) + + +class Test_pydbus_adapter_CachingBus(unittest.TestCase): + class stub_pydbus: + """stub pydbus module""" + class SystemBus: + @staticmethod + def get(busname, objectpath): + return Unique() + + def setUp(self): + self.bus = pydbus_adapter.CachingBus(self.stub_pydbus) + + def test_returns_distinct_objectpaths(self): + obj1 = self.bus.get("busname", "objectpath1") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get("busname", "objectpath2") + self.assertIsInstance(obj2, Unique) + self.assertIsNot(obj1, obj2) + + def test_returns_distinct_busnames(self): + obj1 = self.bus.get("busname1", "objectpath") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get("busname2", "objectpath") + self.assertIsInstance(obj2, Unique) + self.assertIsNot(obj1, obj2) + + def test_returns_distinct_both(self): + obj1 = self.bus.get("busname1", "objectpath") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get("busname2", "objectpath") + self.assertIsInstance(obj2, Unique) + self.assertIsNot(obj1, obj2) + + def test_returns_same(self): + obj1 = self.bus.get("busname", "objectpath") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get("busname", "objectpath") + self.assertIsInstance(obj2, Unique) + self.assertIs(obj1, obj2) + + def test_returns_same_old(self): + obj1 = self.bus.get("busname1", "objectpath1") + self.assertIsInstance(obj1, Unique) + obj2 = self.bus.get("busname2", "objectpath2") + self.assertIsInstance(obj2, Unique) + obj1b = self.bus.get("busname1", "objectpath1") + self.assertIsInstance(obj1b, Unique) + self.assertIsNot(obj1, obj2) + self.assertIsNot(obj2, obj1b) + self.assertIs(obj1, obj1b) + + +class Test_commands_from_options(unittest.TestCase): + + def setUp(self): + self.parser = argparse.ArgumentParser() + add_command_line_options(self.parser) + + def test_is_enabled(self): + self.assert_command_from_args(["--is-enabled", "client"], + command.IsEnabled) + + def assert_command_from_args(self, args, command_cls, length=1, + clients=None, **cmd_attrs): + """Assert that parsing ARGS should result in an instance of +COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS).""" + options = self.parser.parse_args(args) + check_option_syntax(self.parser, options) + commands = commands_from_options(options) + self.assertEqual(length, len(commands)) + for command in commands: + if isinstance(command, command_cls): + break + else: + self.assertIsInstance(command, command_cls) + if clients is not None: + self.assertEqual(clients, options.client) + for key, value in cmd_attrs.items(): + self.assertEqual(value, getattr(command, key)) + + def assert_commands_from_args(self, args, commands, clients=None): + for cmd in commands: + self.assert_command_from_args(args, cmd, + length=len(commands), + clients=clients) + + def test_is_enabled_short(self): + self.assert_command_from_args(["-V", "client"], + command.IsEnabled) + + def test_approve(self): + self.assert_command_from_args(["--approve", "client"], + command.Approve) + + def test_approve_short(self): + self.assert_command_from_args(["-A", "client"], + command.Approve) + + def test_deny(self): + self.assert_command_from_args(["--deny", "client"], + command.Deny) + + def test_deny_short(self): + self.assert_command_from_args(["-D", "client"], command.Deny) + + def test_remove(self): + self.assert_command_from_args(["--remove", "client"], + command.Remove) + + def test_deny_before_remove(self): + options = self.parser.parse_args(["--deny", "--remove", + "client"]) + check_option_syntax(self.parser, options) + commands = commands_from_options(options) + self.assertEqual(2, len(commands)) + self.assertIsInstance(commands[0], command.Deny) + self.assertIsInstance(commands[1], command.Remove) + + def test_deny_before_remove_reversed(self): + options = self.parser.parse_args(["--remove", "--deny", + "--all"]) + check_option_syntax(self.parser, options) + commands = commands_from_options(options) + self.assertEqual(2, len(commands)) + self.assertIsInstance(commands[0], command.Deny) + self.assertIsInstance(commands[1], command.Remove) + + def test_remove_short(self): + self.assert_command_from_args(["-r", "client"], + command.Remove) + + def test_dump_json(self): + self.assert_command_from_args(["--dump-json"], + command.DumpJSON) + + def test_enable(self): + self.assert_command_from_args(["--enable", "client"], + command.Enable) + + def test_enable_short(self): + self.assert_command_from_args(["-e", "client"], + command.Enable) + + def test_disable(self): + self.assert_command_from_args(["--disable", "client"], + command.Disable) + + def test_disable_short(self): + self.assert_command_from_args(["-d", "client"], + command.Disable) + + def test_bump_timeout(self): + self.assert_command_from_args(["--bump-timeout", "client"], + command.BumpTimeout) + + def test_bump_timeout_short(self): + self.assert_command_from_args(["-b", "client"], + command.BumpTimeout) + + def test_start_checker(self): + self.assert_command_from_args(["--start-checker", "client"], + command.StartChecker) + + def test_stop_checker(self): + self.assert_command_from_args(["--stop-checker", "client"], + command.StopChecker) + + def test_approve_by_default(self): + self.assert_command_from_args(["--approve-by-default", + "client"], + command.ApproveByDefault) + + def test_deny_by_default(self): + self.assert_command_from_args(["--deny-by-default", "client"], + command.DenyByDefault) + + def test_checker(self): + self.assert_command_from_args(["--checker", ":", "client"], + command.SetChecker, + value_to_set=":") + + def test_checker_empty(self): + self.assert_command_from_args(["--checker", "", "client"], + command.SetChecker, + value_to_set="") + + def test_checker_short(self): + self.assert_command_from_args(["-c", ":", "client"], + command.SetChecker, + value_to_set=":") + + def test_host(self): + self.assert_command_from_args( + ["--host", "client.example.org", "client"], + command.SetHost, value_to_set="client.example.org") + + def test_host_short(self): + self.assert_command_from_args( + ["-H", "client.example.org", "client"], command.SetHost, + value_to_set="client.example.org") + + def test_secret_devnull(self): + self.assert_command_from_args(["--secret", os.path.devnull, + "client"], command.SetSecret, + value_to_set=b"") + + def test_secret_tempfile(self): + with tempfile.NamedTemporaryFile(mode="r+b") as f: + value = b"secret\0xyzzy\nbar" + f.write(value) + f.seek(0) + self.assert_command_from_args(["--secret", f.name, + "client"], + command.SetSecret, + value_to_set=value) + + def test_secret_devnull_short(self): + self.assert_command_from_args(["-s", os.path.devnull, + "client"], command.SetSecret, + value_to_set=b"") + + def test_secret_tempfile_short(self): + with tempfile.NamedTemporaryFile(mode="r+b") as f: + value = b"secret\0xyzzy\nbar" + f.write(value) + f.seek(0) + self.assert_command_from_args(["-s", f.name, "client"], + command.SetSecret, + value_to_set=value) + + def test_timeout(self): + self.assert_command_from_args(["--timeout", "PT5M", "client"], + command.SetTimeout, + value_to_set=300000) + + def test_timeout_short(self): + self.assert_command_from_args(["-t", "PT5M", "client"], + command.SetTimeout, + value_to_set=300000) + + def test_extended_timeout(self): + self.assert_command_from_args(["--extended-timeout", "PT15M", + "client"], + command.SetExtendedTimeout, + value_to_set=900000) + + def test_interval(self): + self.assert_command_from_args(["--interval", "PT2M", + "client"], command.SetInterval, + value_to_set=120000) + + def test_interval_short(self): + self.assert_command_from_args(["-i", "PT2M", "client"], + command.SetInterval, + value_to_set=120000) + + def test_approval_delay(self): + self.assert_command_from_args(["--approval-delay", "PT30S", + "client"], + command.SetApprovalDelay, + value_to_set=30000) + + def test_approval_duration(self): + self.assert_command_from_args(["--approval-duration", "PT1S", + "client"], + command.SetApprovalDuration, + value_to_set=1000) + + def test_print_table(self): + self.assert_command_from_args([], command.PrintTable, + verbose=False) + + def test_print_table_verbose(self): + self.assert_command_from_args(["--verbose"], + command.PrintTable, + verbose=True) + + def test_print_table_verbose_short(self): + self.assert_command_from_args(["-v"], command.PrintTable, + verbose=True) + + + def test_manual_page_example_1(self): + self.assert_command_from_args("", + command.PrintTable, + clients=[], + verbose=False) + + def test_manual_page_example_2(self): + self.assert_command_from_args( + "--verbose foo1.example.org foo2.example.org".split(), + command.PrintTable, clients=["foo1.example.org", + "foo2.example.org"], + verbose=True) + + def test_manual_page_example_3(self): + self.assert_command_from_args("--enable --all".split(), + command.Enable, + clients=[]) + + def test_manual_page_example_4(self): + self.assert_commands_from_args( + ("--timeout=PT5M --interval=PT1M foo1.example.org" + " foo2.example.org").split(), + [command.SetTimeout, command.SetInterval], + clients=["foo1.example.org", "foo2.example.org"]) + + def test_manual_page_example_5(self): + self.assert_command_from_args("--approve --all".split(), + command.Approve, + clients=[]) + + +class TestCommand(unittest.TestCase): + """Abstract class for tests of command classes""" + + class FakeMandosBus(dbus.MandosBus): + def __init__(self, testcase): + self.client_properties = { + "Name": "foo", + "KeyID": ("92ed150794387c03ce684574b1139a65" + "94a34f895daaaf09fd8ea90a27cddb12"), + "Secret": b"secret", + "Host": "foo.example.org", + "Enabled": True, + "Timeout": 300000, + "LastCheckedOK": "2019-02-03T00:00:00", + "Created": "2019-01-02T00:00:00", + "Interval": 120000, + "Fingerprint": ("778827225BA7DE539C5A" + "7CFA59CFF7CDBD9A5920"), + "CheckerRunning": False, + "LastEnabled": "2019-01-03T00:00:00", + "ApprovalPending": False, + "ApprovedByDefault": True, + "LastApprovalRequest": "", + "ApprovalDelay": 0, + "ApprovalDuration": 1000, + "Checker": "fping -q -- %(host)s", + "ExtendedTimeout": 900000, + "Expires": "2019-02-04T00:00:00", + "LastCheckerStatus": 0, + } + self.other_client_properties = { + "Name": "barbar", + "KeyID": ("0558568eedd67d622f5c83b35a115f79" + "6ab612cff5ad227247e46c2b020f441c"), + "Secret": b"secretbar", + "Host": "192.0.2.3", + "Enabled": True, + "Timeout": 300000, + "LastCheckedOK": "2019-02-04T00:00:00", + "Created": "2019-01-03T00:00:00", + "Interval": 120000, + "Fingerprint": ("3E393AEAEFB84C7E89E2" + "F547B3A107558FCA3A27"), + "CheckerRunning": True, + "LastEnabled": "2019-01-04T00:00:00", + "ApprovalPending": False, + "ApprovedByDefault": False, + "LastApprovalRequest": "2019-01-03T00:00:00", + "ApprovalDelay": 30000, + "ApprovalDuration": 93785000, + "Checker": ":", + "ExtendedTimeout": 900000, + "Expires": "2019-02-05T00:00:00", + "LastCheckerStatus": -2, + } + self.clients = collections.OrderedDict( + [ + ("client_objectpath", self.client_properties), + ("other_client_objectpath", + self.other_client_properties), + ]) + self.one_client = {"client_objectpath": + self.client_properties} + self.testcase = testcase + self.calls = [] + + def call_method(self, methodname, busname, objectpath, + interface, *args): + self.testcase.assertEqual("se.recompile.Mandos", busname) + self.calls.append((methodname, busname, objectpath, + interface, args)) + if interface == "org.freedesktop.DBus.Properties": + if methodname == "Set": + self.testcase.assertEqual(3, len(args)) + interface, key, value = args + self.testcase.assertEqual( + "se.recompile.Mandos.Client", interface) + self.clients[objectpath][key] = value + return + elif interface == "se.recompile.Mandos": + self.testcase.assertEqual("RemoveClient", methodname) + self.testcase.assertEqual(1, len(args)) + clientpath = args[0] + del self.clients[clientpath] + return + elif interface == "se.recompile.Mandos.Client": + if methodname == "Approve": + self.testcase.assertEqual(1, len(args)) + return + raise ValueError() + + def setUp(self): + self.bus = self.FakeMandosBus(self) + + +class TestBaseCommands(TestCommand): + + def test_IsEnabled_exits_successfully(self): + with self.assertRaises(SystemExit) as e: + command.IsEnabled().run(self.bus.one_client) + if e.exception.code is not None: + self.assertEqual(0, e.exception.code) + else: + self.assertIsNone(e.exception.code) + + def test_IsEnabled_exits_with_failure(self): + self.bus.client_properties["Enabled"] = False + with self.assertRaises(SystemExit) as e: + command.IsEnabled().run(self.bus.one_client) + if isinstance(e.exception.code, int): + self.assertNotEqual(0, e.exception.code) + else: + self.assertIsNotNone(e.exception.code) + + def test_Approve(self): + busname = "se.recompile.Mandos" + client_interface = "se.recompile.Mandos.Client" + command.Approve().run(self.bus.clients, self.bus) + for clientpath in self.bus.clients: + self.assertIn(("Approve", busname, clientpath, + client_interface, (True,)), self.bus.calls) + + def test_Deny(self): + busname = "se.recompile.Mandos" + client_interface = "se.recompile.Mandos.Client" + command.Deny().run(self.bus.clients, self.bus) + for clientpath in self.bus.clients: + self.assertIn(("Approve", busname, clientpath, + client_interface, (False,)), + self.bus.calls) + + def test_Remove(self): + command.Remove().run(self.bus.clients, self.bus) + for clientpath in self.bus.clients: + self.assertIn(("RemoveClient", dbus_busname, + dbus_server_path, dbus_server_interface, + (clientpath,)), self.bus.calls) + + expected_json = { + "foo": { + "Name": "foo", + "KeyID": ("92ed150794387c03ce684574b1139a65" + "94a34f895daaaf09fd8ea90a27cddb12"), + "Host": "foo.example.org", + "Enabled": True, + "Timeout": 300000, + "LastCheckedOK": "2019-02-03T00:00:00", + "Created": "2019-01-02T00:00:00", + "Interval": 120000, + "Fingerprint": ("778827225BA7DE539C5A" + "7CFA59CFF7CDBD9A5920"), + "CheckerRunning": False, + "LastEnabled": "2019-01-03T00:00:00", + "ApprovalPending": False, + "ApprovedByDefault": True, + "LastApprovalRequest": "", + "ApprovalDelay": 0, + "ApprovalDuration": 1000, + "Checker": "fping -q -- %(host)s", + "ExtendedTimeout": 900000, + "Expires": "2019-02-04T00:00:00", + "LastCheckerStatus": 0, + }, + "barbar": { + "Name": "barbar", + "KeyID": ("0558568eedd67d622f5c83b35a115f79" + "6ab612cff5ad227247e46c2b020f441c"), + "Host": "192.0.2.3", + "Enabled": True, + "Timeout": 300000, + "LastCheckedOK": "2019-02-04T00:00:00", + "Created": "2019-01-03T00:00:00", + "Interval": 120000, + "Fingerprint": ("3E393AEAEFB84C7E89E2" + "F547B3A107558FCA3A27"), + "CheckerRunning": True, + "LastEnabled": "2019-01-04T00:00:00", + "ApprovalPending": False, + "ApprovedByDefault": False, + "LastApprovalRequest": "2019-01-03T00:00:00", + "ApprovalDelay": 30000, + "ApprovalDuration": 93785000, + "Checker": ":", + "ExtendedTimeout": 900000, + "Expires": "2019-02-05T00:00:00", + "LastCheckerStatus": -2, + }, + } + + def test_DumpJSON_normal(self): + with self.capture_stdout_to_buffer() as buffer: + command.DumpJSON().run(self.bus.clients) + json_data = json.loads(buffer.getvalue()) + self.assertDictEqual(self.expected_json, json_data) + + @staticmethod + @contextlib.contextmanager + def capture_stdout_to_buffer(): + capture_buffer = io.StringIO() + old_stdout = sys.stdout + sys.stdout = capture_buffer + try: + yield capture_buffer + finally: + sys.stdout = old_stdout + + def test_DumpJSON_one_client(self): + with self.capture_stdout_to_buffer() as buffer: + command.DumpJSON().run(self.bus.one_client) + json_data = json.loads(buffer.getvalue()) + expected_json = {"foo": self.expected_json["foo"]} + self.assertDictEqual(expected_json, json_data) + + def test_PrintTable_normal(self): + with self.capture_stdout_to_buffer() as buffer: + command.PrintTable().run(self.bus.clients) + expected_output = "\n".join(( + "Name Enabled Timeout Last Successful Check", + "foo Yes 00:05:00 2019-02-03T00:00:00 ", + "barbar Yes 00:05:00 2019-02-04T00:00:00 ", + )) + "\n" + self.assertEqual(expected_output, buffer.getvalue()) + + def test_PrintTable_verbose(self): + with self.capture_stdout_to_buffer() as buffer: + command.PrintTable(verbose=True).run(self.bus.clients) + columns = ( + ( + "Name ", + "foo ", + "barbar ", + ),( + "Enabled ", + "Yes ", + "Yes ", + ),( + "Timeout ", + "00:05:00 ", + "00:05:00 ", + ),( + "Last Successful Check ", + "2019-02-03T00:00:00 ", + "2019-02-04T00:00:00 ", + ),( + "Created ", + "2019-01-02T00:00:00 ", + "2019-01-03T00:00:00 ", + ),( + "Interval ", + "00:02:00 ", + "00:02:00 ", + ),( + "Host ", + "foo.example.org ", + "192.0.2.3 ", + ),( + ("Key ID " + " "), + ("92ed150794387c03ce684574b1139a6594a34f895daaaf09fd8" + "ea90a27cddb12 "), + ("0558568eedd67d622f5c83b35a115f796ab612cff5ad227247e" + "46c2b020f441c "), + ),( + "Fingerprint ", + "778827225BA7DE539C5A7CFA59CFF7CDBD9A5920 ", + "3E393AEAEFB84C7E89E2F547B3A107558FCA3A27 ", + ),( + "Check Is Running ", + "No ", + "Yes ", + ),( + "Last Enabled ", + "2019-01-03T00:00:00 ", + "2019-01-04T00:00:00 ", + ),( + "Approval Is Pending ", + "No ", + "No ", + ),( + "Approved By Default ", + "Yes ", + "No ", + ),( + "Last Approval Request ", + " ", + "2019-01-03T00:00:00 ", + ),( + "Approval Delay ", + "00:00:00 ", + "00:00:30 ", + ),( + "Approval Duration ", + "00:00:01 ", + "1T02:03:05 ", + ),( + "Checker ", + "fping -q -- %(host)s ", + ": ", + ),( + "Extended Timeout ", + "00:15:00 ", + "00:15:00 ", + ),( + "Expires ", + "2019-02-04T00:00:00 ", + "2019-02-05T00:00:00 ", + ),( + "Last Checker Status", + "0 ", + "-2 ", + ) + ) + num_lines = max(len(rows) for rows in columns) + expected_output = ("\n".join("".join(rows[line] + for rows in columns) + for line in range(num_lines)) + + "\n") + self.assertEqual(expected_output, buffer.getvalue()) + + def test_PrintTable_one_client(self): + with self.capture_stdout_to_buffer() as buffer: + command.PrintTable().run(self.bus.one_client) + expected_output = "\n".join(( + "Name Enabled Timeout Last Successful Check", + "foo Yes 00:05:00 2019-02-03T00:00:00 ", + )) + "\n" + self.assertEqual(expected_output, buffer.getvalue()) + + +class TestPropertySetterCmd(TestCommand): + """Abstract class for tests of command.PropertySetter classes""" + + def runTest(self): + if not hasattr(self, "command"): + return # Abstract TestCase class + + if hasattr(self, "values_to_set"): + cmd_args = [(value,) for value in self.values_to_set] + values_to_get = getattr(self, "values_to_get", + self.values_to_set) + else: + cmd_args = [() for x in range(len(self.values_to_get))] + values_to_get = self.values_to_get + for value_to_get, cmd_arg in zip(values_to_get, cmd_args): + for clientpath in self.bus.clients: + self.bus.clients[clientpath][self.propname] = ( + Unique()) + self.command(*cmd_arg).run(self.bus.clients, self.bus) + for clientpath in self.bus.clients: + value = (self.bus.clients[clientpath] + [self.propname]) + self.assertNotIsInstance(value, Unique) + self.assertEqual(value_to_get, value) + + +class TestEnableCmd(TestPropertySetterCmd): + command = command.Enable + propname = "Enabled" + values_to_get = [True] + + +class TestDisableCmd(TestPropertySetterCmd): + command = command.Disable + propname = "Enabled" + values_to_get = [False] + + +class TestBumpTimeoutCmd(TestPropertySetterCmd): + command = command.BumpTimeout + propname = "LastCheckedOK" + values_to_get = [""] + + +class TestStartCheckerCmd(TestPropertySetterCmd): + command = command.StartChecker + propname = "CheckerRunning" + values_to_get = [True] + + +class TestStopCheckerCmd(TestPropertySetterCmd): + command = command.StopChecker + propname = "CheckerRunning" + values_to_get = [False] + + +class TestApproveByDefaultCmd(TestPropertySetterCmd): + command = command.ApproveByDefault + propname = "ApprovedByDefault" + values_to_get = [True] + + +class TestDenyByDefaultCmd(TestPropertySetterCmd): + command = command.DenyByDefault + propname = "ApprovedByDefault" + values_to_get = [False] + + +class TestSetCheckerCmd(TestPropertySetterCmd): + command = command.SetChecker + propname = "Checker" + values_to_set = ["", ":", "fping -q -- %s"] + + +class TestSetHostCmd(TestPropertySetterCmd): + command = command.SetHost + propname = "Host" + values_to_set = ["192.0.2.3", "client.example.org"] + + +class TestSetSecretCmd(TestPropertySetterCmd): + command = command.SetSecret + propname = "Secret" + values_to_set = [io.BytesIO(b""), + io.BytesIO(b"secret\0xyzzy\nbar")] + values_to_get = [f.getvalue() for f in values_to_set] + + +class TestSetTimeoutCmd(TestPropertySetterCmd): + command = command.SetTimeout + propname = "Timeout" + values_to_set = [datetime.timedelta(), + datetime.timedelta(minutes=5), + datetime.timedelta(seconds=1), + datetime.timedelta(weeks=1), + datetime.timedelta(weeks=52)] + values_to_get = [dt.total_seconds()*1000 for dt in values_to_set] + + +class TestSetExtendedTimeoutCmd(TestPropertySetterCmd): + command = command.SetExtendedTimeout + propname = "ExtendedTimeout" + values_to_set = [datetime.timedelta(), + datetime.timedelta(minutes=5), + datetime.timedelta(seconds=1), + datetime.timedelta(weeks=1), + datetime.timedelta(weeks=52)] + values_to_get = [dt.total_seconds()*1000 for dt in values_to_set] + + +class TestSetIntervalCmd(TestPropertySetterCmd): + command = command.SetInterval + propname = "Interval" + values_to_set = [datetime.timedelta(), + datetime.timedelta(minutes=5), + datetime.timedelta(seconds=1), + datetime.timedelta(weeks=1), + datetime.timedelta(weeks=52)] + values_to_get = [dt.total_seconds()*1000 for dt in values_to_set] + + +class TestSetApprovalDelayCmd(TestPropertySetterCmd): + command = command.SetApprovalDelay + propname = "ApprovalDelay" + values_to_set = [datetime.timedelta(), + datetime.timedelta(minutes=5), + datetime.timedelta(seconds=1), + datetime.timedelta(weeks=1), + datetime.timedelta(weeks=52)] + values_to_get = [dt.total_seconds()*1000 for dt in values_to_set] + + +class TestSetApprovalDurationCmd(TestPropertySetterCmd): + command = command.SetApprovalDuration + propname = "ApprovalDuration" + values_to_set = [datetime.timedelta(), + datetime.timedelta(minutes=5), + datetime.timedelta(seconds=1), + datetime.timedelta(weeks=1), + datetime.timedelta(weeks=52)] + values_to_get = [dt.total_seconds()*1000 for dt in values_to_set] + + + +def should_only_run_tests(): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--check", action='store_true') + args, unknown_args = parser.parse_known_args() + run_tests = args.check + if run_tests: + # Remove --check argument from sys.argv + sys.argv[1:] = unknown_args + return run_tests + +# Add all tests from doctest strings +def load_tests(loader, tests, none): + import doctest + tests.addTests(doctest.DocTestSuite()) + return tests + +if __name__ == "__main__": + try: + if should_only_run_tests(): + # Call using ./tdd-python-script --check [--verbose] + unittest.main() + else: + main() + finally: + logging.shutdown() === added file 'mandos-ctl.xml' --- mandos-ctl.xml 1970-01-01 00:00:00 +0000 +++ mandos-ctl.xml 2019-07-29 16:35:53 +0000 @@ -0,0 +1,652 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8 + + + + &COMMANDNAME; + + Control or query the operation of the Mandos server + + + + + + &COMMANDNAME; + + + + + + + + + + + CLIENT + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CLIENT + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + CLIENT + + + + + &COMMANDNAME; + + + + + + CLIENT + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + + + DESCRIPTION + + &COMMANDNAME; is a program to control or + query the operation of the Mandos server + mandos8. + + + This program can be used to change client settings, approve or + deny client requests, and to remove clients from the server. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OPTIONS + + + + + + + + Show a help message and exit + + + + + + + + + + Enable client(s). An enabled client will be eligble to + receive its secret. + + + + + + + + + + Disable client(s). A disabled client will not be eligble + to receive its secret, and no checkers will be started for + it. + + + + + + + + + Bump the timeout of the specified client(s), just as if a + checker had completed successfully for it/them. + + + + + + + + + Start a new checker now for the specified client(s). + + + + + + + + + Stop any running checker for the specified client(s). + + + + + + + + + + Remove the specified client(s) from the server. + + + + + + + + + + Set the checker option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Set the timeout option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + Set the extended_timeout option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + + Set the interval option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + + Set the approved_by_default option of + the specified client(s) to True or + False, respectively; see + mandos-clients.conf5. + + + + + + + + + Set the approval_delay option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + Set the approval_duration option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + + Set the host option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Set the secfile option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Approve client(s) if currently waiting for approval. + + + + + + + + + + Deny client(s) if currently waiting for approval. + + + + + + + + + + Make the client-modifying options modify all clients. + + + + + + + + + + Show all client settings, not just a subset. + + + + + + + + + + Dump client settings as JSON to standard output. + + + + + + + + + + Check if a single client is enabled or not, and exit with + a successful exit status only if the client is enabled. + + + + + + + + + Show debug output; currently, this means show D-Bus calls. + + + + + + + + + Run self-tests. This includes any unit tests, etc. + + + + + + + + + OVERVIEW + + + This program is a small utility to generate new OpenPGP keys for + new Mandos clients, and to generate sections for inclusion in + clients.conf on the server. + + + + + EXIT STATUS + + If the option is used, the exit + status will be 0 only if the specified client is enabled. + + + + + BUGS + + + + + EXAMPLE + + + + + To list all clients: + + + &COMMANDNAME; + + + + + + + To list all settings for the clients + named foo1.example.org and foo2.example.org: + + + + +&COMMANDNAME; --verbose foo1.example.org foo2.example.org + + + + + + + + To enable all clients: + + + &COMMANDNAME; --enable --all + + + + + + + To change timeout and interval value for the clients + named foo1.example.org and foo2.example.org: + + + + +&COMMANDNAME; --timeout=PT5M --interval=PT1M foo1.example.org foo2.example.org + + + + + + + + To approve all clients currently waiting for approval: + + + &COMMANDNAME; --approve --all + + + + + + SECURITY + + This program must be permitted to access the Mandos server via + the D-Bus interface. This normally requires the root user, but + could be configured otherwise by reconfiguring the D-Bus server. + + + + + SEE ALSO + + intro + 8mandos, + mandos + 8, + mandos-clients.conf + 5, + mandos-monitor + 8 + + + +
+ + + + + === added file 'mandos-keygen' --- mandos-keygen 1970-01-01 00:00:00 +0000 +++ mandos-keygen 2019-09-03 19:06:41 +0000 @@ -0,0 +1,451 @@ +#!/bin/sh -e +# +# Mandos key generator - create new keys for a Mandos client +# +# Copyright © 2008-2019 Teddy Hogeborn +# Copyright © 2008-2019 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos 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. +# +# Mandos 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 Mandos. If not, see . +# +# Contact the authors at . +# + +VERSION="1.8.9" + +KEYDIR="/etc/keys/mandos" +KEYTYPE=RSA +KEYLENGTH=4096 +SUBKEYTYPE=RSA +SUBKEYLENGTH=4096 +KEYNAME="`hostname --fqdn 2>/dev/null || hostname`" +KEYEMAIL="" +KEYCOMMENT="" +KEYEXPIRE=0 +TLS_KEYTYPE=ed25519 +FORCE=no +SSH=yes +KEYCOMMENT_ORIG="$KEYCOMMENT" +mode=keygen + +if [ ! -d "$KEYDIR" ]; then + KEYDIR="/etc/mandos/keys" +fi + +# Parse options +TEMP=`getopt --options vhpF:d:t:l:s:L:n:e:c:x:T:fS \ + --longoptions version,help,password,passfile:,dir:,type:,length:,subtype:,sublength:,name:,email:,comment:,expire:,tls-keytype:,force,no-ssh \ + --name "$0" -- "$@"` + +help(){ +basename="`basename "$0"`" +cat <&2; exit 1;; + esac +done +if [ "$#" -gt 0 ]; then + echo "Unknown arguments: '$*'" >&2 + exit 1 +fi + +SECKEYFILE="$KEYDIR/seckey.txt" +PUBKEYFILE="$KEYDIR/pubkey.txt" +TLS_PRIVKEYFILE="$KEYDIR/tls-privkey.pem" +TLS_PUBKEYFILE="$KEYDIR/tls-pubkey.pem" + +# Check for some invalid values +if [ ! -d "$KEYDIR" ]; then + echo "$KEYDIR not a directory" >&2 + exit 1 +fi +if [ ! -r "$KEYDIR" ]; then + echo "Directory $KEYDIR not readable" >&2 + exit 1 +fi + +if [ "$mode" = keygen ]; then + if [ ! -w "$KEYDIR" ]; then + echo "Directory $KEYDIR not writeable" >&2 + exit 1 + fi + if [ -z "$KEYTYPE" ]; then + echo "Empty key type" >&2 + exit 1 + fi + + if [ -z "$KEYNAME" ]; then + echo "Empty key name" >&2 + exit 1 + fi + + if [ -z "$KEYLENGTH" ] || [ "$KEYLENGTH" -lt 512 ]; then + echo "Invalid key length" >&2 + exit 1 + fi + + if [ -z "$KEYEXPIRE" ]; then + echo "Empty key expiration" >&2 + exit 1 + fi + + # Make FORCE be 0 or 1 + case "$FORCE" in + [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) FORCE=1;; + [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) FORCE=0;; + esac + + if { [ -e "$SECKEYFILE" ] || [ -e "$PUBKEYFILE" ] \ + || [ -e "$TLS_PRIVKEYFILE" ] \ + || [ -e "$TLS_PUBKEYFILE" ]; } \ + && [ "$FORCE" -eq 0 ]; then + echo "Refusing to overwrite old key files; use --force" >&2 + exit 1 + fi + + # Set lines for GnuPG batch file + if [ -n "$KEYCOMMENT" ]; then + KEYCOMMENTLINE="Name-Comment: $KEYCOMMENT" + fi + if [ -n "$KEYEMAIL" ]; then + KEYEMAILLINE="Name-Email: $KEYEMAIL" + fi + + # Create temporary gpg batch file + BATCHFILE="`mktemp -t mandos-keygen-batch.XXXXXXXXXX`" + TLS_PRIVKEYTMP="`mktemp -t mandos-keygen-privkey.XXXXXXXXXX`" +fi + +if [ "$mode" = password ]; then + # Create temporary encrypted password file + SECFILE="`mktemp -t mandos-keygen-secfile.XXXXXXXXXX`" +fi + +# Create temporary key ring directory +RINGDIR="`mktemp -d -t mandos-keygen-keyrings.XXXXXXXXXX`" + +# Remove temporary files on exit +trap " +set +e; \ +test -n \"$SECFILE\" && shred --remove \"$SECFILE\"; \ +test -n \"$TLS_PRIVKEYTMP\" && shred --remove \"$TLS_PRIVKEYTMP\"; \ +shred --remove \"$RINGDIR\"/sec* 2>/dev/null; +test -n \"$BATCHFILE\" && rm --force \"$BATCHFILE\"; \ +rm --recursive --force \"$RINGDIR\"; +tty --quiet && stty echo; \ +" EXIT + +set -e + +umask 077 + +if [ "$mode" = keygen ]; then + # Create batch file for GnuPG + cat >"$BATCHFILE" <<-EOF + Key-Type: $KEYTYPE + Key-Length: $KEYLENGTH + Key-Usage: sign,auth + Subkey-Type: $SUBKEYTYPE + Subkey-Length: $SUBKEYLENGTH + Subkey-Usage: encrypt + Name-Real: $KEYNAME + $KEYCOMMENTLINE + $KEYEMAILLINE + Expire-Date: $KEYEXPIRE + #Preferences: + #Handle: + #%pubring pubring.gpg + #%secring secring.gpg + %no-protection + %commit + EOF + + if tty --quiet; then + cat <<-EOF + Note: Due to entropy requirements, key generation could take + anything from a few minutes to SEVERAL HOURS. Please be + patient and/or supply the system with more entropy if needed. + EOF + echo -n "Started: " + date + fi + + # Generate TLS private key + if certtool --generate-privkey --password='' \ + --outfile "$TLS_PRIVKEYTMP" --sec-param ultra \ + --key-type="$TLS_KEYTYPE" --pkcs8 --no-text 2>/dev/null; then + + # Backup any old key files + if cp --backup=numbered --force "$TLS_PRIVKEYFILE" "$TLS_PRIVKEYFILE" \ + 2>/dev/null; then + shred --remove "$TLS_PRIVKEYFILE" 2>/dev/null || : + fi + if cp --backup=numbered --force "$TLS_PUBKEYFILE" "$TLS_PUBKEYFILE" \ + 2>/dev/null; then + rm --force "$TLS_PUBKEYFILE" + fi + cp --archive "$TLS_PRIVKEYTMP" "$TLS_PRIVKEYFILE" + shred --remove "$TLS_PRIVKEYTMP" 2>/dev/null || : + + ## TLS public key + + # First try certtool from GnuTLS + if ! certtool --password='' --load-privkey="$TLS_PRIVKEYFILE" \ + --outfile="$TLS_PUBKEYFILE" --pubkey-info --no-text \ + 2>/dev/null; then + # Otherwise try OpenSSL + if ! openssl pkey -in "$TLS_PRIVKEYFILE" \ + -out "$TLS_PUBKEYFILE" -pubout; then + rm --force "$TLS_PUBKEYFILE" + # None of the commands succeded; give up + return 1 + fi + fi + fi + + # Make sure trustdb.gpg exists; + # this is a workaround for Debian bug #737128 + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" \ + --import-ownertrust < /dev/null + # Generate a new key in the key rings + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --trust-model always \ + --gen-key "$BATCHFILE" + rm --force "$BATCHFILE" + + if tty --quiet; then + echo -n "Finished: " + date + fi + + # Backup any old key files + if cp --backup=numbered --force "$SECKEYFILE" "$SECKEYFILE" \ + 2>/dev/null; then + shred --remove "$SECKEYFILE" 2>/dev/null || : + fi + if cp --backup=numbered --force "$PUBKEYFILE" "$PUBKEYFILE" \ + 2>/dev/null; then + rm --force "$PUBKEYFILE" + fi + + FILECOMMENT="Mandos client key for $KEYNAME" + if [ "$KEYCOMMENT" != "$KEYCOMMENT_ORIG" ]; then + FILECOMMENT="$FILECOMMENT ($KEYCOMMENT)" + fi + + if [ -n "$KEYEMAIL" ]; then + FILECOMMENT="$FILECOMMENT <$KEYEMAIL>" + fi + + # Export key from key rings to key files + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --armor --export-options export-minimal \ + --comment "$FILECOMMENT" --output "$SECKEYFILE" \ + --export-secret-keys + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --armor --export-options export-minimal \ + --comment "$FILECOMMENT" --output "$PUBKEYFILE" --export +fi + +if [ "$mode" = password ]; then + + # Make SSH be 0 or 1 + case "$SSH" in + [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) SSH=1;; + [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) SSH=0;; + esac + + if [ $SSH -eq 1 ]; then + for ssh_keytype in ecdsa-sha2-nistp256 ed25519 rsa; do + set +e + ssh_fingerprint="`ssh-keyscan -t $ssh_keytype localhost 2>/dev/null`" + err=$? + set -e + if [ $err -ne 0 ]; then + ssh_fingerprint="" + continue + fi + if [ -n "$ssh_fingerprint" ]; then + ssh_fingerprint="${ssh_fingerprint#localhost }" + break + fi + done + fi + + # Import key into temporary key rings + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --trust-model always --armor \ + --import "$SECKEYFILE" + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --trust-model always --armor \ + --import "$PUBKEYFILE" + + # Get fingerprint of key + FINGERPRINT="`gpg --quiet --batch --no-tty --no-options \ + --enable-dsa2 --homedir "$RINGDIR" --trust-model always \ + --fingerprint --with-colons \ + | sed --quiet \ + --expression='/^fpr:/{s/^fpr:.*:\\([0-9A-Z]*\\):\$/\\1/p;q}'`" + + test -n "$FINGERPRINT" + + if [ -r "$TLS_PUBKEYFILE" ]; then + KEY_ID="$(certtool --key-id --hash=sha256 \ + --infile="$TLS_PUBKEYFILE" 2>/dev/null || :)" + + if [ -z "$KEY_ID" ]; then + KEY_ID=$(openssl pkey -pubin -in "$TLS_PUBKEYFILE" \ + -outform der \ + | openssl sha256 \ + | sed --expression='s/^.*[^[:xdigit:]]//') + fi + test -n "$KEY_ID" + fi + + FILECOMMENT="Encrypted password for a Mandos client" + + while [ ! -s "$SECFILE" ]; do + if [ -n "$PASSFILE" ]; then + cat -- "$PASSFILE" + else + tty --quiet && stty -echo + echo -n "Enter passphrase: " >/dev/tty + read -r first + tty --quiet && echo >&2 + echo -n "Repeat passphrase: " >/dev/tty + read -r second + if tty --quiet; then + echo >&2 + stty echo + fi + if [ "$first" != "$second" ]; then + echo "Passphrase mismatch" >&2 + touch "$RINGDIR"/mismatch + else + echo -n "$first" + fi + fi | gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" --trust-model always --armor \ + --encrypt --sign --recipient "$FINGERPRINT" --comment \ + "$FILECOMMENT" > "$SECFILE" + if [ -e "$RINGDIR"/mismatch ]; then + rm --force "$RINGDIR"/mismatch + if tty --quiet; then + > "$SECFILE" + else + exit 1 + fi + fi + done + + cat <<-EOF + [$KEYNAME] + host = $KEYNAME + EOF + if [ -n "$KEY_ID" ]; then + echo "key_id = $KEY_ID" + fi + cat <<-EOF + fingerprint = $FINGERPRINT + secret = + EOF + sed --quiet --expression=' + /^-----BEGIN PGP MESSAGE-----$/,/^-----END PGP MESSAGE-----$/{ + /^$/,${ + # Remove 24-bit Radix-64 checksum + s/=....$// + # Indent four spaces + /^[^-]/s/^/ /p + } + }' < "$SECFILE" + if [ -n "$ssh_fingerprint" ]; then + echo 'checker = ssh-keyscan -t '"$ssh_keytype"' %%(host)s 2>/dev/null | grep --fixed-strings --line-regexp --quiet --regexp=%%(host)s" %(ssh_fingerprint)s"' + echo "ssh_fingerprint = ${ssh_fingerprint}" + fi +fi + +trap - EXIT + +set +e +# Remove the password file, if any +if [ -n "$SECFILE" ]; then + shred --remove "$SECFILE" 2>/dev/null +fi +# Remove the key rings +shred --remove "$RINGDIR"/sec* 2>/dev/null +rm --recursive --force "$RINGDIR" === added file 'mandos-keygen.xml' --- mandos-keygen.xml 1970-01-01 00:00:00 +0000 +++ mandos-keygen.xml 2019-07-18 00:02:43 +0000 @@ -0,0 +1,590 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8 + + + + &COMMANDNAME; + + Generate key and password for Mandos client and server. + + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; + + + + + + FILE + + + + + + + + + + + + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + + + + + + DESCRIPTION + + &COMMANDNAME; is a program to generate the + TLS and OpenPGP keys used by + mandos-client + 8mandos. The keys are + normally written to /etc/keys/mandos for later installation into + the initrd image, but this, and most other things, can be + changed with command line options. + + + This program can also be used with the + or + options to generate a ready-made section for + clients.conf (see + mandos-clients.conf + 5). + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OPTIONS + + + + + + + + Show a help message and exit + + + + + + + + + + Target directory for key files. Default is /etc/keys/mandos. + + + + + + + + + + OpenPGP key type. Default is RSA. + + + + + + + + + + OpenPGP key length in bits. Default is 4096. + + + + + + + + + + OpenPGP subkey type. Default is RSA + + + + + + + + + + OpenPGP subkey length in bits. Default is 4096. + + + + + + + + + + Email address of key. Default is empty. + + + + + + + + + + Comment field for key. Default is empty. + + + + + + + + + + Key expire time. Default is no expiration. See + gpg + 1 for syntax. + + + + + + + + + + TLS key type. Default is ed25519 + + + + + + + + + + Force overwriting old key. + + + + + + + + + Prompt for a password and encrypt it with the key already + present in either /etc/keys/mandos or + the directory specified with the + option. Outputs, on standard output, a section suitable + for inclusion in mandos-clients.conf8. The host name or the name + specified with the option is used + for the section header. All other options are ignored, + and no key is created. Note: white space is stripped from + the beginning and from the end of the password; See . + + + + + + + + + The same as , but read from + FILE, not the terminal, and + white space is not stripped from the password in any way. + + + + + + + + + When or + is given, this option will + prevent &COMMANDNAME; from calling + ssh-keyscan to get an SSH fingerprint + for this host and, if successful, output suitable config + options to use this fingerprint as a + option in the output. This is + otherwise the default behavior. + + + + + + + + OVERVIEW + + + This program is a small utility to generate new TLS and OpenPGP + keys for new Mandos clients, and to generate sections for + inclusion in clients.conf on the server. + + + + + EXIT STATUS + + The exit status will be 0 if a new key (or password, if the + option was used) was successfully + created, otherwise not. + + + + + ENVIRONMENT + + + TMPDIR + + + If set, temporary files will be created here. See + mktemp + 1. + + + + + + + + FILES + + Use the option to change where + &COMMANDNAME; will write the key files. The + default file names are shown here. + + + + /etc/keys/mandos/seckey.txt + + + OpenPGP secret key file which will be created or + overwritten. + + + + + /etc/keys/mandos/pubkey.txt + + + OpenPGP public key file which will be created or + overwritten. + + + + + /etc/keys/mandos/tls-privkey.pem + + + Private key file which will be created or overwritten. + + + + + /etc/keys/mandos/tls-pubkey.pem + + + Public key file which will be created or overwritten. + + + + + /tmp + + + Temporary files will be written here if + TMPDIR is not set. + + + + + + + + BUGS + + The / option + strips white space from the start and from the end of the + password before using it. If this is a problem, use the + option instead, which does not do + this. + + + + + + EXAMPLE + + + Normal invocation needs no options: + + + &COMMANDNAME; + + + + + Create key in another directory and of another type. Force + overwriting old key files: + + + + +&COMMANDNAME; --dir ~/keydir --type RSA --force + + + + + + Prompt for a password, encrypt it with the keys in /etc/keys/mandos and output a + section suitable for clients.conf. + + + &COMMANDNAME; --password + + + + + Prompt for a password, encrypt it with the keys in the + client-key directory and output a section + suitable for clients.conf. + + + + +&COMMANDNAME; --password --dir client-key + + + + + + + SECURITY + + The , , + , and + options can be used to create keys of low security. If in + doubt, leave them to the default values. + + + The key expire time is not guaranteed to be + honored by mandos + 8. + + + + + SEE ALSO + + intro + 8mandos, + gpg + 1, + mandos-clients.conf + 5, + mandos + 8, + mandos-client + 8mandos, + ssh-keyscan + 1 + + + +
+ + + + + === added file 'mandos-monitor' --- mandos-monitor 1970-01-01 00:00:00 +0000 +++ mandos-monitor 2019-11-03 19:09:41 +0000 @@ -0,0 +1,757 @@ +#!/usr/bin/python3 -bbI +# -*- mode: python; coding: utf-8 -*- +# +# Mandos Monitor - Control and monitor the Mandos server +# +# Copyright © 2009-2019 Teddy Hogeborn +# Copyright © 2009-2019 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos 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. +# +# Mandos 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 Mandos. If not, see . +# +# Contact the authors at . +# + +from __future__ import (division, absolute_import, print_function, + unicode_literals) +try: + from future_builtins import * +except ImportError: + pass + +import sys +import os +import warnings +import datetime +import locale +import logging + +import urwid.curses_display +import urwid + +from dbus.mainloop.glib import DBusGMainLoop +from gi.repository import GLib + +import dbus + +if sys.version_info.major == 2: + str = unicode + +log = logging.getLogger(os.path.basename(sys.argv[0])) +logging.basicConfig(level="NOTSET", # Show all messages + format="%(message)s") # Show basic log messages + +logging.captureWarnings(True) # Show warnings via the logging system + +locale.setlocale(locale.LC_ALL, "") + +logging.getLogger("dbus.proxies").setLevel(logging.CRITICAL) + +# Some useful constants +domain = "se.recompile" +server_interface = domain + ".Mandos" +client_interface = domain + ".Mandos.Client" +version = "1.8.9" + +try: + dbus.OBJECT_MANAGER_IFACE +except AttributeError: + dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager" + + +def isoformat_to_datetime(iso): + "Parse an ISO 8601 date string to a datetime.datetime()" + if not iso: + return None + d, t = iso.split("T", 1) + year, month, day = d.split("-", 2) + hour, minute, second = t.split(":", 2) + second, fraction = divmod(float(second), 1) + return datetime.datetime(int(year), + int(month), + int(day), + int(hour), + int(minute), + int(second), # Whole seconds + int(fraction*1000000)) # Microseconds + + +class MandosClientPropertyCache(object): + """This wraps a Mandos Client D-Bus proxy object, caches the + properties and calls a hook function when any of them are + changed. + """ + def __init__(self, proxy_object=None, properties=None, **kwargs): + self.proxy = proxy_object # Mandos Client proxy object + self.properties = dict() if properties is None else properties + self.property_changed_match = ( + self.proxy.connect_to_signal("PropertiesChanged", + self.properties_changed, + dbus.PROPERTIES_IFACE, + byte_arrays=True)) + + if properties is None: + self.properties.update(self.proxy.GetAll( + client_interface, + dbus_interface=dbus.PROPERTIES_IFACE)) + + super(MandosClientPropertyCache, self).__init__(**kwargs) + + def properties_changed(self, interface, properties, invalidated): + """This is called whenever we get a PropertiesChanged signal + It updates the changed properties in the "properties" dict. + """ + # Update properties dict with new value + if interface == client_interface: + self.properties.update(properties) + + def delete(self): + self.property_changed_match.remove() + + +class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache): + """A Mandos Client which is visible on the screen. + """ + + def __init__(self, server_proxy_object=None, update_hook=None, + delete_hook=None, **kwargs): + # Called on update + self.update_hook = update_hook + # Called on delete + self.delete_hook = delete_hook + # Mandos Server proxy object + self.server_proxy_object = server_proxy_object + + self._update_timer_callback_tag = None + + # The widget shown normally + self._text_widget = urwid.Text("") + # The widget shown when we have focus + self._focus_text_widget = urwid.Text("") + super(MandosClientWidget, self).__init__(**kwargs) + self.update() + self.opened = False + + self.match_objects = ( + self.proxy.connect_to_signal("CheckerCompleted", + self.checker_completed, + client_interface, + byte_arrays=True), + self.proxy.connect_to_signal("CheckerStarted", + self.checker_started, + client_interface, + byte_arrays=True), + self.proxy.connect_to_signal("GotSecret", + self.got_secret, + client_interface, + byte_arrays=True), + self.proxy.connect_to_signal("NeedApproval", + self.need_approval, + client_interface, + byte_arrays=True), + self.proxy.connect_to_signal("Rejected", + self.rejected, + client_interface, + byte_arrays=True)) + log.debug("Created client %s", self.properties["Name"]) + + def using_timer(self, flag): + """Call this method with True or False when timer should be + activated or deactivated. + """ + if flag and self._update_timer_callback_tag is None: + # Will update the shown timer value every second + self._update_timer_callback_tag = ( + GLib.timeout_add(1000, + glib_safely(self.update_timer))) + elif not (flag or self._update_timer_callback_tag is None): + GLib.source_remove(self._update_timer_callback_tag) + self._update_timer_callback_tag = None + + def checker_completed(self, exitstatus, condition, command): + if exitstatus == 0: + log.debug('Checker for client %s (command "%s")' + " succeeded", self.properties["Name"], command) + self.update() + return + # Checker failed + if os.WIFEXITED(condition): + log.info('Checker for client %s (command "%s") failed' + " with exit code %d", self.properties["Name"], + command, os.WEXITSTATUS(condition)) + elif os.WIFSIGNALED(condition): + log.info('Checker for client %s (command "%s") was' + " killed by signal %d", self.properties["Name"], + command, os.WTERMSIG(condition)) + self.update() + + def checker_started(self, command): + """Server signals that a checker started.""" + log.debug('Client %s started checker "%s"', + self.properties["Name"], command) + + def got_secret(self): + log.info("Client %s received its secret", + self.properties["Name"]) + + def need_approval(self, timeout, default): + if not default: + message = "Client %s needs approval within %f seconds" + else: + message = "Client %s will get its secret in %f seconds" + log.info(message, self.properties["Name"], timeout/1000) + + def rejected(self, reason): + log.info("Client %s was rejected; reason: %s", + self.properties["Name"], reason) + + def selectable(self): + """Make this a "selectable" widget. + This overrides the method from urwid.FlowWidget.""" + return True + + def rows(self, maxcolrow, focus=False): + """How many rows this widget will occupy might depend on + whether we have focus or not. + This overrides the method from urwid.FlowWidget""" + return self.current_widget(focus).rows(maxcolrow, focus=focus) + + def current_widget(self, focus=False): + if focus or self.opened: + return self._focus_widget + return self._widget + + def update(self): + "Called when what is visible on the screen should be updated." + # How to add standout mode to a style + with_standout = {"normal": "standout", + "bold": "bold-standout", + "underline-blink": + "underline-blink-standout", + "bold-underline-blink": + "bold-underline-blink-standout", + } + + # Rebuild focus and non-focus widgets using current properties + + # Base part of a client. Name! + base = "{name}: ".format(name=self.properties["Name"]) + if not self.properties["Enabled"]: + message = "DISABLED" + self.using_timer(False) + elif self.properties["ApprovalPending"]: + timeout = datetime.timedelta( + milliseconds=self.properties["ApprovalDelay"]) + last_approval_request = isoformat_to_datetime( + self.properties["LastApprovalRequest"]) + if last_approval_request is not None: + timer = max(timeout - (datetime.datetime.utcnow() + - last_approval_request), + datetime.timedelta()) + else: + timer = datetime.timedelta() + if self.properties["ApprovedByDefault"]: + message = "Approval in {}. (d)eny?" + else: + message = "Denial in {}. (a)pprove?" + message = message.format(str(timer).rsplit(".", 1)[0]) + self.using_timer(True) + elif self.properties["LastCheckerStatus"] != 0: + # When checker has failed, show timer until client expires + expires = self.properties["Expires"] + if expires == "": + timer = datetime.timedelta(0) + else: + expires = (datetime.datetime.strptime + (expires, "%Y-%m-%dT%H:%M:%S.%f")) + timer = max(expires - datetime.datetime.utcnow(), + datetime.timedelta()) + message = ("A checker has failed! Time until client" + " gets disabled: {}" + .format(str(timer).rsplit(".", 1)[0])) + self.using_timer(True) + else: + message = "enabled" + self.using_timer(False) + self._text = "{}{}".format(base, message) + + if not urwid.supports_unicode(): + self._text = self._text.encode("ascii", "replace") + textlist = [("normal", self._text)] + self._text_widget.set_text(textlist) + self._focus_text_widget.set_text([(with_standout[text[0]], + text[1]) + if isinstance(text, tuple) + else text + for text in textlist]) + self._widget = self._text_widget + self._focus_widget = urwid.AttrWrap(self._focus_text_widget, + "standout") + # Run update hook, if any + if self.update_hook is not None: + self.update_hook() + + def update_timer(self): + """called by GLib. Will indefinitely loop until + GLib.source_remove() on tag is called + """ + self.update() + return True # Keep calling this + + def delete(self, **kwargs): + if self._update_timer_callback_tag is not None: + GLib.source_remove(self._update_timer_callback_tag) + self._update_timer_callback_tag = None + for match in self.match_objects: + match.remove() + self.match_objects = () + if self.delete_hook is not None: + self.delete_hook(self) + return super(MandosClientWidget, self).delete(**kwargs) + + def render(self, maxcolrow, focus=False): + """Render differently if we have focus. + This overrides the method from urwid.FlowWidget""" + return self.current_widget(focus).render(maxcolrow, + focus=focus) + + def keypress(self, maxcolrow, key): + """Handle keys. + This overrides the method from urwid.FlowWidget""" + if key == "+": + self.proxy.Set(client_interface, "Enabled", + dbus.Boolean(True), ignore_reply=True, + dbus_interface=dbus.PROPERTIES_IFACE) + elif key == "-": + self.proxy.Set(client_interface, "Enabled", False, + ignore_reply=True, + dbus_interface=dbus.PROPERTIES_IFACE) + elif key == "a": + self.proxy.Approve(dbus.Boolean(True, variant_level=1), + dbus_interface=client_interface, + ignore_reply=True) + elif key == "d": + self.proxy.Approve(dbus.Boolean(False, variant_level=1), + dbus_interface=client_interface, + ignore_reply=True) + elif key == "R" or key == "_" or key == "ctrl k": + self.server_proxy_object.RemoveClient(self.proxy + .object_path, + ignore_reply=True) + elif key == "s": + self.proxy.Set(client_interface, "CheckerRunning", + dbus.Boolean(True), ignore_reply=True, + dbus_interface=dbus.PROPERTIES_IFACE) + elif key == "S": + self.proxy.Set(client_interface, "CheckerRunning", + dbus.Boolean(False), ignore_reply=True, + dbus_interface=dbus.PROPERTIES_IFACE) + elif key == "C": + self.proxy.CheckedOK(dbus_interface=client_interface, + ignore_reply=True) + # xxx +# elif key == "p" or key == "=": +# self.proxy.pause() +# elif key == "u" or key == ":": +# self.proxy.unpause() +# elif key == "RET": +# self.open() + else: + return key + + def properties_changed(self, interface, properties, invalidated): + """Call self.update() if any properties changed. + This overrides the method from MandosClientPropertyCache""" + old_values = {key: self.properties.get(key) + for key in properties.keys()} + super(MandosClientWidget, self).properties_changed( + interface, properties, invalidated) + if any(old_values[key] != self.properties.get(key) + for key in old_values): + self.update() + + +def glib_safely(func, retval=True): + def safe_func(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: + log.exception("") + return retval + return safe_func + + +class ConstrainedListBox(urwid.ListBox): + """Like a normal urwid.ListBox, but will consume all "up" or + "down" key presses, thus not allowing any containing widgets to + use them as an excuse to shift focus away from this widget. + """ + def keypress(self, *args, **kwargs): + ret = (super(ConstrainedListBox, self) + .keypress(*args, **kwargs)) + if ret in ("up", "down"): + return + return ret + + +class UserInterface(object): + """This is the entire user interface - the whole screen + with boxes, lists of client widgets, etc. + """ + def __init__(self, max_log_length=1000): + DBusGMainLoop(set_as_default=True) + + self.screen = urwid.curses_display.Screen() + + self.screen.register_palette(( + ("normal", + "default", "default", None), + ("bold", + "bold", "default", "bold"), + ("underline-blink", + "underline,blink", "default", "underline,blink"), + ("standout", + "standout", "default", "standout"), + ("bold-underline-blink", + "bold,underline,blink", "default", + "bold,underline,blink"), + ("bold-standout", + "bold,standout", "default", "bold,standout"), + ("underline-blink-standout", + "underline,blink,standout", "default", + "underline,blink,standout"), + ("bold-underline-blink-standout", + "bold,underline,blink,standout", "default", + "bold,underline,blink,standout"), + )) + + if urwid.supports_unicode(): + self.divider = "─" # \u2500 + else: + self.divider = "_" # \u005f + + self.screen.start() + + self.size = self.screen.get_cols_rows() + + self.clients = urwid.SimpleListWalker([]) + self.clients_dict = {} + + # We will add Text widgets to this list + self.log = urwid.SimpleListWalker([]) + self.max_log_length = max_log_length + + # We keep a reference to the log widget so we can remove it + # from the ListWalker without it getting destroyed + self.logbox = ConstrainedListBox(self.log) + + # This keeps track of whether self.uilist currently has + # self.logbox in it or not + self.log_visible = True + self.log_wrap = "any" + + self.loghandler = UILogHandler(self) + + self.rebuild() + self.add_log_line(("bold", + "Mandos Monitor version " + version)) + self.add_log_line(("bold", "q: Quit ?: Help")) + + self.busname = domain + ".Mandos" + self.main_loop = GLib.MainLoop() + + def client_not_found(self, key_id, address): + log.info("Client with address %s and key ID %s could" + " not be found", address, key_id) + + def rebuild(self): + """This rebuilds the User Interface. + Call this when the widget layout needs to change""" + self.uilist = [] + # self.uilist.append(urwid.ListBox(self.clients)) + self.uilist.append(urwid.Frame(ConstrainedListBox(self. + clients), + # header=urwid.Divider(), + header=None, + footer=urwid.Divider( + div_char=self.divider))) + if self.log_visible: + self.uilist.append(self.logbox) + self.topwidget = urwid.Pile(self.uilist) + + def add_log_line(self, markup): + self.log.append(urwid.Text(markup, wrap=self.log_wrap)) + if self.max_log_length: + if len(self.log) > self.max_log_length: + del self.log[0:(len(self.log) - self.max_log_length)] + self.logbox.set_focus(len(self.logbox.body.contents)-1, + coming_from="above") + self.refresh() + + def toggle_log_display(self): + """Toggle visibility of the log buffer.""" + self.log_visible = not self.log_visible + self.rebuild() + log.debug("Log visibility changed to: %s", self.log_visible) + + def change_log_display(self): + """Change type of log display. + Currently, this toggles wrapping of text lines.""" + if self.log_wrap == "clip": + self.log_wrap = "any" + else: + self.log_wrap = "clip" + for textwidget in self.log: + textwidget.set_wrap_mode(self.log_wrap) + log.debug("Wrap mode: %s", self.log_wrap) + + def find_and_remove_client(self, path, interfaces): + """Find a client by its object path and remove it. + + This is connected to the InterfacesRemoved signal from the + Mandos server object.""" + if client_interface not in interfaces: + # Not a Mandos client object; ignore + return + try: + client = self.clients_dict[path] + except KeyError: + # not found? + log.warning("Unknown client %s removed", path) + return + client.delete() + + def add_new_client(self, path, ifs_and_props): + """Find a client by its object path and remove it. + + This is connected to the InterfacesAdded signal from the + Mandos server object. + """ + if client_interface not in ifs_and_props: + # Not a Mandos client object; ignore + return + client_proxy_object = self.bus.get_object(self.busname, path) + self.add_client(MandosClientWidget( + server_proxy_object=self.mandos_serv, + proxy_object=client_proxy_object, + update_hook=self.refresh, + delete_hook=self.remove_client, + properties=dict(ifs_and_props[client_interface])), + path=path) + + def add_client(self, client, path=None): + self.clients.append(client) + if path is None: + path = client.proxy.object_path + self.clients_dict[path] = client + self.clients.sort(key=lambda c: c.properties["Name"]) + self.refresh() + + def remove_client(self, client, path=None): + self.clients.remove(client) + if path is None: + path = client.proxy.object_path + del self.clients_dict[path] + self.refresh() + + def refresh(self): + """Redraw the screen""" + canvas = self.topwidget.render(self.size, focus=True) + self.screen.draw_screen(self.size, canvas) + + def run(self): + """Start the main loop and exit when it's done.""" + log.addHandler(self.loghandler) + self.orig_log_propagate = log.propagate + log.propagate = False + self.orig_log_level = log.level + log.setLevel("INFO") + self.bus = dbus.SystemBus() + mandos_dbus_objc = self.bus.get_object( + self.busname, "/", follow_name_owner_changes=True) + self.mandos_serv = dbus.Interface( + mandos_dbus_objc, dbus_interface=server_interface) + try: + mandos_clients = (self.mandos_serv + .GetAllClientsWithProperties()) + if not mandos_clients: + log.warning("Note: Server has no clients.") + except dbus.exceptions.DBusException: + log.warning("Note: No Mandos server running.") + mandos_clients = dbus.Dictionary() + + (self.mandos_serv + .connect_to_signal("InterfacesRemoved", + self.find_and_remove_client, + dbus_interface=dbus.OBJECT_MANAGER_IFACE, + byte_arrays=True)) + (self.mandos_serv + .connect_to_signal("InterfacesAdded", + self.add_new_client, + dbus_interface=dbus.OBJECT_MANAGER_IFACE, + byte_arrays=True)) + (self.mandos_serv + .connect_to_signal("ClientNotFound", + self.client_not_found, + dbus_interface=server_interface, + byte_arrays=True)) + for path, client in mandos_clients.items(): + client_proxy_object = self.bus.get_object(self.busname, + path) + self.add_client(MandosClientWidget( + server_proxy_object=self.mandos_serv, + proxy_object=client_proxy_object, + properties=client, + update_hook=self.refresh, + delete_hook=self.remove_client), + path=path) + + self.refresh() + self._input_callback_tag = ( + GLib.io_add_watch( + GLib.IOChannel.unix_new(sys.stdin.fileno()), + GLib.PRIORITY_DEFAULT, GLib.IO_IN, + glib_safely(self.process_input))) + self.main_loop.run() + # Main loop has finished, we should close everything now + GLib.source_remove(self._input_callback_tag) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", BytesWarning) + self.screen.stop() + + def stop(self): + self.main_loop.quit() + log.removeHandler(self.loghandler) + log.propagate = self.orig_log_propagate + + def process_input(self, source, condition): + keys = self.screen.get_input() + translations = {"ctrl n": "down", # Emacs + "ctrl p": "up", # Emacs + "ctrl v": "page down", # Emacs + "meta v": "page up", # Emacs + " ": "page down", # less + "f": "page down", # less + "b": "page up", # less + "j": "down", # vi + "k": "up", # vi + } + for key in keys: + try: + key = translations[key] + except KeyError: # :-) + pass + + if key == "q" or key == "Q": + self.stop() + break + elif key == "window resize": + self.size = self.screen.get_cols_rows() + self.refresh() + elif key == "ctrl l": + self.screen.clear() + self.refresh() + elif key == "l" or key == "D": + self.toggle_log_display() + self.refresh() + elif key == "w" or key == "i": + self.change_log_display() + self.refresh() + elif key == "?" or key == "f1" or key == "esc": + if not self.log_visible: + self.log_visible = True + self.rebuild() + self.add_log_line(("bold", + " ".join(("q: Quit", + "?: Help", + "l: Log window toggle", + "TAB: Switch window", + "w: Wrap (log lines)", + "v: Toggle verbose log", + )))) + self.add_log_line(("bold", + " ".join(("Clients:", + "+: Enable", + "-: Disable", + "R: Remove", + "s: Start new checker", + "S: Stop checker", + "C: Checker OK", + "a: Approve", + "d: Deny", + )))) + self.refresh() + elif key == "tab": + if self.topwidget.get_focus() is self.logbox: + self.topwidget.set_focus(0) + else: + self.topwidget.set_focus(self.logbox) + self.refresh() + elif key == "v": + if log.level < logging.INFO: + log.setLevel(logging.INFO) + log.info("Verbose mode: Off") + else: + log.setLevel(logging.NOTSET) + log.info("Verbose mode: On") + # elif (key == "end" or key == "meta >" or key == "G" + # or key == ">"): + # pass # xxx end-of-buffer + # elif (key == "home" or key == "meta <" or key == "g" + # or key == "<"): + # pass # xxx beginning-of-buffer + # elif key == "ctrl e" or key == "$": + # pass # xxx move-end-of-line + # elif key == "ctrl a" or key == "^": + # pass # xxx move-beginning-of-line + # elif key == "ctrl b" or key == "meta (" or key == "h": + # pass # xxx left + # elif key == "ctrl f" or key == "meta )" or key == "l": + # pass # xxx right + # elif key == "a": + # pass # scroll up log + # elif key == "z": + # pass # scroll down log + elif self.topwidget.selectable(): + self.topwidget.keypress(self.size, key) + self.refresh() + return True + + +class UILogHandler(logging.Handler): + def __init__(self, ui, *args, **kwargs): + self.ui = ui + super(UILogHandler, self).__init__(*args, **kwargs) + self.setFormatter( + logging.Formatter("%(asctime)s: %(message)s")) + def emit(self, record): + msg = self.format(record) + if record.levelno > logging.INFO: + msg = ("bold", msg) + self.ui.add_log_line(msg) + + +ui = UserInterface() +try: + ui.run() +except KeyboardInterrupt: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "", BytesWarning) + ui.screen.stop() +except Exception: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "", BytesWarning) + ui.screen.stop() + raise === added file 'mandos-monitor.xml' --- mandos-monitor.xml 1970-01-01 00:00:00 +0000 +++ mandos-monitor.xml 2019-02-10 04:20:26 +0000 @@ -0,0 +1,249 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8 + + + + &COMMANDNAME; + + Text-based GUI to control the Mandos server. + + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + &COMMANDNAME; is an interactive program to + monitor and control the operations of the Mandos server (see + mandos8). + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OVERVIEW + + + This program is used to monitor and control the Mandos server. + In particular, it can be used to approve Mandos clients which + have been configured to require approval. It also shows all + significant events reported by the Mandos server. + + + + + KEYS + + This program is used to monitor and control the Mandos server. + In particular, it can be used to approve Mandos clients which + have been configured to require approval. It also shows all + significant events reported by the Mandos server. + + + Global Keys + + Keys + Function + + + + q, Q + Quit + + + Ctrl-L + Redraw screen + + + ?, F1 + Show help + + + l, D + Toggle log window + + + TAB + Switch window + + + w, i + Toggle log window line wrap + + + v + Toggle verbose logging + + + Up, Ctrl-P, k + Move up a line + + + Down, Ctrl-N, j + Move down a line + + + PageUp, Meta-V, b + Move up a page + + + PageDown, Ctrl-V, SPACE, f + Move down a page + +
+ + Client List Keys + + Keys + Function + + + + + + Enable client + + + - + Disable client + + + a + Approve client + + + d + Deny client + + + R, _, Ctrl-K + Remove client + + + s + Start checker for client + + + S + Stop checker for client + + + C + Force a successful check for this client. + +
+
+ + + BUGS + + This program can currently only be used to monitor and control a + Mandos server with the default D-Bus bus name of + se.recompile.Mandos. + + + + + + EXAMPLE + + + This program takes no options: + + + &COMMANDNAME; + + + + + + SECURITY + + This program must be permitted to access the Mandos server via + the D-Bus interface. This normally requires the root user, but + could be configured otherwise by reconfiguring the D-Bus server. + + + + + SEE ALSO + + intro + 8mandos, + mandos + 8, + mandos-ctl + 8 + + + +
+ + + + + === added file 'mandos-options.xml' --- mandos-options.xml 1970-01-01 00:00:00 +0000 +++ mandos-options.xml 2019-07-25 21:42:40 +0000 @@ -0,0 +1,125 @@ + + + + + +
+ + + <para id="interface"> + If this is specified, the server will only announce the service + and listen to requests on the specified network interface. + Default is to use all available interfaces. <emphasis + >Note:</emphasis> a failure to bind to the specified + interface is not considered critical, and the server will not + exit, but instead continue normally. + </para> + + <para id="address"> + If this option is used, the server will only listen to the + specified IPv6 address. If a link-local address is specified, an + interface should be set, since a link-local address is only valid + on a single interface. By default, the server will listen to all + available addresses. If set, this must normally be an IPv6 + address; an IPv4 address can only be specified using IPv4-mapped + IPv6 address syntax: <quote><systemitem class="ipaddress" + >::FFFF:192.0.2.3</systemitem ></quote>. (Only if IPv6 usage is + <emphasis>disabled</emphasis> (see below) must this be an IPv4 + address.) + </para> + + <para id="port"> + If this option is used, the server will bind to that port. By + default, the server will listen to an arbitrary port given by the + operating system. + </para> + + <para id="debug"> + If the server is run in debug mode, it will run in the foreground + and print a lot of debugging information. The default is to + <emphasis>not</emphasis> run in debug mode. + </para> + + <para id="priority"> + GnuTLS priority string for the <acronym>TLS</acronym> handshake. + The default is + <!-- ​ is Unicode ZERO WIDTH SPACE; allows line breaks --> + <quote><literal>SECURE128​:!CTYPE-X.509​:+CTYPE-RAWPK​:!RSA​:!VERS-ALL​:+VERS-TLS1.3​:%PROFILE_ULTRA</literal></quote> + when using raw public keys in TLS, and + <quote><literal>SECURE256​:!CTYPE-X.509​:+CTYPE-OPENPGP​:!RSA​:+SIGN-DSA-SHA256</literal></quote> + when using OpenPGP keys in TLS,. See <citerefentry><refentrytitle + >gnutls_priority_init</refentrytitle> + <manvolnum>3</manvolnum></citerefentry> for the syntax. + <emphasis>Warning</emphasis>: changing this may make the + <acronym>TLS</acronym> handshake fail, making server-client + communication impossible. Changing this option may also make the + network traffic decryptable by an attacker. + </para> + + <para id="servicename"> + Zeroconf service name. The default is + <quote><literal>Mandos</literal></quote>. This only needs to be + changed if for some reason is would be necessary to run more than + one server on the same <emphasis>host</emphasis>. This would not + normally be useful. If there are name collisions on the same + <emphasis>network</emphasis>, the newer server will automatically + rename itself to <quote><literal>Mandos #2</literal></quote>, and + so on; therefore, this option is not needed in that case. + </para> + + <para id="dbus"> + This option controls whether the server will provide a D-Bus + system bus interface. The default is to provide such an + interface. + </para> + + <para id="ipv6"> + This option controls whether the server will use IPv6 sockets and + addresses. The default is to use IPv6. This option should + <emphasis>never</emphasis> normally be turned off, <emphasis>even in + IPv4-only environments</emphasis>. This is because <citerefentry> + <refentrytitle>mandos-client</refentrytitle> + <manvolnum>8mandos</manvolnum></citerefentry> will normally use + IPv6 link-local addresses, and will not be able to find or connect + to the server if this option is turned off. <emphasis>Only + advanced users should consider changing this option</emphasis>. + </para> + + <para id="restore"> + This option controls whether the server will restore its state + from the last time it ran. Default is to restore last state. + </para> + + <para id="statedir"> + Directory to save (and restore) state in. Default is + <quote><filename + class="directory">/var/lib/mandos</filename></quote>. + </para> + + <para id="socket"> + If this option is used, the server will not create a new network + socket, but will instead use the supplied file descriptor. By + default, the server will create a new network socket. + </para> + + <para id="foreground"> + This option will make the server run in the foreground and not + write a PID file. The default is to <emphasis>not</emphasis> run + in the foreground, except in <option>debug</option> mode, which + implies this option. + </para> + + <para id="zeroconf"> + This option controls whether the server will announce its + existence using Zeroconf. Default is to use Zeroconf. If + Zeroconf is not used, a <option>port</option> number or a + <option>socket</option> is required. + </para> + +</section> === added file 'mandos-to-cryptroot-unlock' --- mandos-to-cryptroot-unlock 1970-01-01 00:00:00 +0000 +++ mandos-to-cryptroot-unlock 2019-07-24 11:02:24 +0000 @@ -0,0 +1,82 @@ +#!/bin/sh +# +# Script to get password from plugin-runner to cryptroot-unlock +# +# Copyright © 2018 Teddy Hogeborn +# Copyright © 2018 Björn Påhlsson +# +# This file is part of Mandos. +# +# Mandos 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. +# +# Mandos 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 Mandos. If not, see <http://www.gnu.org/licenses/>. +# +# Contact the authors at <mandos@recompile.se>. + +# This script is made to run in the initramfs, and must not be run in +# the normal system environment. + +# Temporary file for the password +passfile=$(mktemp -p /run -t mandos.XXXXXX) +trap "rm -f -- $passfile 2>/dev/null" EXIT + +# Disable the plugins which conflict with "askpass" as distributed by +# cryptsetup. +cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf + + --disable=askpass-fifo + --disable=password-prompt + --disable=plymouth +EOF + +# In case a password is retrieved by other means than by plugin-runner +# (such as typing it on the console into the prompt given by the +# "askpass" program), this dummy plugin will be made to exit +# successfully, thereby forcing plugin-runner to stop all its plugins +# and also exit itself. +cat <<-EOF > /lib/mandos/plugins.d/dummy + #!/bin/sh + + while [ -e /run/mandos-keep-running ]; do + sleep 1 + done + + # exit successfully to force plugin-runner to finish + exit 0 +EOF +chmod u=rwx,go=rx /lib/mandos/plugins.d/dummy + +# This file is the flag which keeps the dummy plugin running +touch /run/mandos-keep-running + +# Keep running plugin-runner and trying any password, until either a +# password is accepted by cryptroot-unlock, or plugin-runner fails, or +# the file /run/mandos-keep-running has been removed. +while command -v cryptroot-unlock >/dev/null 2>&1; do + /lib/mandos/plugin-runner > "$passfile" & + echo $! > /run/mandos-plugin-runner.pid + wait %% || break + + # Try this password ten times (or ten seconds) + for loop in 1 2 3 4 5 6 7 8 9 10; do + if [ -e /run/mandos-keep-running ]; then + cryptroot-unlock < "$passfile" >/dev/null 2>&1 && break 2 + sleep 1 + else + break 2 + fi + done +done + +exec >/dev/null 2>&1 + +rm -f /run/mandos-plugin-runner.pid /run/mandos-keep-running === added file 'mandos.conf' --- mandos.conf 1970-01-01 00:00:00 +0000 +++ mandos.conf 2015-07-20 03:03:33 +0000 @@ -0,0 +1,53 @@ +# This file must have exactly one section named "DEFAULT". +[DEFAULT] + +# These are the default values for the server, uncomment and change +# them if needed. + +# If "interface" is set, the server will only listen to a specific +# network interface. +;interface = + +# If "address" is set, the server will only listen to a specific +# address. This must currently be an IPv6 address; an IPv4 address +# can be specified using the "::FFFF:192.0.2.3" syntax. Also, if this +# is a link-local address, an interface should be set above. +;address = + +# If "port" is set, the server to bind to that port. By default, the +# server will listen to an arbitrary port. +;port = + +# If "debug" is true, the server will run in the foreground and print +# a lot of debugging information. +;debug = False + +# GnuTLS priority for the TLS handshake. See gnutls_priority_init(3). +;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA:+SIGN-DSA-SHA256 + +# Zeroconf service name. You need to change this if you for some +# reason want to run more than one server on the same *host*. +# If there are name collisions on the same *network*, the server will +# rename itself to "Mandos #2", etc. +;servicename = Mandos + +# Whether to provide a D-Bus system bus interface or not +;use_dbus = True + +# Whether to use IPv6. (Changing this is NOT recommended.) +;use_ipv6 = True + +# Whether to restore saved state on startup +;restore = True + +# The directory where state is saved +;statedir = /var/lib/mandos + +# Whether to run in the foreground +;foreground = False + +# File descriptor number to use for network socket +;socket = + +# Whether to use ZeroConf; if false, requires port or socket +;zeroconf = True === added file 'mandos.conf.xml' --- mandos.conf.xml 1970-01-01 00:00:00 +0000 +++ mandos.conf.xml 2019-06-20 18:54:10 +0000 @@ -0,0 +1,308 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" + "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ +<!ENTITY CONFNAME "mandos.conf"> +<!ENTITY CONFPATH "<filename>/etc/mandos/mandos.conf</filename>"> +<!ENTITY TIMESTAMP "2019-06-20"> +<!ENTITY % common SYSTEM "common.ent"> +%common; +]> + +<refentry xmlns:xi="http://www.w3.org/2001/XInclude"> + <refentryinfo> + <title>Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + + + + + &CONFNAME; + 5 + + + + &CONFNAME; + + Configuration file for the Mandos server + + + + + &CONFPATH; + + + + DESCRIPTION + + The file &CONFPATH; is a simple configuration file for + mandos + 8, and is read by it at + startup. The configuration file starts with [DEFAULT] on a line by itself, followed by + any number of option=value entries, + with continuations in the style of RFC 822. option: value is also accepted. Note that + leading whitespace is removed from values. Lines beginning with + # or ; are ignored and may be used + to provide comments. + + + + + OPTIONS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FILES + + The file described here is &CONFPATH; + + + + + BUGS + + The [DEFAULT] is necessary because the Python + built-in module ConfigParser + requires it. + + + + + + EXAMPLE + + + No options are actually required: + + +[DEFAULT] + + + + + An example using all the options: + + +[DEFAULT] +# A configuration example +interface = enp1s0 +address = fe80::aede:48ff:fe71:f6f2 +port = 1025 +debug = True +priority = SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA:!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA +servicename = Daena +use_dbus = False +use_ipv6 = True +restore = True +statedir = /var/lib/mandos + + + + + + SEE ALSO + + intro + 8mandos, + gnutls_priority_init3, + mandos + 8, + mandos-clients.conf + 5 + + + + + + RFC 4291: IP Version 6 Addressing + Architecture + + + + + Section 2.2: Text Representation of + Addresses + + + + Section 2.5.5.2: IPv4-Mapped IPv6 + Address + + + + Section 2.5.6, Link-Local IPv6 Unicast + Addresses + + + The clients use IPv6 link-local addresses, which are + immediately usable since a link-local addresses is + automatically assigned to a network interface when it + is brought up. + + + + + + + + + Zeroconf + + + + Zeroconf is the network protocol standard used by clients + for finding the Mandos server on the local network. + + + + + + + + + + + === added file 'mandos.lsm' --- mandos.lsm 1970-01-01 00:00:00 +0000 +++ mandos.lsm 2019-09-03 19:06:41 +0000 @@ -0,0 +1,23 @@ +Begin4 +Title: Mandos +Version: 1.8.9 +Entered-date: 2019-09-03 +Description: The Mandos system allows computers to have encrypted + root file systems and at the same time be capable of + remote and/or unattended reboots. +Keywords: boot, encryption, luks, cryptsetup, network, openpgp, + tls, dm-crypt +Author: teddy@recompile.se (Teddy Hogeborn), + belorn@recompile.se (Björn Påhlsson) +Maintained-by: teddy@recompile.se (Teddy Hogeborn), + belorn@recompile.se (Björn Påhlsson) +Primary-site: https://www.recompile.se/mandos + 234K mandos_1.8.9.orig.tar.gz +Alternate-site: ftp://ftp.recompile.se/pub/mandos + 234K mandos_1.8.9.orig.tar.gz +Platforms: Requires GCC, GNU libC, Avahi, GnuPG, Python 2.7, and + various other libraries. While made for Debian + GNU/Linux, it is probably portable to other + distributions, but not other Unixes. +Copying-policy: GNU General Public License version 3.0 or later +End === added file 'mandos.service' --- mandos.service 1970-01-01 00:00:00 +0000 +++ mandos.service 2017-08-20 14:14:14 +0000 @@ -0,0 +1,35 @@ +[Unit] +Description=Server of encrypted passwords to Mandos clients +Documentation=man:intro(8mandos) man:mandos(8) +## If the server is configured to listen to a specific IP or network +## interface, it may be necessary to change "network.target" to +## "network-online.target". +After=network.target +## If the server is configured to not use ZeroConf, these two lines +## become unnecessary and should be removed or commented out. +After=avahi-daemon.service +Requisite=avahi-daemon.service + +[Service] +## If the server's D-Bus interface is disabled, the "BusName" setting +## should be removed or commented out. +BusName=se.recompile.Mandos +ExecStart=/usr/sbin/mandos --foreground +Restart=always +KillMode=mixed +## Using socket activation won't work, because systemd always does +## bind() on the socket, and also won't announce the ZeroConf service. +#ExecStart=/usr/sbin/mandos --foreground --socket=0 +#StandardInput=socket +# Restrict what the Mandos daemon can do. Note that this also affects +# "checker" programs! +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +CapabilityBoundingSet=CAP_KILL CAP_SETGID CAP_SETUID CAP_DAC_OVERRIDE CAP_NET_RAW +ProtectKernelTunables=yes +ProtectControlGroups=yes + +[Install] +WantedBy=multi-user.target === added file 'mandos.xml' --- mandos.xml 1970-01-01 00:00:00 +0000 +++ mandos.xml 2019-07-24 06:16:09 +0000 @@ -0,0 +1,806 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8 + + + + &COMMANDNAME; + + Gives encrypted passwords to authenticated Mandos clients + + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + + DESCRIPTION + + &COMMANDNAME; is a server daemon which + handles incoming request for passwords for a pre-defined list of + client host computers. For an introduction, see + intro + 8mandos. The Mandos server + uses Zeroconf to announce itself on the local network, and uses + TLS to communicate securely with and to authenticate the + clients. The Mandos server uses IPv6 to allow Mandos clients to + use IPv6 link-local addresses, since the clients will probably + not have any other addresses configured (see ). Any authenticated client is then given + the stored pre-encrypted password for that specific client. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OPTIONS + + + + + + + Show a help message and exit + + + + + + + NAME + + NAME + + + + + + + + + + + + + + + + + + + + + + + + + + Run the server’s self-tests. This includes any unit + tests, etc. + + + + + + + + + + + + + + + + Set the debugging log level. + LEVEL is a string, one of + CRITICAL, + ERROR, + WARNING, + INFO, or + DEBUG, in order of + increasing verbosity. The default level is + WARNING. + + + + + + + + + + + + + + + + + + + + + + + Directory to search for configuration files. Default is + /etc/mandos. See + mandos.conf + 5 and + mandos-clients.conf + 5. + + + + + + + + + Prints the program version and exit. + + + + + + + + + + See also . + + + + + + + + + + + + + + + + + See also . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OVERVIEW + + + This program is the server part. It is a normal server program + and will run in a normal system environment, not in an initial + RAM disk environment. + + + + + NETWORK PROTOCOL + + The Mandos server announces itself as a Zeroconf service of type + _mandos._tcp. The Mandos + client connects to the announced address and port, and sends a + line of text where the first whitespace-separated field is the + protocol version, which currently is + 1. The client and server then + start a TLS protocol handshake with a slight quirk: the Mandos + server program acts as a TLS client while the + connecting Mandos client acts as a TLS server. + The Mandos client must supply a TLS public key, and the key ID + of this public key is used by the Mandos server to look up (in a + list read from clients.conf at start time) + which binary blob to give the client. No other authentication + or authorization is done by the server. + + + Mandos Protocol (Version 1) + + Mandos Client + Direction + Mandos Server + + + + Connect + -> + + + 1\r\n + -> + + + TLS handshake as TLS server + + <-> + TLS handshake as TLS client + + + + Public key (part of TLS handshake) + -> + + + + <- + Binary blob (client will assume OpenPGP data) + + + + <- + Close + +
+
+ + + CHECKING + + The server will, by default, continually check that the clients + are still up. If a client has not been confirmed as being up + for some time, the client is assumed to be compromised and is no + longer eligible to receive the encrypted password. (Manual + intervention is required to re-enable a client.) The timeout, + extended timeout, checker program, and interval between checks + can be configured both globally and per client; see + mandos-clients.conf + 5. + + + + + APPROVAL + + The server can be configured to require manual approval for a + client before it is sent its secret. The delay to wait for such + approval and the default action (approve or deny) can be + configured both globally and per client; see + mandos-clients.conf + 5. By default all clients + will be approved immediately without delay. + + + This can be used to deny a client its secret if not manually + approved within a specified time. It can also be used to make + the server delay before giving a client its secret, allowing + optional manual denying of this specific client. + + + + + + LOGGING + + The server will send log message with various severity levels to + /dev/log. With the + option, it will log even more messages, + and also show them on the console. + + + + + PERSISTENT STATE + + Client settings, initially read from + clients.conf, are persistent across + restarts, and run-time changes will override settings in + clients.conf. However, if a setting is + changed (or a client added, or removed) in + clients.conf, this will take precedence. + + + + + D-BUS INTERFACE + + The server will by default provide a D-Bus system bus interface. + This interface will only be accessible by the root user or a + Mandos-specific user, if such a user exists. For documentation + of the D-Bus API, see the file DBUS-API. + + + + + EXIT STATUS + + The server will exit with a non-zero exit status only when a + critical error is encountered. + + + + + ENVIRONMENT + + + PATH + + + To start the configured checker (see ), the server uses + /bin/sh, which in turn uses + PATH to search for matching commands if + an absolute path is not given. See + sh1 + . + + + + + + + + FILES + + Use the option to change where + &COMMANDNAME; looks for its configurations + files. The default file names are listed here. + + + + /etc/mandos/mandos.conf + + + Server-global settings. See + mandos.conf + 5 for details. + + + + + /etc/mandos/clients.conf + + + List of clients and client-specific settings. See + mandos-clients.conf + 5 for details. + + + + + /run/mandos.pid + + + The file containing the process id of the + &COMMANDNAME; process started last. + Note: If the /run directory does not + exist, /var/run/mandos.pid will be + used instead. + + + + + /var/lib/mandos + + + Directory where persistent state will be saved. Change + this with the option. See + also the option. + + + + + /dev/log + + + The Unix domain socket to where local syslog messages are + sent. + + + + + /bin/sh + + + This is used to start the configured checker command for + each client. See + mandos-clients.conf + 5 for details. + + + + + + + + BUGS + + This server might, on especially fatal errors, emit a Python + backtrace. This could be considered a feature. + + + There is no fine-grained control over logging and debug output. + + + + + + EXAMPLE + + + Normal invocation needs no options: + + + &COMMANDNAME; + + + + + Run the server in debug mode, read configuration files from + the ~/mandos directory, + and use the Zeroconf service name Test to not + collide with any other official Mandos server on this host: + + + + +&COMMANDNAME; --debug --configdir ~/mandos --servicename Test + + + + + + Run the server normally, but only listen to one interface and + only on the link-local address on that interface: + + + + +&COMMANDNAME; --interface eth7 --address fe80::aede:48ff:fe71:f6f2 + + + + + + + SECURITY + + SERVER + + Running this &COMMANDNAME; server program + should not in itself present any security risk to the host + computer running it. The program switches to a non-root user + soon after startup. + + + + CLIENTS + + The server only gives out its stored data to clients which + does have the correct key ID of the stored key ID. This is + guaranteed by the fact that the client sends its public key in + the TLS handshake; this ensures it to be genuine. The server + computes the key ID of the key itself and looks up the key ID + in its list of clients. The clients.conf + file (see + mandos-clients.conf + 5) + must be made non-readable by anyone + except the user starting the server (usually root). + + + As detailed in , the status of all + client computers will continually be checked and be assumed + compromised if they are gone for too long. + + + For more details on client-side security, see + mandos-client + 8mandos. + + + + + + SEE ALSO + + intro + 8mandos, + mandos-clients.conf + 5, + mandos.conf + 5, + mandos-client + 8mandos, + sh + 1 + + + + + Zeroconf + + + + Zeroconf is the network protocol standard used by clients + for finding this Mandos server on the local network. + + + + + + Avahi + + + + Avahi is the library this server calls to implement + Zeroconf service announcements. + + + + + + GnuTLS + + + + GnuTLS is the library this server uses to implement TLS for + communicating securely with the client, and at the same time + confidently get the client’s public key. + + + + + + RFC 4291: IP Version 6 Addressing + Architecture + + + + + Section 2.2: Text Representation of + Addresses + + + + Section 2.5.5.2: IPv4-Mapped IPv6 + Address + + + + Section 2.5.6, Link-Local IPv6 Unicast + Addresses + + + The clients use IPv6 link-local addresses, which are + immediately usable since a link-local addresses is + automatically assigned to a network interfaces when it + is brought up. + + + + + + + + + RFC 5246: The Transport Layer Security (TLS) + Protocol Version 1.2 + + + + TLS 1.2 is the protocol implemented by GnuTLS. + + + + + + RFC 4880: OpenPGP Message Format + + + + The data sent to clients is binary encrypted OpenPGP data. + + + + + + RFC 7250: Using Raw Public Keys in Transport + Layer Security (TLS) and Datagram Transport Layer Security + (DTLS) + + + + This is implemented by GnuTLS version 3.6.6 and is, if + present, used by this server so that raw public keys can be + used. + + + + + + RFC 6091: Using OpenPGP Keys for Transport Layer + Security (TLS) Authentication + + + + This is implemented by GnuTLS before version 3.6.0 and is, + if present, used by this server so that OpenPGP keys can be + used. + + + + + +
+ + + + + === added directory 'network-hooks.d' === added file 'network-hooks.d/bridge' --- network-hooks.d/bridge 1970-01-01 00:00:00 +0000 +++ network-hooks.d/bridge 2018-02-08 10:23:55 +0000 @@ -0,0 +1,93 @@ +#!/bin/sh +# +# This is an example of a Mandos client network hook. This hook +# brings up a bridge interface as specified in a separate +# configuration file. To be used, this file and any needed +# configuration file(s) should be copied into the +# /etc/mandos/network-hooks.d directory. +# +# Copyright © 2012-2018 Teddy Hogeborn +# Copyright © 2012-2018 Björn Påhlsson +# +# 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. + +set -e + +CONFIG="$MANDOSNETHOOKDIR/bridge.conf" + +addrtoif(){ + grep -liFe "$1" /sys/class/net/*/address \ + | sed -e 's,.*/\([^/]*\)/[^/]*,\1,' -e "/^${BRIDGE}\$/d" +} + +# Read config file, which must set "BRIDGE", "PORT_ADDRESSES", and +# optionally "IPADDRS" and "ROUTES". +if [ -e "$CONFIG" ]; then + . "$CONFIG" +fi + +if [ -z "$BRIDGE" ] || [ -z "$PORT_ADDRESSES" ]; then + exit +fi + +if [ -n "$DEVICE" ]; then + case "$DEVICE" in + *,"$BRIDGE"|*,"$BRIDGE",*|"$BRIDGE",*|"$BRIDGE") :;; + *) exit;; + esac +fi + +brctl="/sbin/brctl" +for b in "$brctl" /usr/sbin/brctl; do + if [ -e "$b" ]; then + brctl="$b" + break + fi +done + +do_start(){ + "$brctl" addbr "$BRIDGE" + for address in $PORT_ADDRESSES; do + interface=`addrtoif "$address"` + "$brctl" addif "$BRIDGE" "$interface" + ip link set dev "$interface" up + done + ip link set dev "$BRIDGE" up + sleep "${DELAY%%.*}" + if [ -n "$IPADDRS" ]; then + for ipaddr in $IPADDRS; do + ip addr add "$ipaddr" dev "$BRIDGE" + done + fi + if [ -n "$ROUTES" ]; then + for route in $ROUTES; do + ip route add "$route" dev "$BRIDGE" + done + fi +} + +do_stop(){ + ip link set dev "$BRIDGE" down + for address in $PORT_ADDRESSES; do + interface=`addrtoif "$address"` + ip link set dev "$interface" down + "$brctl" delif "$BRIDGE" "$interface" + done + "$brctl" delbr "$BRIDGE" +} + +case "${MODE:-$1}" in + start|stop) + do_"${MODE:-$1}" + ;; + files) + echo /bin/ip + echo "$brctl" + ;; + modules) + echo bridge + ;; +esac === added file 'network-hooks.d/bridge.conf' --- network-hooks.d/bridge.conf 1970-01-01 00:00:00 +0000 +++ network-hooks.d/bridge.conf 2011-12-31 13:25:58 +0000 @@ -0,0 +1,11 @@ +## Required + +#BRIDGE=br0 + +#PORT_ADDRESSES="00:11:22:33:44:55 11:22:33:44:55:66" + +## Optional + +#IPADDRS="192.0.2.3/24 2001:DB8::aede:48ff:fe71:f6f2/32" + +#ROUTES="192.0.2.0/24 2001:DB8::/32" === added file 'network-hooks.d/openvpn' --- network-hooks.d/openvpn 1970-01-01 00:00:00 +0000 +++ network-hooks.d/openvpn 2018-02-08 10:23:55 +0000 @@ -0,0 +1,66 @@ +#!/bin/sh +# +# This is an example of a Mandos client network hook. This hook +# brings up an OpenVPN interface as specified in a separate +# configuration file. To be used, this file and any needed +# configuration file(s) should be copied into the +# /etc/mandos/network-hooks.d directory. +# +# Copyright © 2012-2018 Teddy Hogeborn +# Copyright © 2012-2018 Björn Påhlsson +# +# 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. + +set -e + +CONFIG="openvpn.conf" + +# Extract the "dev" setting from the config file +VPNDEVICE=`sed -n -e 's/[[:space:]]#.*//' \ + -e 's/^[[:space:]]*dev[[:space:]]\+//p' \ + "$MANDOSNETHOOKDIR/$CONFIG"` + +PIDFILE=/run/openvpn-mandos.pid + +# Exit if no device set in config +if [ -z "$VPNDEVICE" ]; then + exit +fi + +# Exit if DEVICE is set and it doesn't match the VPN interface +if [ -n "$DEVICE" ]; then + case "$DEVICE" in + *,"$VPNDEVICE"*|"$VPNDEVICE"*) :;; + *) exit;; + esac +fi + +openvpn=/usr/sbin/openvpn + +do_start(){ + "$openvpn" --cd "$MANDOSNETHOOKDIR" --daemon 'openvpn(Mandos)' \ + --writepid "$PIDFILE" --config "$CONFIG" + sleep "$DELAY" +} + +do_stop(){ + PID="`cat \"$PIDFILE\"`" + if [ "$PID" -gt 0 ]; then + kill "$PID" + fi +} + +case "${MODE:-$1}" in + start|stop) + do_"${MODE:-$1}" + ;; + files) + echo "$openvpn" + ;; + modules) + echo tun + ;; +esac === added file 'network-hooks.d/openvpn.conf' --- network-hooks.d/openvpn.conf 1970-01-01 00:00:00 +0000 +++ network-hooks.d/openvpn.conf 2011-12-02 16:52:50 +0000 @@ -0,0 +1,19 @@ +# Sample OpenVPN configuration file +# Uncomment and change - see openvpn(8) + +# Network device. +#dev tun + +# Our remote peer +#remote 192.0.2.3 +#float 192.0.2.3 +#port 1194 + +# VPN endpoints +#ifconfig 10.1.0.1 10.1.0.2 + +# A pre-shared static key +#secret openvpn.key + +# Cipher +#cipher AES-128-CBC === added file 'network-hooks.d/wireless' --- network-hooks.d/wireless 1970-01-01 00:00:00 +0000 +++ network-hooks.d/wireless 2018-02-08 10:23:55 +0000 @@ -0,0 +1,165 @@ +#!/bin/sh +# +# This is an example of a Mandos client network hook. This hook +# brings up a wireless interface as specified in a separate +# configuration file. To be used, this file and any needed +# configuration file(s) should be copied into the +# /etc/mandos/network-hooks.d directory. +# +# Copyright © 2012-2018 Teddy Hogeborn +# Copyright © 2012-2018 Björn Påhlsson +# +# 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. + +set -e + +RUNDIR="/run" +CTRL="$RUNDIR/wpa_supplicant-global" +CTRLDIR="$RUNDIR/wpa_supplicant" +PIDFILE="$RUNDIR/wpa_supplicant-mandos.pid" + +CONFIG="$MANDOSNETHOOKDIR/wireless.conf" + +addrtoif(){ + grep -liFe "$1" /sys/class/net/*/address \ + | sed -e 's,.*/\([^/]*\)/[^/]*,\1,' +} + +# Read config file +if [ -e "$CONFIG" ]; then + . "$CONFIG" +else + exit +fi + +ifkeys=`sed -n -e 's/^ADDRESS_\([^=]*\)=.*/\1/p' "$CONFIG" | sort -u` + +# Exit if DEVICE is set and is not any of the wireless interfaces +if [ -n "$DEVICE" ]; then + while :; do + for KEY in $ifkeys; do + ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"` + INTERFACE=`addrtoif "$ADDRESS"` + + case "$DEVICE" in + *,"$INTERFACE"|*,"$INTERFACE",*|"$INTERFACE",*|"$INTERFACE") + break 2;; + esac + done + exit + done +fi + +wpa_supplicant=/sbin/wpa_supplicant +wpa_cli=/sbin/wpa_cli +ip=/bin/ip + +# Used by the wpa_interface_* functions in the wireless.conf file +wpa_cli_set(){ + case "$1" in + ssid|psk) arg="\"$2\"" ;; + *) arg="$2" ;; + esac + "$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" set_network "$NETWORK" \ + "$1" "$arg" 2>&1 | sed -e '/^OK$/d' +} + +if [ $VERBOSITY -gt 0 ]; then + WPAS_OPTIONS="-d $WPAS_OPTIONS" +fi +if [ -n "$PIDFILE" ]; then + WPAS_OPTIONS="-P$PIDFILE $WPAS_OPTIONS" +fi + +do_start(){ + mkdir -m u=rwx,go= -p "$CTRLDIR" + "$wpa_supplicant" -B -g "$CTRL" -p "$CTRLDIR" $WPAS_OPTIONS + for KEY in $ifkeys; do + ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"` + INTERFACE=`addrtoif "$ADDRESS"` + DRIVER=`eval 'echo "$WPA_DRIVER_'"$KEY"\"` + IFDELAY=`eval 'echo "$DELAY_'"$KEY"\"` + "$wpa_cli" -g "$CTRL" interface_add "$INTERFACE" "" \ + "${DRIVER:-wext}" "$CTRLDIR" > /dev/null \ + | sed -e '/^OK$/d' + NETWORK=`"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" add_network` + eval wpa_interface_"$KEY" + "$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" enable_network \ + "$NETWORK" | sed -e '/^OK$/d' + sleep "${IFDELAY:-$DELAY}" & + sleep=$! + while :; do + kill -0 $sleep 2>/dev/null || break + STATE=`"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" status \ + | sed -n -e 's/^wpa_state=//p'` + if [ "$STATE" = COMPLETED ]; then + while :; do + kill -0 $sleep 2>/dev/null || break 2 + UP=`cat /sys/class/net/"$INTERFACE"/operstate` + if [ "$UP" = up ]; then + kill $sleep 2>/dev/null + break 2 + fi + sleep 1 + done + fi + sleep 1 + done & + wait $sleep || : + IPADDRS=`eval 'echo "$IPADDRS_'"$KEY"\"` + if [ -n "$IPADDRS" ]; then + if [ "$IPADDRS" = dhcp ]; then + ipconfig -c dhcp -d "$INTERFACE" || : + #dhclient "$INTERFACE" + else + for ipaddr in $IPADDRS; do + "$ip" addr add "$ipaddr" dev "$INTERFACE" + done + fi + fi + ROUTES=`eval 'echo "$ROUTES_'"$KEY"\"` + if [ -n "$ROUTES" ]; then + for route in $ROUTES; do + "$ip" route add "$route" dev "$INTERFACE" + done + fi + done +} + +do_stop(){ + "$wpa_cli" -g "$CTRL" terminate 2>&1 | sed -e '/^OK$/d' + for KEY in $ifkeys; do + ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"` + INTERFACE=`addrtoif "$ADDRESS"` + "$ip" addr show scope global permanent dev "$INTERFACE" \ + | while read type addr rest; do + case "$type" in + inet|inet6) + "$ip" addr del "$addr" dev "$INTERFACE" + ;; + esac + done + "$ip" link set dev "$INTERFACE" down + done +} + +case "${MODE:-$1}" in + start|stop) + do_"${MODE:-$1}" + ;; + files) + echo "$wpa_supplicant" + echo "$wpa_cli" + echo "$ip" + ;; + modules) + if [ "$IPADDRS" = dhcp ]; then + echo af_packet + fi + sed -n -e 's/#.*$//' -e 's/[ ]*$//' \ + -e 's/^MODULE_[^=]\+=//p' "$CONFIG" + ;; +esac === added file 'network-hooks.d/wireless.conf' --- network-hooks.d/wireless.conf 1970-01-01 00:00:00 +0000 +++ network-hooks.d/wireless.conf 2011-12-31 13:25:58 +0000 @@ -0,0 +1,23 @@ +# Extra options for wpa_supplicant, if any +#WPAS_OPTIONS="" + +# wlan0 +ADDRESS_0=00:11:22:33:44:55 +MODULE_0=ath9k +#WPA_DRIVER_0=wext +wpa_interface_0(){ + # Use this format to set simple things: + wpa_cli_set ssid home + wpa_cli_set psk "secret passphrase" + # Use this format to do more complex things with wpa_cli: + #"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" bssid "$NETWORK" 00:11:22:33:44:55 + #"$wpa_cli" -g "$CTRL" ping +} +#DELAY_0=10 +IPADDRS_0=dhcp +#IPADDRS_0="192.0.2.3/24 2001:DB8::aede:48ff:fe71:f6f2/32" +#ROUTES_0="192.0.2.0/24 2001:DB8::/32" + +#ADDRESS_1=11:22:33:44:55:66 +#MODULE_1=... +#... === added file 'overview.xml' --- overview.xml 1970-01-01 00:00:00 +0000 +++ overview.xml 2019-02-09 23:23:26 +0000 @@ -0,0 +1,17 @@ + + + + This is part of the Mandos system for allowing computers to have + encrypted root file systems and at the same time be capable of + remote and/or unattended reboots. The computers run a small client + program in the initial RAM disk environment which + will communicate with a server over a network. All network + communication is encrypted using TLS. The + clients are identified by the server using a TLS key; each client + has one unique to it. The server sends the clients an encrypted + password. The encrypted password is decrypted by the clients using + a separate OpenPGP key, and the password is then used to unlock the + root file system, whereupon the computers can continue booting + normally. + === added directory 'plugin-helpers' === added file 'plugin-helpers/mandos-client-iprouteadddel.c' --- plugin-helpers/mandos-client-iprouteadddel.c 1970-01-01 00:00:00 +0000 +++ plugin-helpers/mandos-client-iprouteadddel.c 2018-02-18 01:29:21 +0000 @@ -0,0 +1,282 @@ +/* -*- coding: utf-8 -*- */ +/* + * iprouteadddel - Add or delete direct route to a local IP address + * + * Copyright © 2015-2018 Teddy Hogeborn + * Copyright © 2015-2018 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos 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. + * + * Mandos 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 Mandos. If not, see . + * + * Contact the authors at . + */ + +#define _GNU_SOURCE /* program_invocation_short_name */ +#include /* bool, false, true */ +#include /* fprintf(), stderr, FILE, vfprintf */ +#include /* program_invocation_short_name, + errno, perror(), EINVAL, ENOMEM */ +#include /* va_list, va_start */ +#include /* EXIT_SUCCESS */ +#include /* struct argp_option, error_t, struct + argp_state, ARGP_KEY_ARG, + argp_usage(), ARGP_KEY_END, + ARGP_ERR_UNKNOWN, struct argp, + argp_parse() */ +#include /* EX_USAGE, EX_OSERR */ +#include /* sa_family_t, AF_INET6, AF_INET */ +#include /* PRIdMAX, intmax_t */ + +#include /* struct nl_addr, nl_addr_parse(), + nl_geterror(), + nl_addr_get_family(), + nl_addr_put() */ +#include /* struct rtnl_route, + struct rtnl_nexthop, + rtnl_route_alloc(), + rtnl_route_set_family(), + rtnl_route_set_protocol(), + RTPROT_BOOT, + rtnl_route_set_scope(), + RT_SCOPE_LINK, + rtnl_route_set_type(), + RTN_UNICAST, + rtnl_route_set_dst(), + rtnl_route_set_table(), + RT_TABLE_MAIN, + rtnl_route_nh_alloc(), + rtnl_route_nh_set_ifindex(), + rtnl_route_add_nexthop(), + rtnl_route_add(), + rtnl_route_delete(), + rtnl_route_put(), + rtnl_route_nh_free() */ +#include /* struct nl_sock, nl_socket_alloc(), + nl_connect(), nl_socket_free() */ +#include /* rtnl_link_get_kernel(), + rtnl_link_get_ifindex(), + rtnl_link_put() */ + +bool debug = false; +const char *argp_program_version = "mandos-client-iprouteadddel " VERSION; +const char *argp_program_bug_address = ""; + +/* Function to use when printing errors */ +void perror_plus(const char *print_text){ + int e = errno; + fprintf(stderr, "Mandos plugin helper %s: ", + program_invocation_short_name); + errno = e; + perror(print_text); +} + +__attribute__((format (gnu_printf, 2, 3), nonnull)) +int fprintf_plus(FILE *stream, const char *format, ...){ + va_list ap; + va_start (ap, format); + + fprintf(stream, "Mandos plugin helper %s: ", + program_invocation_short_name); + return vfprintf(stream, format, ap); +} + +int main(int argc, char *argv[]){ + int ret; + int exitcode = EXIT_SUCCESS; + struct arguments { + bool add; /* true: add, false: delete */ + char *address; /* IP address as string */ + struct nl_addr *nl_addr; /* Netlink IP address */ + char *interface; /* interface name */ + } arguments = { .add = true, .address = NULL, .interface = NULL }; + struct argp_option options[] = { + { .name = "debug", .key = 128, + .doc = "Debug mode" }, + { .name = NULL } + }; + struct rtnl_route *route = NULL; + struct rtnl_nexthop *nexthop = NULL; + struct nl_sock *sk = NULL; + + error_t parse_opt(int key, char *arg, struct argp_state *state){ + int lret; + errno = 0; + switch(key){ + case 128: /* --debug */ + debug = true; + break; + case ARGP_KEY_ARG: + switch(state->arg_num){ + case 0: + if(strcasecmp(arg, "add") == 0){ + ((struct arguments *)(state->input))->add = true; + } else if(strcasecmp(arg, "delete") == 0){ + ((struct arguments *)(state->input))->add = false; + } else { + fprintf_plus(stderr, "Unrecognized command: %s\n", arg); + argp_usage(state); + } + break; + case 1: + ((struct arguments *)(state->input))->address = arg; + lret = nl_addr_parse(arg, AF_UNSPEC, &(((struct arguments *) + (state->input)) + ->nl_addr)); + if(lret != 0){ + fprintf_plus(stderr, "Failed to parse address %s: %s\n", + arg, nl_geterror(lret)); + argp_usage(state); + } + break; + case 2: + ((struct arguments *)(state->input))->interface = arg; + break; + default: + argp_usage(state); + } + break; + case ARGP_KEY_END: + if(state->arg_num < 3){ + argp_usage(state); + } + break; + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + struct argp argp = { .options = options, .parser = parse_opt, + .args_doc = "[ add | delete ] ADDRESS INTERFACE", + .doc = "Mandos client helper -- Add or delete" + " local route to IP address on interface" }; + + ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, 0, &arguments); + switch(ret){ + case 0: + break; + case EINVAL: + exit(EX_USAGE); + case ENOMEM: + default: + errno = ret; + perror_plus("argp_parse"); + exitcode = EX_OSERR; + goto end; + } + /* Get netlink socket */ + sk = nl_socket_alloc(); + if(sk == NULL){ + fprintf_plus(stderr, "Failed to allocate netlink socket: %s\n", + nl_geterror(ret)); + exitcode = EX_OSERR; + goto end; + } + /* Connect socket to netlink */ + ret = nl_connect(sk, NETLINK_ROUTE); + if(ret < 0){ + fprintf_plus(stderr, "Failed to connect socket to netlink: %s\n", + nl_geterror(ret)); + exitcode = EX_OSERR; + goto end; + } + /* Get link object of specified interface */ + struct rtnl_link *link = NULL; + ret = rtnl_link_get_kernel(sk, 0, arguments.interface, &link); + if(ret < 0){ + fprintf_plus(stderr, "Failed to use interface %s: %s\n", + arguments.interface, nl_geterror(ret)); + exitcode = EX_OSERR; + goto end; + } + /* Get netlink route object */ + route = rtnl_route_alloc(); + if(route == NULL){ + fprintf_plus(stderr, "Failed to get netlink route:\n"); + exitcode = EX_OSERR; + goto end; + } + /* Get address family of specified address */ + sa_family_t af = (sa_family_t)nl_addr_get_family(arguments.nl_addr); + if(debug){ + fprintf_plus(stderr, "Address family of %s is %s (%" PRIdMAX + ")\n", arguments.address, + af == AF_INET6 ? "AF_INET6" : + ( af == AF_INET ? "AF_INET" : "UNKNOWN"), + (intmax_t)af); + } + /* Set route parameters: */ + rtnl_route_set_family(route, (uint8_t)af); /* Address family */ + rtnl_route_set_protocol(route, RTPROT_BOOT); /* protocol - see + ip-route(8) */ + rtnl_route_set_scope(route, RT_SCOPE_LINK); /* link scope */ + rtnl_route_set_type(route, RTN_UNICAST); /* normal unicast + address route */ + rtnl_route_set_dst(route, arguments.nl_addr); /* Destination + address */ + rtnl_route_set_table(route, RT_TABLE_MAIN); /* "main" routing + table */ + /* Create nexthop */ + nexthop = rtnl_route_nh_alloc(); + if(nexthop == NULL){ + fprintf_plus(stderr, "Failed to get netlink route nexthop\n"); + exitcode = EX_OSERR; + goto end; + } + /* Get index number of specified interface */ + int ifindex = rtnl_link_get_ifindex(link); + if(debug){ + fprintf_plus(stderr, "ifindex of %s is %d\n", arguments.interface, + ifindex); + } + /* Set interface index number on nexthop object */ + rtnl_route_nh_set_ifindex(nexthop, ifindex); + /* Set route to use nexthop object */ + rtnl_route_add_nexthop(route, nexthop); + /* Add or delete route? */ + if(arguments.add){ + ret = rtnl_route_add(sk, route, NLM_F_EXCL); + } else { + ret = rtnl_route_delete(sk, route, 0); + } + if(ret < 0){ + fprintf_plus(stderr, "Failed to %s route: %s\n", + arguments.add ? "add" : "delete", + nl_geterror(ret)); + exitcode = EX_OSERR; + goto end; + } + end: + /* Deallocate route */ + if(route){ + rtnl_route_put(route); + } else if(nexthop) { + /* Deallocate route nexthop */ + rtnl_route_nh_free(nexthop); + } + /* Deallocate parsed address */ + if(arguments.nl_addr){ + nl_addr_put(arguments.nl_addr); + } + /* Deallocate link struct */ + if(link){ + rtnl_link_put(link); + } + /* Deallocate netlink socket struct */ + if(sk){ + nl_socket_free(sk); + } + return exitcode; +} === renamed file 'plugbasedclient.c' => 'plugin-runner.c' --- plugbasedclient.c 2008-07-21 15:34:44 +0000 +++ plugin-runner.c 2019-07-07 20:50:21 +0000 @@ -1,214 +1,1331 @@ -/* -*- coding: utf-8 -*- */ +/* -*- coding: utf-8; mode: c; mode: orgtbl -*- */ /* * Mandos plugin runner - Run Mandos plugins * - * Copyright © 2007-2008 Teddy Hogeborn and Björn Påhlsson. - * - * 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 + * Copyright © 2008-2018 Teddy Hogeborn + * Copyright © 2008-2018 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos 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. + * + * Mandos 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 - * . + * along with Mandos. If not, see . * - * Contact the authors at and - * . + * Contact the authors at . */ -#define _FORTIFY_SOURCE 2 - -#include /* popen, fileno */ -#include /* and, or, not */ -#include /* DIR, opendir, stat, struct stat, waitpid, - WIFEXITED, WEXITSTATUS, wait */ -#include /* wait */ -#include /* DIR, opendir */ -#include /* stat, struct stat */ -#include /* stat, struct stat, chdir */ -#include /* EXIT_FAILURE */ -#include /* fd_set, select, FD_ZERO, FD_SET, - FD_ISSET */ -#include /* strlen, strcpy, strcat */ -#include /* true */ -#include /* waitpid, WIFEXITED, WEXITSTATUS */ -#include /* errno */ - -struct process; - -typedef struct process{ +#define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), getline(), + O_CLOEXEC, pipe2() */ +#include /* size_t, NULL */ +#include /* malloc(), exit(), EXIT_SUCCESS, + realloc() */ +#include /* bool, true, false */ +#include /* fileno(), fprintf(), + stderr, STDOUT_FILENO, fclose() */ +#include /* fstat(), struct stat, waitpid(), + WIFEXITED(), WEXITSTATUS(), wait(), + pid_t, uid_t, gid_t, getuid(), + getgid() */ +#include /* fd_set, select(), FD_ZERO(), + FD_SET(), FD_ISSET(), FD_CLR */ +#include /* wait(), waitpid(), WIFEXITED(), + WEXITSTATUS(), WTERMSIG() */ +#include /* struct stat, fstat(), S_ISREG() */ +#include /* and, or, not */ +#include /* struct dirent, scandirat() */ +#include /* fcntl(), F_GETFD, F_SETFD, + FD_CLOEXEC, write(), STDOUT_FILENO, + struct stat, fstat(), close(), + setgid(), setuid(), S_ISREG(), + faccessat() pipe2(), fork(), + _exit(), dup2(), fexecve(), read() + */ +#include /* fcntl(), F_GETFD, F_SETFD, + FD_CLOEXEC, openat(), scandirat(), + pipe2() */ +#include /* strsep, strlen(), strsignal(), + strcmp(), strncmp() */ +#include /* errno */ +#include /* struct argp_option, struct + argp_state, struct argp, + argp_parse(), ARGP_ERR_UNKNOWN, + ARGP_KEY_END, ARGP_KEY_ARG, + error_t */ +#include /* struct sigaction, sigemptyset(), + sigaddset(), sigaction(), + sigprocmask(), SIG_BLOCK, SIGCHLD, + SIG_UNBLOCK, kill(), sig_atomic_t + */ +#include /* errno, EBADF */ +#include /* intmax_t, PRIdMAX, strtoimax() */ +#include /* EX_OSERR, EX_USAGE, EX_IOERR, + EX_CONFIG, EX_UNAVAILABLE, EX_OK */ +#include /* errno */ +#include /* error() */ +#include /* fnmatch() */ + +#define BUFFER_SIZE 256 + +#define PDIR "/lib/mandos/plugins.d" +#define PHDIR "/lib/mandos/plugin-helpers" +#define AFILE "/conf/conf.d/mandos/plugin-runner.conf" + +const char *argp_program_version = "plugin-runner " VERSION; +const char *argp_program_bug_address = ""; + +typedef struct plugin{ + char *name; /* can be NULL or any plugin name */ + char **argv; + int argc; + char **environ; + int envc; + bool disabled; + + /* Variables used for running processes*/ pid_t pid; int fd; char *buffer; size_t buffer_size; size_t buffer_length; - struct process *next; -} process; - -#define BUFFER_SIZE 256 + bool eof; + volatile sig_atomic_t completed; + int status; + struct plugin *next; +} plugin; + +static plugin *plugin_list = NULL; + +/* Gets an existing plugin based on name, + or if none is found, creates a new one */ +__attribute__((warn_unused_result)) +static plugin *getplugin(char *name){ + /* Check for existing plugin with that name */ + for(plugin *p = plugin_list; p != NULL; p = p->next){ + if((p->name == name) + or (p->name and name and (strcmp(p->name, name) == 0))){ + return p; + } + } + /* Create a new plugin */ + plugin *new_plugin = NULL; + do { + new_plugin = malloc(sizeof(plugin)); + } while(new_plugin == NULL and errno == EINTR); + if(new_plugin == NULL){ + return NULL; + } + char *copy_name = NULL; + if(name != NULL){ + do { + copy_name = strdup(name); + } while(copy_name == NULL and errno == EINTR); + if(copy_name == NULL){ + int e = errno; + free(new_plugin); + errno = e; + return NULL; + } + } + + *new_plugin = (plugin){ .name = copy_name, + .argc = 1, + .disabled = false, + .next = plugin_list }; + + do { + new_plugin->argv = malloc(sizeof(char *) * 2); + } while(new_plugin->argv == NULL and errno == EINTR); + if(new_plugin->argv == NULL){ + int e = errno; + free(copy_name); + free(new_plugin); + errno = e; + return NULL; + } + new_plugin->argv[0] = copy_name; + new_plugin->argv[1] = NULL; + + do { + new_plugin->environ = malloc(sizeof(char *)); + } while(new_plugin->environ == NULL and errno == EINTR); + if(new_plugin->environ == NULL){ + int e = errno; + free(copy_name); + free(new_plugin->argv); + free(new_plugin); + errno = e; + return NULL; + } + new_plugin->environ[0] = NULL; + + /* Append the new plugin to the list */ + plugin_list = new_plugin; + return new_plugin; +} + +/* Helper function for add_argument and add_environment */ +__attribute__((nonnull, warn_unused_result)) +static bool add_to_char_array(const char *new, char ***array, + int *len){ + /* Resize the pointed-to array to hold one more pointer */ + char **new_array = NULL; + do { + new_array = realloc(*array, sizeof(char *) + * (size_t) ((*len) + 2)); + } while(new_array == NULL and errno == EINTR); + /* Malloc check */ + if(new_array == NULL){ + return false; + } + *array = new_array; + /* Make a copy of the new string */ + char *copy; + do { + copy = strdup(new); + } while(copy == NULL and errno == EINTR); + if(copy == NULL){ + return false; + } + /* Insert the copy */ + (*array)[*len] = copy; + (*len)++; + /* Add a new terminating NULL pointer to the last element */ + (*array)[*len] = NULL; + return true; +} + +/* Add to a plugin's argument vector */ +__attribute__((nonnull(2), warn_unused_result)) +static bool add_argument(plugin *p, const char *arg){ + if(p == NULL){ + return false; + } + return add_to_char_array(arg, &(p->argv), &(p->argc)); +} + +/* Add to a plugin's environment */ +__attribute__((nonnull(2), warn_unused_result)) +static bool add_environment(plugin *p, const char *def, bool replace){ + if(p == NULL){ + return false; + } + /* namelen = length of name of environment variable */ + size_t namelen = (size_t)(strchrnul(def, '=') - def); + /* Search for this environment variable */ + for(char **envdef = p->environ; *envdef != NULL; envdef++){ + if(strncmp(*envdef, def, namelen + 1) == 0){ + /* It already exists */ + if(replace){ + char *new_envdef; + do { + new_envdef = realloc(*envdef, strlen(def) + 1); + } while(new_envdef == NULL and errno == EINTR); + if(new_envdef == NULL){ + return false; + } + *envdef = new_envdef; + strcpy(*envdef, def); + } + return true; + } + } + return add_to_char_array(def, &(p->environ), &(p->envc)); +} + +#ifndef O_CLOEXEC +/* + * Based on the example in the GNU LibC manual chapter 13.13 "File + * Descriptor Flags". + | [[info:libc:Descriptor%20Flags][File Descriptor Flags]] | + */ +__attribute__((warn_unused_result)) +static int set_cloexec_flag(int fd){ + int ret = (int)TEMP_FAILURE_RETRY(fcntl(fd, F_GETFD, 0)); + /* If reading the flags failed, return error indication now. */ + if(ret < 0){ + return ret; + } + /* Store modified flag word in the descriptor. */ + return (int)TEMP_FAILURE_RETRY(fcntl(fd, F_SETFD, + ret | FD_CLOEXEC)); +} +#endif /* not O_CLOEXEC */ + + +/* Mark processes as completed when they exit, and save their exit + status. */ +static void handle_sigchld(__attribute__((unused)) int sig){ + int old_errno = errno; + while(true){ + plugin *proc = plugin_list; + int status; + pid_t pid = waitpid(-1, &status, WNOHANG); + if(pid == 0){ + /* Only still running child processes */ + break; + } + if(pid == -1){ + if(errno == ECHILD){ + /* No child processes */ + break; + } + error(0, errno, "waitpid"); + } + + /* A child exited, find it in process_list */ + while(proc != NULL and proc->pid != pid){ + proc = proc->next; + } + if(proc == NULL){ + /* Process not found in process list */ + continue; + } + proc->status = status; + proc->completed = 1; + } + errno = old_errno; +} + +/* Prints out a password to stdout */ +__attribute__((nonnull, warn_unused_result)) +static bool print_out_password(const char *buffer, size_t length){ + ssize_t ret; + for(size_t written = 0; written < length; written += (size_t)ret){ + ret = TEMP_FAILURE_RETRY(write(STDOUT_FILENO, buffer + written, + length - written)); + if(ret < 0){ + return false; + } + } + return true; +} + +/* Removes and free a plugin from the plugin list */ +__attribute__((nonnull)) +static void free_plugin(plugin *plugin_node){ + + for(char **arg = (plugin_node->argv)+1; *arg != NULL; arg++){ + free(*arg); + } + free(plugin_node->name); + free(plugin_node->argv); + for(char **env = plugin_node->environ; *env != NULL; env++){ + free(*env); + } + free(plugin_node->environ); + free(plugin_node->buffer); + + /* Removes the plugin from the singly-linked list */ + if(plugin_node == plugin_list){ + /* First one - simple */ + plugin_list = plugin_list->next; + } else { + /* Second one or later */ + for(plugin *p = plugin_list; p != NULL; p = p->next){ + if(p->next == plugin_node){ + p->next = plugin_node->next; + break; + } + } + } + + free(plugin_node); +} + +static void free_plugin_list(void){ + while(plugin_list != NULL){ + free_plugin(plugin_list); + } +} int main(int argc, char *argv[]){ - char plugindir[] = "plugins.d"; - size_t d_name_len, plugindir_len = sizeof(plugindir)-1; - DIR *dir; - struct dirent *dirst; + char *plugindir = NULL; + char *pluginhelperdir = NULL; + char *argfile = NULL; + FILE *conffp; + struct dirent **direntries = NULL; struct stat st; - fd_set rfds_orig; + fd_set rfds_all; int ret, maxfd = 0; - process *process_list = NULL; - - dir = opendir(plugindir); - - if(dir == NULL){ - fprintf(stderr, "Can not open directory\n"); - return EXIT_FAILURE; - } - - FD_ZERO(&rfds_orig); - - while(true){ - dirst = readdir(dir); - - // All directory entries have been processed - if(dirst == NULL){ - break; - } - - d_name_len = strlen(dirst->d_name); - - // Ignore dotfiles and backup files - if (dirst->d_name[0] == '.' - or dirst->d_name[d_name_len - 1] == '~'){ - continue; - } - - char *filename = malloc(d_name_len + plugindir_len + 2); - strcpy(filename, plugindir); - strcat(filename, "/"); - strcat(filename, dirst->d_name); - - stat(filename, &st); - - if (S_ISREG(st.st_mode) and (access(filename, X_OK) == 0)){ - // Starting a new process to be watched - process *new_process = malloc(sizeof(process)); - int pipefd[2]; - pipe(pipefd); - new_process->pid = fork(); - if(new_process->pid == 0){ - /* this is the child process */ - closedir(dir); - close(pipefd[0]); /* close unused read end of pipe */ - dup2(pipefd[1], STDOUT_FILENO); /* replace our stdout */ - /* create a new modified argument list */ - char **new_argv = malloc(sizeof(char *) - * ((unsigned int) argc + 1)); - new_argv[0] = filename; - for(int i = 1; i < argc; i++){ - new_argv[i] = argv[i]; - } - new_argv[argc] = NULL; - if(execv(filename, new_argv) < 0){ - perror(argv[0]); - close(pipefd[1]); - exit(EXIT_FAILURE); - } - /* no return */ - } - close(pipefd[1]); /* close unused write end of pipe */ - new_process->fd = pipefd[0]; - new_process->buffer = malloc(BUFFER_SIZE); - if (new_process->buffer == NULL){ - perror(argv[0]); - goto end; - } - new_process->buffer_size = BUFFER_SIZE; - new_process->buffer_length = 0; - FD_SET(new_process->fd, &rfds_orig); - - if (maxfd < new_process->fd){ - maxfd = new_process->fd; - } - - //List handling - new_process->next = process_list; - process_list = new_process; - } - } - - closedir(dir); - - if (process_list != NULL){ + ssize_t sret; + uid_t uid = 65534; + gid_t gid = 65534; + bool debug = false; + int exitstatus = EXIT_SUCCESS; + struct sigaction old_sigchld_action; + struct sigaction sigchld_action = { .sa_handler = handle_sigchld, + .sa_flags = SA_NOCLDSTOP }; + char **custom_argv = NULL; + int custom_argc = 0; + int dir_fd = -1; + + /* Establish a signal handler */ + sigemptyset(&sigchld_action.sa_mask); + ret = sigaddset(&sigchld_action.sa_mask, SIGCHLD); + if(ret == -1){ + error(0, errno, "sigaddset"); + exitstatus = EX_OSERR; + goto fallback; + } + ret = sigaction(SIGCHLD, &sigchld_action, &old_sigchld_action); + if(ret == -1){ + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto fallback; + } + + /* The options we understand. */ + struct argp_option options[] = { + { .name = "global-options", .key = 'g', + .arg = "OPTION[,OPTION[,...]]", + .doc = "Options passed to all plugins" }, + { .name = "global-env", .key = 'G', + .arg = "VAR=value", + .doc = "Environment variable passed to all plugins" }, + { .name = "options-for", .key = 'o', + .arg = "PLUGIN:OPTION[,OPTION[,...]]", + .doc = "Options passed only to specified plugin" }, + { .name = "env-for", .key = 'E', + .arg = "PLUGIN:ENV=value", + .doc = "Environment variable passed to specified plugin" }, + { .name = "disable", .key = 'd', + .arg = "PLUGIN", + .doc = "Disable a specific plugin", .group = 1 }, + { .name = "enable", .key = 'e', + .arg = "PLUGIN", + .doc = "Enable a specific plugin", .group = 1 }, + { .name = "plugin-dir", .key = 128, + .arg = "DIRECTORY", + .doc = "Specify a different plugin directory", .group = 2 }, + { .name = "config-file", .key = 129, + .arg = "FILE", + .doc = "Specify a different configuration file", .group = 2 }, + { .name = "userid", .key = 130, + .arg = "ID", .flags = 0, + .doc = "User ID the plugins will run as", .group = 3 }, + { .name = "groupid", .key = 131, + .arg = "ID", .flags = 0, + .doc = "Group ID the plugins will run as", .group = 3 }, + { .name = "debug", .key = 132, + .doc = "Debug mode", .group = 4 }, + { .name = "plugin-helper-dir", .key = 133, + .arg = "DIRECTORY", + .doc = "Specify a different plugin helper directory", + .group = 2 }, + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + { .name = "help", .key = '?', + .doc = "Give this help list", .group = -1 }, + { .name = "usage", .key = -3, + .doc = "Give a short usage message", .group = -1 }, + { .name = "version", .key = 'V', + .doc = "Print program version", .group = -1 }, + { .name = NULL } + }; + + __attribute__((nonnull(3))) + error_t parse_opt(int key, char *arg, struct argp_state *state){ + errno = 0; + switch(key){ + char *tmp; + intmax_t tmp_id; + case 'g': /* --global-options */ + { + char *plugin_option; + while((plugin_option = strsep(&arg, ",")) != NULL){ + if(not add_argument(getplugin(NULL), plugin_option)){ + break; + } + } + errno = 0; + } + break; + case 'G': /* --global-env */ + if(add_environment(getplugin(NULL), arg, true)){ + errno = 0; + } + break; + case 'o': /* --options-for */ + { + char *option_list = strchr(arg, ':'); + if(option_list == NULL){ + argp_error(state, "No colon in \"%s\"", arg); + errno = EINVAL; + break; + } + *option_list = '\0'; + option_list++; + if(arg[0] == '\0'){ + argp_error(state, "Empty plugin name"); + errno = EINVAL; + break; + } + char *option; + while((option = strsep(&option_list, ",")) != NULL){ + if(not add_argument(getplugin(arg), option)){ + break; + } + } + errno = 0; + } + break; + case 'E': /* --env-for */ + { + char *envdef = strchr(arg, ':'); + if(envdef == NULL){ + argp_error(state, "No colon in \"%s\"", arg); + errno = EINVAL; + break; + } + *envdef = '\0'; + envdef++; + if(arg[0] == '\0'){ + argp_error(state, "Empty plugin name"); + errno = EINVAL; + break; + } + if(add_environment(getplugin(arg), envdef, true)){ + errno = 0; + } + } + break; + case 'd': /* --disable */ + { + plugin *p = getplugin(arg); + if(p != NULL){ + p->disabled = true; + errno = 0; + } + } + break; + case 'e': /* --enable */ + { + plugin *p = getplugin(arg); + if(p != NULL){ + p->disabled = false; + errno = 0; + } + } + break; + case 128: /* --plugin-dir */ + free(plugindir); + plugindir = strdup(arg); + if(plugindir != NULL){ + errno = 0; + } + break; + case 129: /* --config-file */ + /* This is already done by parse_opt_config_file() */ + break; + case 130: /* --userid */ + tmp_id = strtoimax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmp_id != (uid_t)tmp_id){ + argp_error(state, "Bad user ID number: \"%s\", using %" + PRIdMAX, arg, (intmax_t)uid); + break; + } + uid = (uid_t)tmp_id; + errno = 0; + break; + case 131: /* --groupid */ + tmp_id = strtoimax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmp_id != (gid_t)tmp_id){ + argp_error(state, "Bad group ID number: \"%s\", using %" + PRIdMAX, arg, (intmax_t)gid); + break; + } + gid = (gid_t)tmp_id; + errno = 0; + break; + case 132: /* --debug */ + debug = true; + break; + case 133: /* --plugin-helper-dir */ + free(pluginhelperdir); + pluginhelperdir = strdup(arg); + if(pluginhelperdir != NULL){ + errno = 0; + } + break; + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + case '?': /* --help */ + state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */ + argp_state_help(state, state->out_stream, ARGP_HELP_STD_HELP); + __builtin_unreachable(); + case -3: /* --usage */ + state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */ + argp_state_help(state, state->out_stream, + ARGP_HELP_USAGE | ARGP_HELP_EXIT_OK); + __builtin_unreachable(); + case 'V': /* --version */ + fprintf(state->out_stream, "%s\n", argp_program_version); + exit(EXIT_SUCCESS); + break; +/* + * When adding more options before this line, remember to also add a + * "case" to the "parse_opt_config_file" function below. + */ + case ARGP_KEY_ARG: + /* Cryptsetup always passes an argument, which is an empty + string if "none" was specified in /etc/crypttab. So if + argument was empty, we ignore it silently. */ + if(arg[0] == '\0'){ + break; + } +#if __GNUC__ >= 7 + __attribute__((fallthrough)); +#else + /* FALLTHROUGH */ +#endif + default: + return ARGP_ERR_UNKNOWN; + } + return errno; /* Set to 0 at start */ + } + + /* This option parser is the same as parse_opt() above, except it + ignores everything but the --config-file option. */ + error_t parse_opt_config_file(int key, char *arg, + __attribute__((unused)) + struct argp_state *state){ + errno = 0; + switch(key){ + case 'g': /* --global-options */ + case 'G': /* --global-env */ + case 'o': /* --options-for */ + case 'E': /* --env-for */ + case 'd': /* --disable */ + case 'e': /* --enable */ + case 128: /* --plugin-dir */ + break; + case 129: /* --config-file */ + free(argfile); + argfile = strdup(arg); + if(argfile != NULL){ + errno = 0; + } + break; + case 130: /* --userid */ + case 131: /* --groupid */ + case 132: /* --debug */ + case 133: /* --plugin-helper-dir */ + case '?': /* --help */ + case -3: /* --usage */ + case 'V': /* --version */ + case ARGP_KEY_ARG: + break; + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + struct argp argp = { .options = options, + .parser = parse_opt_config_file, + .args_doc = "", + .doc = "Mandos plugin runner -- Run plugins" }; + + /* Parse using parse_opt_config_file() in order to get the custom + config file location, if any. */ + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, + NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + exitstatus = EX_OSERR; + goto fallback; + case EINVAL: + exitstatus = EX_USAGE; + goto fallback; + } + + /* Reset to the normal argument parser */ + argp.parser = parse_opt; + + /* Open the configfile if available */ + if(argfile == NULL){ + conffp = fopen(AFILE, "r"); + } else { + conffp = fopen(argfile, "r"); + } + if(conffp != NULL){ + char *org_line = NULL; + char *p, *arg, *new_arg, *line; + size_t size = 0; + const char whitespace_delims[] = " \r\t\f\v\n"; + const char comment_delim[] = "#"; + + custom_argc = 1; + custom_argv = malloc(sizeof(char*) * 2); + if(custom_argv == NULL){ + error(0, errno, "malloc"); + exitstatus = EX_OSERR; + goto fallback; + } + custom_argv[0] = argv[0]; + custom_argv[1] = NULL; + + /* for each line in the config file, strip whitespace and ignore + commented text */ while(true){ - fd_set rfds = rfds_orig; - int select_ret = select(maxfd+1, &rfds, NULL, NULL, NULL); - if (select_ret == -1){ - perror(argv[0]); - goto end; - }else{ - for(process *process_itr = process_list; process_itr != NULL; - process_itr = process_itr->next){ - if(FD_ISSET(process_itr->fd, &rfds)){ - if(process_itr->buffer_length + BUFFER_SIZE - > process_itr->buffer_size){ - process_itr->buffer = realloc(process_itr->buffer, - process_itr->buffer_size - + (size_t) BUFFER_SIZE); - if (process_itr->buffer == NULL){ - perror(argv[0]); - goto end; - } - process_itr->buffer_size += BUFFER_SIZE; - } - ret = read(process_itr->fd, process_itr->buffer - + process_itr->buffer_length, BUFFER_SIZE); - if(ret < 0){ - /* Read error from this process; ignore it */ - continue; - } - process_itr->buffer_length += (size_t) ret; - if(ret == 0){ - /* got EOF */ - /* wait for process exit */ - int status; - waitpid(process_itr->pid, &status, 0); - if(WIFEXITED(status) and WEXITSTATUS(status) == 0){ - write(STDOUT_FILENO, process_itr->buffer, - process_itr->buffer_length); - goto end; - } else { - FD_CLR(process_itr->fd, &rfds_orig); - } - } - } - } - } - } - } - - end: - for(process *process_itr = process_list; process_itr != NULL; - process_itr = process_itr->next){ - close(process_itr->fd); - kill(process_itr->pid, SIGTERM); - free(process_itr->buffer); - } - - while(true){ - int status; - ret = wait(&status); - if (ret == -1){ - if(errno != ECHILD){ - perror("wait"); - } - break; - } - } - return EXIT_SUCCESS; + sret = getline(&org_line, &size, conffp); + if(sret == -1){ + break; + } + + line = org_line; + arg = strsep(&line, comment_delim); + while((p = strsep(&arg, whitespace_delims)) != NULL){ + if(p[0] == '\0'){ + continue; + } + new_arg = strdup(p); + if(new_arg == NULL){ + error(0, errno, "strdup"); + exitstatus = EX_OSERR; + free(org_line); + goto fallback; + } + + custom_argc += 1; + { + char **new_argv = realloc(custom_argv, sizeof(char *) + * ((size_t)custom_argc + 1)); + if(new_argv == NULL){ + error(0, errno, "realloc"); + exitstatus = EX_OSERR; + free(new_arg); + free(org_line); + goto fallback; + } else { + custom_argv = new_argv; + } + } + custom_argv[custom_argc-1] = new_arg; + custom_argv[custom_argc] = NULL; + } + } + do { + ret = fclose(conffp); + } while(ret == EOF and errno == EINTR); + if(ret == EOF){ + error(0, errno, "fclose"); + exitstatus = EX_IOERR; + goto fallback; + } + free(org_line); + } else { + /* Check for harmful errors and go to fallback. Other errors might + not affect opening plugins */ + if(errno == EMFILE or errno == ENFILE or errno == ENOMEM){ + error(0, errno, "fopen"); + exitstatus = EX_OSERR; + goto fallback; + } + } + /* If there were any arguments from the configuration file, pass + them to parser as command line arguments */ + if(custom_argv != NULL){ + ret = argp_parse(&argp, custom_argc, custom_argv, + ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, + NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + exitstatus = EX_OSERR; + goto fallback; + case EINVAL: + exitstatus = EX_CONFIG; + goto fallback; + } + } + + /* Parse actual command line arguments, to let them override the + config file */ + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, + NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + exitstatus = EX_OSERR; + goto fallback; + case EINVAL: + exitstatus = EX_USAGE; + goto fallback; + } + + { + char *pluginhelperenv; + bool bret = true; + ret = asprintf(&pluginhelperenv, "MANDOSPLUGINHELPERDIR=%s", + pluginhelperdir != NULL ? pluginhelperdir : PHDIR); + if(ret != -1){ + bret = add_environment(getplugin(NULL), pluginhelperenv, true); + } + if(ret == -1 or not bret){ + error(0, errno, "Failed to set MANDOSPLUGINHELPERDIR" + " environment variable to \"%s\" for all plugins\n", + pluginhelperdir != NULL ? pluginhelperdir : PHDIR); + } + if(ret != -1){ + free(pluginhelperenv); + } + } + + if(debug){ + for(plugin *p = plugin_list; p != NULL; p = p->next){ + fprintf(stderr, "Plugin: %s has %d arguments\n", + p->name ? p->name : "Global", p->argc - 1); + for(char **a = p->argv; *a != NULL; a++){ + fprintf(stderr, "\tArg: %s\n", *a); + } + fprintf(stderr, "...and %d environment variables\n", p->envc); + for(char **a = p->environ; *a != NULL; a++){ + fprintf(stderr, "\t%s\n", *a); + } + } + } + + if(getuid() == 0){ + /* Work around Debian bug #633582: + */ + int plugindir_fd = open(/* plugindir or */ PDIR, O_RDONLY); + if(plugindir_fd == -1){ + if(errno != ENOENT){ + error(0, errno, "open(\"" PDIR "\")"); + } + } else { + ret = (int)TEMP_FAILURE_RETRY(fstat(plugindir_fd, &st)); + if(ret == -1){ + error(0, errno, "fstat"); + } else { + if(S_ISDIR(st.st_mode) and st.st_uid == 0 and st.st_gid == 0){ + ret = fchown(plugindir_fd, uid, gid); + if(ret == -1){ + error(0, errno, "fchown"); + } + } + } + close(plugindir_fd); + } + } + + /* Lower permissions */ + ret = setgid(gid); + if(ret == -1){ + error(0, errno, "setgid"); + } + ret = setuid(uid); + if(ret == -1){ + error(0, errno, "setuid"); + } + + /* Open plugin directory with close_on_exec flag */ + { + dir_fd = open(plugindir != NULL ? plugindir : PDIR, O_RDONLY | +#ifdef O_CLOEXEC + O_CLOEXEC +#else /* not O_CLOEXEC */ + 0 +#endif /* not O_CLOEXEC */ + ); + if(dir_fd == -1){ + error(0, errno, "Could not open plugin dir"); + exitstatus = EX_UNAVAILABLE; + goto fallback; + } + +#ifndef O_CLOEXEC + /* Set the FD_CLOEXEC flag on the directory */ + ret = set_cloexec_flag(dir_fd); + if(ret < 0){ + error(0, errno, "set_cloexec_flag"); + exitstatus = EX_OSERR; + goto fallback; + } +#endif /* O_CLOEXEC */ + } + + int good_name(const struct dirent * const dirent){ + const char * const patterns[] = { ".*", "#*#", "*~", "*.dpkg-new", + "*.dpkg-old", "*.dpkg-bak", + "*.dpkg-divert", NULL }; +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + for(const char **pat = (const char **)patterns; + *pat != NULL; pat++){ +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + if(fnmatch(*pat, dirent->d_name, FNM_FILE_NAME | FNM_PERIOD) + != FNM_NOMATCH){ + if(debug){ + fprintf(stderr, "Ignoring plugin dir entry \"%s\"" + " matching pattern %s\n", dirent->d_name, *pat); + } + return 0; + } + } + return 1; + } + + int numplugins = scandirat(dir_fd, ".", &direntries, good_name, + alphasort); + if(numplugins == -1){ + error(0, errno, "Could not scan plugin dir"); + direntries = NULL; + exitstatus = EX_OSERR; + goto fallback; + } + + FD_ZERO(&rfds_all); + + /* Read and execute any executable in the plugin directory*/ + for(int i = 0; i < numplugins; i++){ + + int plugin_fd = openat(dir_fd, direntries[i]->d_name, O_RDONLY); + if(plugin_fd == -1){ + error(0, errno, "Could not open plugin"); + free(direntries[i]); + continue; + } + ret = (int)TEMP_FAILURE_RETRY(fstat(plugin_fd, &st)); + if(ret == -1){ + error(0, errno, "stat"); + close(plugin_fd); + free(direntries[i]); + continue; + } + + /* Ignore non-executable files */ + if(not S_ISREG(st.st_mode) + or (TEMP_FAILURE_RETRY(faccessat(dir_fd, direntries[i]->d_name, + X_OK, 0)) != 0)){ + if(debug){ + fprintf(stderr, "Ignoring plugin dir entry \"%s/%s\"" + " with bad type or mode\n", + plugindir != NULL ? plugindir : PDIR, + direntries[i]->d_name); + } + close(plugin_fd); + free(direntries[i]); + continue; + } + + plugin *p = getplugin(direntries[i]->d_name); + if(p == NULL){ + error(0, errno, "getplugin"); + close(plugin_fd); + free(direntries[i]); + continue; + } + if(p->disabled){ + if(debug){ + fprintf(stderr, "Ignoring disabled plugin \"%s\"\n", + direntries[i]->d_name); + } + close(plugin_fd); + free(direntries[i]); + continue; + } + { + /* Add global arguments to argument list for this plugin */ + plugin *g = getplugin(NULL); + if(g != NULL){ + for(char **a = g->argv + 1; *a != NULL; a++){ + if(not add_argument(p, *a)){ + error(0, errno, "add_argument"); + } + } + /* Add global environment variables */ + for(char **e = g->environ; *e != NULL; e++){ + if(not add_environment(p, *e, false)){ + error(0, errno, "add_environment"); + } + } + } + } + /* If this plugin has any environment variables, we need to + duplicate the environment from this process, too. */ + if(p->environ[0] != NULL){ + for(char **e = environ; *e != NULL; e++){ + if(not add_environment(p, *e, false)){ + error(0, errno, "add_environment"); + } + } + } + + int pipefd[2]; +#ifndef O_CLOEXEC + ret = (int)TEMP_FAILURE_RETRY(pipe(pipefd)); +#else /* O_CLOEXEC */ + ret = (int)TEMP_FAILURE_RETRY(pipe2(pipefd, O_CLOEXEC)); +#endif /* O_CLOEXEC */ + if(ret == -1){ + error(0, errno, "pipe"); + exitstatus = EX_OSERR; + free(direntries[i]); + goto fallback; + } + if(pipefd[0] >= FD_SETSIZE){ + fprintf(stderr, "pipe()[0] (%d) >= FD_SETSIZE (%d)", pipefd[0], + FD_SETSIZE); + close(pipefd[0]); + close(pipefd[1]); + exitstatus = EX_OSERR; + free(direntries[i]); + goto fallback; + } +#ifndef O_CLOEXEC + /* Ask OS to automatic close the pipe on exec */ + ret = set_cloexec_flag(pipefd[0]); + if(ret < 0){ + error(0, errno, "set_cloexec_flag"); + close(pipefd[0]); + close(pipefd[1]); + exitstatus = EX_OSERR; + free(direntries[i]); + goto fallback; + } + ret = set_cloexec_flag(pipefd[1]); + if(ret < 0){ + error(0, errno, "set_cloexec_flag"); + close(pipefd[0]); + close(pipefd[1]); + exitstatus = EX_OSERR; + free(direntries[i]); + goto fallback; + } +#endif /* not O_CLOEXEC */ + /* Block SIGCHLD until process is safely in process list */ + ret = (int)TEMP_FAILURE_RETRY(sigprocmask(SIG_BLOCK, + &sigchld_action.sa_mask, + NULL)); + if(ret < 0){ + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; + free(direntries[i]); + goto fallback; + } + /* Starting a new process to be watched */ + pid_t pid; + do { + pid = fork(); + } while(pid == -1 and errno == EINTR); + if(pid == -1){ + error(0, errno, "fork"); + TEMP_FAILURE_RETRY(sigprocmask(SIG_UNBLOCK, + &sigchld_action.sa_mask, NULL)); + close(pipefd[0]); + close(pipefd[1]); + exitstatus = EX_OSERR; + free(direntries[i]); + goto fallback; + } + if(pid == 0){ + /* this is the child process */ + ret = sigaction(SIGCHLD, &old_sigchld_action, NULL); + if(ret < 0){ + error(0, errno, "sigaction"); + _exit(EX_OSERR); + } + ret = sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL); + if(ret < 0){ + error(0, errno, "sigprocmask"); + _exit(EX_OSERR); + } + + ret = dup2(pipefd[1], STDOUT_FILENO); /* replace our stdout */ + if(ret == -1){ + error(0, errno, "dup2"); + _exit(EX_OSERR); + } + + if(fexecve(plugin_fd, p->argv, + (p->environ[0] != NULL) ? p->environ : environ) < 0){ + error(0, errno, "fexecve for %s/%s", + plugindir != NULL ? plugindir : PDIR, + direntries[i]->d_name); + _exit(EX_OSERR); + } + /* no return */ + } + /* Parent process */ + close(pipefd[1]); /* Close unused write end of pipe */ + close(plugin_fd); + plugin *new_plugin = getplugin(direntries[i]->d_name); + if(new_plugin == NULL){ + error(0, errno, "getplugin"); + ret = (int)(TEMP_FAILURE_RETRY + (sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, + NULL))); + if(ret < 0){ + error(0, errno, "sigprocmask"); + } + exitstatus = EX_OSERR; + free(direntries[i]); + goto fallback; + } + free(direntries[i]); + + new_plugin->pid = pid; + new_plugin->fd = pipefd[0]; + + if(debug){ + fprintf(stderr, "Plugin %s started (PID %" PRIdMAX ")\n", + new_plugin->name, (intmax_t) (new_plugin->pid)); + } + + /* Unblock SIGCHLD so signal handler can be run if this process + has already completed */ + ret = (int)TEMP_FAILURE_RETRY(sigprocmask(SIG_UNBLOCK, + &sigchld_action.sa_mask, + NULL)); + if(ret < 0){ + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; + goto fallback; + } + + FD_SET(new_plugin->fd, &rfds_all); + + if(maxfd < new_plugin->fd){ + maxfd = new_plugin->fd; + } + } + + free(direntries); + direntries = NULL; + close(dir_fd); + dir_fd = -1; + free_plugin(getplugin(NULL)); + + for(plugin *p = plugin_list; p != NULL; p = p->next){ + if(p->pid != 0){ + break; + } + if(p->next == NULL){ + fprintf(stderr, "No plugin processes started. Incorrect plugin" + " directory?\n"); + free_plugin_list(); + } + } + + /* Main loop while running plugins exist */ + while(plugin_list){ + fd_set rfds = rfds_all; + int select_ret = select(maxfd+1, &rfds, NULL, NULL, NULL); + if(select_ret == -1 and errno != EINTR){ + error(0, errno, "select"); + exitstatus = EX_OSERR; + goto fallback; + } + /* OK, now either a process completed, or something can be read + from one of them */ + for(plugin *proc = plugin_list; proc != NULL;){ + /* Is this process completely done? */ + if(proc->completed and proc->eof){ + /* Only accept the plugin output if it exited cleanly */ + if(not WIFEXITED(proc->status) + or WEXITSTATUS(proc->status) != 0){ + /* Bad exit by plugin */ + + if(debug){ + if(WIFEXITED(proc->status)){ + fprintf(stderr, "Plugin %s [%" PRIdMAX "] exited with" + " status %d\n", proc->name, + (intmax_t) (proc->pid), + WEXITSTATUS(proc->status)); + } else if(WIFSIGNALED(proc->status)){ + fprintf(stderr, "Plugin %s [%" PRIdMAX "] killed by" + " signal %d: %s\n", proc->name, + (intmax_t) (proc->pid), + WTERMSIG(proc->status), + strsignal(WTERMSIG(proc->status))); + } + } + + /* Remove the plugin */ + FD_CLR(proc->fd, &rfds_all); + + /* Block signal while modifying process_list */ + ret = (int)TEMP_FAILURE_RETRY(sigprocmask + (SIG_BLOCK, + &sigchld_action.sa_mask, + NULL)); + if(ret < 0){ + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; + goto fallback; + } + + plugin *next_plugin = proc->next; + free_plugin(proc); + proc = next_plugin; + + /* We are done modifying process list, so unblock signal */ + ret = (int)(TEMP_FAILURE_RETRY + (sigprocmask(SIG_UNBLOCK, + &sigchld_action.sa_mask, NULL))); + if(ret < 0){ + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; + goto fallback; + } + + if(plugin_list == NULL){ + break; + } + + continue; + } + + /* This process exited nicely, so print its buffer */ + + bool bret = print_out_password(proc->buffer, + proc->buffer_length); + if(not bret){ + error(0, errno, "print_out_password"); + exitstatus = EX_IOERR; + } + goto fallback; + } + + /* This process has not completed. Does it have any output? */ + if(proc->eof or not FD_ISSET(proc->fd, &rfds)){ + /* This process had nothing to say at this time */ + proc = proc->next; + continue; + } + /* Before reading, make the process' data buffer large enough */ + if(proc->buffer_length + BUFFER_SIZE > proc->buffer_size){ + char *new_buffer = realloc(proc->buffer, proc->buffer_size + + (size_t) BUFFER_SIZE); + if(new_buffer == NULL){ + error(0, errno, "malloc"); + exitstatus = EX_OSERR; + goto fallback; + } + proc->buffer = new_buffer; + proc->buffer_size += BUFFER_SIZE; + } + /* Read from the process */ + sret = TEMP_FAILURE_RETRY(read(proc->fd, + proc->buffer + + proc->buffer_length, + BUFFER_SIZE)); + if(sret < 0){ + /* Read error from this process; ignore the error */ + proc = proc->next; + continue; + } + if(sret == 0){ + /* got EOF */ + proc->eof = true; + } else { + proc->buffer_length += (size_t) sret; + } + } + } + + + fallback: + + if(plugin_list == NULL or (exitstatus != EXIT_SUCCESS + and exitstatus != EX_OK)){ + /* Fallback if all plugins failed, none are found or an error + occured */ + bool bret; + fprintf(stderr, "Going to fallback mode using getpass(3)\n"); + char *passwordbuffer = getpass("Password: "); + size_t len = strlen(passwordbuffer); + /* Strip trailing newline */ + if(len > 0 and passwordbuffer[len-1] == '\n'){ + passwordbuffer[len-1] = '\0'; /* not strictly necessary */ + len--; + } + bret = print_out_password(passwordbuffer, len); + if(not bret){ + error(0, errno, "print_out_password"); + exitstatus = EX_IOERR; + } + } + + /* Restore old signal handler */ + ret = sigaction(SIGCHLD, &old_sigchld_action, NULL); + if(ret == -1){ + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; + } + + if(custom_argv != NULL){ + for(char **arg = custom_argv+1; *arg != NULL; arg++){ + free(*arg); + } + free(custom_argv); + } + + free(direntries); + + if(dir_fd != -1){ + close(dir_fd); + } + + /* Kill the processes */ + for(plugin *p = plugin_list; p != NULL; p = p->next){ + if(p->pid != 0){ + close(p->fd); + ret = kill(p->pid, SIGTERM); + if(ret == -1 and errno != ESRCH){ + /* Set-uid proccesses might not get closed */ + error(0, errno, "kill"); + } + } + } + + /* Wait for any remaining child processes to terminate */ + do { + ret = wait(NULL); + } while(ret >= 0); + if(errno != ECHILD){ + error(0, errno, "wait"); + } + + free_plugin_list(); + + free(plugindir); + free(pluginhelperdir); + free(argfile); + + return exitstatus; } === added file 'plugin-runner.conf' --- plugin-runner.conf 1970-01-01 00:00:00 +0000 +++ plugin-runner.conf 2009-04-17 08:26:17 +0000 @@ -0,0 +1,10 @@ +## This is the configuration file for plugin-runner(8mandos). This +## file should be installed as "/etc/mandos/plugin-runner.conf", and +## will be copied to "/conf/conf.d/mandos/plugin-runner.conf" in the +## initrd.img file. +## +## After editing this file, the initrd image file must be updated for +## the changes to take effect! + +## Example: +#--options-for=mandos-client:--debug === added file 'plugin-runner.xml' --- plugin-runner.xml 1970-01-01 00:00:00 +0000 +++ plugin-runner.xml 2019-07-25 22:44:36 +0000 @@ -0,0 +1,698 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + + Run Mandos plugins, pass data from first to succeed. + + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + + + + + DESCRIPTION + + &COMMANDNAME; is a program which is meant to + be specified as a keyscript for the root disk in + crypttab + 5. The aim of this + program is therefore to output a password, which then + cryptsetup + 8 will use to unlock the + root disk. + + + This program is not meant to be invoked directly, but can be in + order to test it. Note that any password obtained will simply + be output on standard output. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OPTIONS + + + + + + + This option will add an environment variable setting to + all plugins. This will override any inherited environment + variable. + + + + + + + + + + This option will add an environment variable setting to + the PLUGIN plugin. This will + override any inherited environment variables or + environment variables specified using + . + + + + + + + + + + Pass some options to all plugins. + OPTIONS is a comma separated + list of options. This is not a very useful option, except + for specifying the + option to all plugins. + + + + + + + + + + Pass some options to a specific plugin. PLUGIN is the name (file basename) of a + plugin, and OPTIONS is a comma + separated list of options. + + + Note that since options are not split on whitespace, the + way to pass, to the plugin + foo, the option + with the option argument + baz is either + --options-for=foo:--bar=baz or + --options-for=foo:--bar,baz. Using + --options-for="foo:--bar baz". will + not work. + + + + + + + + + + Disable the plugin named + PLUGIN. The plugin will not be + started. + + + + + + + + + + Re-enable the plugin named + PLUGIN. This is only useful to + undo a previous option, maybe + from the configuration file. + + + + + + + + + Change to group ID ID on + startup. The default is 65534. All plugins will be + started using this group ID. Note: + This must be a number, not a name. + + + + + + + + + Change to user ID ID on + startup. The default is 65534. All plugins will be + started using this user ID. Note: + This must be a number, not a name. + + + + + + + + + Specify a different plugin directory. The default is + /lib/mandos/plugins.d, which will + exist in the initial RAM disk + environment. + + + + + + + + + Specify a different plugin helper directory. The default + is /lib/mandos/plugin-helpers, which + will exist in the initial RAM disk + environment. (This will simply be passed to all plugins + via the MANDOSPLUGINHELPERDIR environment + variable. See ) + + + + + + + + + Specify a different file to read additional options from. + See . Other command line options + will override options specified in the file. + + + + + + + + + Enable debug mode. This will enable a lot of output to + standard error about what the program is doing. The + program will still perform all other functions normally. + The default is to not run in debug + mode. + + + The plugins will not be affected by + this option. Use + + if complete debugging eruption is desired. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + + + + + + OVERVIEW + + + This program will run on the client side in the initial + RAM disk environment, and is responsible for + getting a password. It does this by running plugins, one of + which will normally be the actual client program communicating + with the server. + + + + PLUGINS + + This program will get a password by running a number of + plugins, which are simply executable + programs in a directory in the initial RAM + disk environment. The default directory is + /lib/mandos/plugins.d, but this can be + changed with the option. The + plugins are started in parallel, and the first plugin to output + a password and exit with a successful exit + code will make this plugin-runner output the password from that + plugin, stop any other plugins, and exit. + + + + WRITING PLUGINS + + A plugin is simply a program which prints a password to its + standard output and then exits with a successful (zero) exit + status. If the exit status is not zero, any output on + standard output will be ignored by the plugin runner. Any + output on its standard error channel will simply be passed to + the standard error of the plugin runner, usually the system + console. + + + If the password is a single-line, manually entered passprase, + a final trailing newline character should + not be printed. + + + The plugin will run in the initial RAM disk environment, so + care must be taken not to depend on any files or running + services not available there. Any helper executables required + by the plugin (which are not in the PATH) can + be placed in the plugin helper directory, the name of which + will be made available to the plugin via the + MANDOSPLUGINHELPERDIR environment variable. + + + The plugin must exit cleanly and free all allocated resources + upon getting the TERM signal, since this is what the plugin + runner uses to stop all other plugins when one plugin has + output a password and exited cleanly. + + + The plugin must not use resources, like for instance reading + from the standard input, without knowing that no other plugin + is also using it. + + + It is useful, but not required, for the plugin to take the + option. + + + + + + FALLBACK + + If no plugins succeed, this program will, as a fallback, ask for + a password on the console using getpass3, + and output it. This is not meant to be the normal mode of + operation, as there is a separate plugin for getting a password + from the console. + + + + + EXIT STATUS + + Exit status of this program is zero if no errors were + encountered, and otherwise not. The fallback (see ) may or may not have succeeded in either + case. + + + + + ENVIRONMENT + + This program does not use any environment variables itself, it + only passes on its environment to all the plugins. The + environment passed to plugins can be modified using the + and + options. Also, the option + will affect the environment variable + MANDOSPLUGINHELPERDIR for the plugins. + + + + + FILES + + + + /conf/conf.d/mandos/plugin-runner.conf + + + Since this program will be run as a keyscript, there is + little to no opportunity to pass command line arguments + to it. Therefore, it will also + read this file and use its contents as + whitespace-separated command line options. Also, + everything from a # character to the end + of a line is ignored. + + + This program is meant to run in the initial RAM disk + environment, so that is where this file is assumed to + exist. The file does not need to exist in the normal + file system. + + + This file will be processed before + the normal command line options, so the latter can + override the former, if need be. + + + This file name is the default; the file to read for + arguments can be changed using the + option. + + + + + /lib/mandos/plugins.d + + + The default plugin directory; can be changed by the + option. + + + + + /lib/mandos/plugin-helpers + + + The default plugin helper directory; can be changed by + the option. + + + + + + + + + BUGS + + The option is ignored when + specified from within a configuration file. + + + + + + EXAMPLE + + + Normal invocation needs no options: + + + &COMMANDNAME; + + + + + Run the program, but not the plugins, in debug mode: + + + + + &COMMANDNAME; --debug + + + + + + Run all plugins, but run the foo plugin in + debug mode: + + + + + &COMMANDNAME; --options-for=foo:--debug + + + + + + Run all plugins, but not the program, in debug mode: + + + + + &COMMANDNAME; --global-options=--debug + + + + + + Read a different configuration file, run plugins from a + different directory, specify an alternate plugin helper + directory and add four options to the + mandos-client + 8mandos plugin: + + + + +cd /etc/keys/mandos; &COMMANDNAME; --config-file=/etc/mandos/plugin-runner.conf --plugin-dir /usr/lib/x86_64-linux-gnu/mandos/plugins.d --plugin-helper-dir /usr/lib/x86_64-linux-gnu/mandos/plugin-helpers --options-for=mandos-client:--pubkey=pubkey.txt,​--seckey=seckey.txt,​--tls-pubkey=tls-pubkey.pem,​--tls-privkey=tls-privkey.pem + + + + + + SECURITY + + This program will, when starting, try to switch to another user. + If it is started as root, it will succeed, and will by default + switch to user and group 65534, which are assumed to be + non-privileged. This user and group is then what all plugins + will be started as. Therefore, the only way to run a plugin as + a privileged user is to have the set-user-ID or set-group-ID bit + set on the plugin executable file (see + execve2 + ). + + + If this program is used as a keyscript in crypttab5 + , there is a slight risk that if this program + fails to work, there might be no way to boot the system except + for booting from another media and editing the initial RAM disk + image to not run this program. This is, however, unlikely, + since the password-prompt8mandos + plugin will read a password from the console in + case of failure of the other plugins, and this plugin runner + will also, in case of catastrophic failure, itself fall back to + asking and outputting a password on the console (see ). + + + + + SEE ALSO + + intro + 8mandos, + cryptsetup + 8, + crypttab + 5, + execve + 2, + mandos + 8, + password-prompt + 8mandos, + mandos-client + 8mandos + + + +
+ + + + + === added file 'plugins.d/askpass-fifo.c' --- plugins.d/askpass-fifo.c 1970-01-01 00:00:00 +0000 +++ plugins.d/askpass-fifo.c 2019-02-11 07:06:55 +0000 @@ -0,0 +1,227 @@ +/* -*- coding: utf-8 -*- */ +/* + * Askpass-FIFO - Read a password from a FIFO and output it + * + * Copyright © 2008-2019 Teddy Hogeborn + * Copyright © 2008-2019 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos 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. + * + * Mandos 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 Mandos. If not, see . + * + * Contact the authors at . + */ + +#define _GNU_SOURCE /* TEMP_FAILURE_RETRY() */ +#include /* uid_t, gid_t, ssize_t */ +#include /* mkfifo(), S_IRUSR, S_IWUSR */ +#include /* and */ +#include /* errno, EACCES, ENOTDIR, ELOOP, + ENAMETOOLONG, ENOSPC, EROFS, + ENOENT, EEXIST, EFAULT, EMFILE, + ENFILE, ENOMEM, EBADF, EINVAL, EIO, + EISDIR, EFBIG */ +#include /* error() */ +#include /* fprintf(), vfprintf(), + vasprintf() */ +#include /* EXIT_FAILURE, NULL, size_t, free(), + realloc(), EXIT_SUCCESS */ +#include /* open(), O_RDONLY */ +#include /* read(), close(), write(), + STDOUT_FILENO */ +#include /* EX_OSERR, EX_OSFILE, + EX_UNAVAILABLE, EX_IOERR */ +#include /* strerror() */ +#include /* va_list, va_start(), ... */ + +uid_t uid = 65534; +gid_t gid = 65534; + +/* Function to use when printing errors */ +__attribute__((format (gnu_printf, 3, 4))) +void error_plus(int status, int errnum, const char *formatstring, + ...){ + va_list ap; + char *text; + int ret; + + va_start(ap, formatstring); + ret = vasprintf(&text, formatstring, ap); + if(ret == -1){ + fprintf(stderr, "Mandos plugin %s: ", + program_invocation_short_name); + vfprintf(stderr, formatstring, ap); + fprintf(stderr, ": "); + fprintf(stderr, "%s\n", strerror(errnum)); + error(status, errno, "vasprintf while printing error"); + if(status){ + __builtin_unreachable(); + } + return; + } + fprintf(stderr, "Mandos plugin "); + error(status, errnum, "%s", text); + if(status){ + __builtin_unreachable(); + } + free(text); +} + +int main(__attribute__((unused))int argc, + __attribute__((unused))char **argv){ + int ret = 0; + ssize_t sret; + + uid = getuid(); + gid = getgid(); + + /* Create FIFO */ + const char passfifo[] = "/lib/cryptsetup/passfifo"; + ret = mkfifo(passfifo, S_IRUSR | S_IWUSR); + if(ret == -1){ + int e = errno; + switch(e){ + case EACCES: + case ENOTDIR: + case ELOOP: + error_plus(EX_OSFILE, errno, "mkfifo"); + __builtin_unreachable(); + case ENAMETOOLONG: + case ENOSPC: + case EROFS: + default: + error_plus(EX_OSERR, errno, "mkfifo"); + __builtin_unreachable(); + case ENOENT: + /* no "/lib/cryptsetup"? */ + error_plus(EX_UNAVAILABLE, errno, "mkfifo"); + __builtin_unreachable(); + case EEXIST: + break; /* not an error */ + } + } + + /* Open FIFO */ + int fifo_fd = open(passfifo, O_RDONLY); + if(fifo_fd == -1){ + int e = errno; + error_plus(0, errno, "open"); + switch(e){ + case EACCES: + case ENOENT: + case EFAULT: + return EX_UNAVAILABLE; + case ENAMETOOLONG: + case EMFILE: + case ENFILE: + case ENOMEM: + default: + return EX_OSERR; + case ENOTDIR: + case ELOOP: + return EX_OSFILE; + } + } + + /* Lower group privileges */ + if(setgid(gid) == -1){ + error_plus(0, errno, "setgid"); + } + + /* Lower user privileges */ + if(setuid(uid) == -1){ + error_plus(0, errno, "setuid"); + } + + /* Read from FIFO */ + char *buf = NULL; + size_t buf_len = 0; + { + size_t buf_allocated = 0; + const size_t blocksize = 1024; + do { + if(buf_len + blocksize > buf_allocated){ + char *tmp = realloc(buf, buf_allocated + blocksize); + if(tmp == NULL){ + error_plus(0, errno, "realloc"); + free(buf); + return EX_OSERR; + } + buf = tmp; + buf_allocated += blocksize; + } + sret = read(fifo_fd, buf + buf_len, buf_allocated - buf_len); + if(sret == -1){ + int e = errno; + free(buf); + errno = e; + error_plus(0, errno, "read"); + switch(e){ + case EBADF: + case EFAULT: + case EINVAL: + default: + return EX_OSERR; + case EIO: + return EX_IOERR; + case EISDIR: + return EX_UNAVAILABLE; + } + } + buf_len += (size_t)sret; + } while(sret != 0); + } + + /* Close FIFO */ + close(fifo_fd); + + /* Print password to stdout */ + size_t written = 0; + while(written < buf_len){ + sret = write(STDOUT_FILENO, buf + written, buf_len - written); + if(sret == -1){ + int e = errno; + free(buf); + errno = e; + error_plus(0, errno, "write"); + switch(e){ + case EBADF: + case EFAULT: + case EINVAL: + return EX_OSFILE; + case EFBIG: + case EIO: + case ENOSPC: + default: + return EX_IOERR; + } + } + written += (size_t)sret; + } + free(buf); + + ret = close(STDOUT_FILENO); + if(ret == -1){ + int e = errno; + error_plus(0, errno, "close"); + switch(e){ + case EBADF: + return EX_OSFILE; + case EIO: + default: + return EX_IOERR; + } + } + return EXIT_SUCCESS; +} === added file 'plugins.d/askpass-fifo.xml' --- plugins.d/askpass-fifo.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/askpass-fifo.xml 2019-02-10 04:20:26 +0000 @@ -0,0 +1,178 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Mandos plugin to get a password from a + FIFO. + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + This program reads a password from a FIFO and + outputs it to standard output. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + This program is meant to be imitate a feature of the + askpass program, so that programs written to + interface with it can keep working under the + Mandos system. + + + + + OPTIONS + + This program takes no options. + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program was interrupted or encountered an error, and any output + so far could be corrupt and/or truncated, and should therefore + be ignored. + + + + + FILES + + + /lib/cryptsetup/passfifo + + + This is the FIFO where this program + will read the password. If it does not exist, it will be + created. + + + + + + + + BUGS + + + + + EXAMPLE + + Note that normally, this program will not be invoked directly, + but instead started by the Mandos plugin-runner8mandos + . + + + + This program takes no options. + + + &COMMANDNAME; + + + + + + SECURITY + + The only thing that could be considered worthy of note is + this: This program is meant to be run by + plugin-runner8mandos, and will, when run + standalone, outside, in a normal environment, immediately output + on its standard output any presumably secret password it just + received. Therefore, when running this program standalone + (which should never normally be done), take care not to type in + any real secret password by force of habit, since it would then + immediately be shown as output. + + + + + SEE ALSO + + intro + 8mandos, + fifo + 7, + plugin-runner + 8mandos + + +
+ + + + + === renamed file 'plugins.d/mandosclient.c' => 'plugins.d/mandos-client.c' --- plugins.d/mandosclient.c 2008-07-22 06:23:29 +0000 +++ plugins.d/mandos-client.c 2019-02-11 05:14:10 +0000 @@ -1,6 +1,6 @@ /* -*- coding: utf-8 -*- */ /* - * Mandos client - get and decrypt data from a Mandos server + * Mandos-client - get and decrypt data from a Mandos server * * This program is partly derived from an example program for an Avahi * service browser, downloaded from @@ -8,38 +8,108 @@ * includes the following functions: "resolve_callback", * "browse_callback", and parts of "main". * - * Everything else is Copyright © 2007-2008 Teddy Hogeborn and Björn - * Påhlsson. - * - * 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 + * Everything else is + * Copyright © 2008-2019 Teddy Hogeborn + * Copyright © 2008-2019 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos 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. + * + * Mandos 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 - * . + * along with Mandos. If not, see . * - * Contact the authors at and - * . + * Contact the authors at . */ -#define _FORTIFY_SOURCE 2 - +/* Needed by GPGME, specifically gpgme_data_seek() */ +#ifndef _LARGEFILE_SOURCE #define _LARGEFILE_SOURCE +#endif /* not _LARGEFILE_SOURCE */ +#ifndef _FILE_OFFSET_BITS #define _FILE_OFFSET_BITS 64 - -#include -#include -#include -#include -#include /* if_nametoindex */ - +#endif /* not _FILE_OFFSET_BITS */ + +#define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), asprintf() */ + +#include /* fprintf(), stderr, fwrite(), + stdout, ferror() */ +#include /* uint16_t, uint32_t, intptr_t */ +#include /* NULL, size_t, ssize_t */ +#include /* free(), EXIT_SUCCESS, srand(), + strtof(), abort() */ +#include /* bool, false, true */ +#include /* strcmp(), strlen(), strerror(), + asprintf(), strncpy(), strsignal() + */ +#include /* ioctl */ +#include /* socket(), inet_pton(), sockaddr, + sockaddr_in6, PF_INET6, + SOCK_STREAM, uid_t, gid_t, open(), + opendir(), DIR */ +#include /* open(), S_ISREG */ +#include /* socket(), struct sockaddr_in6, + inet_pton(), connect(), + getnameinfo() */ +#include /* open(), unlinkat(), AT_REMOVEDIR */ +#include /* opendir(), struct dirent, readdir() + */ +#include /* PRIu16, PRIdMAX, intmax_t, + strtoimax() */ +#include /* perror(), errno, EINTR, EINVAL, + EAI_SYSTEM, ENETUNREACH, + EHOSTUNREACH, ECONNREFUSED, EPROTO, + EIO, ENOENT, ENXIO, ENOMEM, EISDIR, + ENOTEMPTY, + program_invocation_short_name */ +#include /* nanosleep(), time(), sleep() */ +#include /* ioctl, ifreq, SIOCGIFFLAGS, IFF_UP, + SIOCSIFFLAGS, if_indextoname(), + if_nametoindex(), IF_NAMESIZE */ +#include /* IN6_IS_ADDR_LINKLOCAL, + INET_ADDRSTRLEN, INET6_ADDRSTRLEN + */ +#include /* close(), SEEK_SET, off_t, write(), + getuid(), getgid(), seteuid(), + setgid(), pause(), _exit(), + unlinkat() */ +#include /* inet_pton(), htons() */ +#include /* not, or, and */ +#include /* struct argp_option, error_t, struct + argp_state, struct argp, + argp_parse(), ARGP_KEY_ARG, + ARGP_KEY_END, ARGP_ERR_UNKNOWN */ +#include /* sigemptyset(), sigaddset(), + sigaction(), SIGTERM, sig_atomic_t, + raise() */ +#include /* EX_OSERR, EX_USAGE, EX_UNAVAILABLE, + EX_NOHOST, EX_IOERR, EX_PROTOCOL */ +#include /* waitpid(), WIFEXITED(), + WEXITSTATUS(), WTERMSIG() */ +#include /* setgroups() */ +#include /* argz_add_sep(), argz_next(), + argz_delete(), argz_append(), + argz_stringify(), argz_add(), + argz_count() */ +#include /* getnameinfo(), NI_NUMERICHOST, + EAI_SYSTEM, gai_strerror() */ + +#ifdef __linux__ +#include /* klogctl() */ +#endif /* __linux__ */ + +/* Avahi */ +/* All Avahi types, constants and functions + Avahi*, avahi_*, + AVAHI_* */ #include #include #include @@ -47,505 +117,1578 @@ #include #include -//mandos client part -#include /* socket(), inet_pton() */ -#include /* socket(), struct sockaddr_in6, - struct in6_addr, inet_pton() */ -#include /* All GnuTLS stuff */ -#include /* GnuTLS with openpgp stuff */ - -#include /* close() */ -#include -#include /* true */ -#include /* memset */ -#include /* inet_pton() */ -#include /* not */ - -// gpgme -#include /* perror() */ -#include - -// getopt long -#include - -#ifndef CERT_ROOT -#define CERT_ROOT "/conf/conf.d/cryptkeyreq/" +/* GnuTLS */ +#include /* All GnuTLS types, constants and + functions: + gnutls_* + init_gnutls_session(), + GNUTLS_* */ +#if GNUTLS_VERSION_NUMBER < 0x030600 +#include + /* gnutls_certificate_set_openpgp_key_file(), + GNUTLS_OPENPGP_FMT_BASE64 */ +#elif GNUTLS_VERSION_NUMBER >= 0x030606 +#include /* gnutls_pkcs_encrypt_flags_t, + GNUTLS_PKCS_PLAIN, + GNUTLS_PKCS_NULL_PASSWORD */ #endif -#define CERTFILE CERT_ROOT "openpgp-client.txt" -#define KEYFILE CERT_ROOT "openpgp-client-key.txt" + +/* GPGME */ +#include /* All GPGME types, constants and + functions: + gpgme_* + GPGME_PROTOCOL_OpenPGP, + GPG_ERR_NO_* */ + #define BUFFER_SIZE 256 -#define DH_BITS 1024 + +#define PATHDIR "/conf/conf.d/mandos" +#define SECKEY "seckey.txt" +#define PUBKEY "pubkey.txt" +#define TLS_PRIVKEY "tls-privkey.pem" +#define TLS_PUBKEY "tls-pubkey.pem" +#define HOOKDIR "/lib/mandos/network-hooks.d" bool debug = false; - +static const char mandos_protocol_version[] = "1"; +const char *argp_program_version = "mandos-client " VERSION; +const char *argp_program_bug_address = ""; +static const char sys_class_net[] = "/sys/class/net"; +char *connect_to = NULL; +const char *hookdir = HOOKDIR; +int hookdir_fd = -1; +uid_t uid = 65534; +gid_t gid = 65534; + +/* Doubly linked list that need to be circularly linked when used */ +typedef struct server{ + const char *ip; + in_port_t port; + AvahiIfIndex if_index; + int af; + struct timespec last_seen; + struct server *next; + struct server *prev; +} server; + +/* Used for passing in values through the Avahi callback functions */ typedef struct { - gnutls_session_t session; + AvahiServer *server; gnutls_certificate_credentials_t cred; + unsigned int dh_bits; gnutls_dh_params_t dh_params; -} encrypted_session; - - -ssize_t pgp_packet_decrypt (char *packet, size_t packet_size, - char **new_packet, const char *homedir){ - gpgme_data_t dh_crypto, dh_plain; + const char *priority; gpgme_ctx_t ctx; + server *current_server; + char *interfaces; + size_t interfaces_size; +} mandos_context; + +/* global so signal handler can reach it*/ +AvahiSimplePoll *simple_poll; + +sig_atomic_t quit_now = 0; +int signal_received = 0; + +/* Function to use when printing errors */ +void perror_plus(const char *print_text){ + int e = errno; + fprintf(stderr, "Mandos plugin %s: ", + program_invocation_short_name); + errno = e; + perror(print_text); +} + +__attribute__((format (gnu_printf, 2, 3), nonnull)) +int fprintf_plus(FILE *stream, const char *format, ...){ + va_list ap; + va_start (ap, format); + + TEMP_FAILURE_RETRY(fprintf(stream, "Mandos plugin %s: ", + program_invocation_short_name)); + return (int)TEMP_FAILURE_RETRY(vfprintf(stream, format, ap)); +} + +/* + * Make additional room in "buffer" for at least BUFFER_SIZE more + * bytes. "buffer_capacity" is how much is currently allocated, + * "buffer_length" is how much is already used. + */ +__attribute__((nonnull, warn_unused_result)) +size_t incbuffer(char **buffer, size_t buffer_length, + size_t buffer_capacity){ + if(buffer_length + BUFFER_SIZE > buffer_capacity){ + char *new_buf = realloc(*buffer, buffer_capacity + BUFFER_SIZE); + if(new_buf == NULL){ + int old_errno = errno; + free(*buffer); + errno = old_errno; + *buffer = NULL; + return 0; + } + *buffer = new_buf; + buffer_capacity += BUFFER_SIZE; + } + return buffer_capacity; +} + +/* Add server to set of servers to retry periodically */ +__attribute__((nonnull, warn_unused_result)) +bool add_server(const char *ip, in_port_t port, AvahiIfIndex if_index, + int af, server **current_server){ + int ret; + server *new_server = malloc(sizeof(server)); + if(new_server == NULL){ + perror_plus("malloc"); + return false; + } + *new_server = (server){ .ip = strdup(ip), + .port = port, + .if_index = if_index, + .af = af }; + if(new_server->ip == NULL){ + perror_plus("strdup"); + free(new_server); + return false; + } + ret = clock_gettime(CLOCK_MONOTONIC, &(new_server->last_seen)); + if(ret == -1){ + perror_plus("clock_gettime"); +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + free((char *)(new_server->ip)); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + free(new_server); + return false; + } + /* Special case of first server */ + if(*current_server == NULL){ + new_server->next = new_server; + new_server->prev = new_server; + *current_server = new_server; + } else { + /* Place the new server last in the list */ + new_server->next = *current_server; + new_server->prev = (*current_server)->prev; + new_server->prev->next = new_server; + (*current_server)->prev = new_server; + } + return true; +} + +/* Set effective uid to 0, return errno */ +__attribute__((warn_unused_result)) +int raise_privileges(void){ + int old_errno = errno; + int ret = 0; + if(seteuid(0) == -1){ + ret = errno; + } + errno = old_errno; + return ret; +} + +/* Set effective and real user ID to 0. Return errno. */ +__attribute__((warn_unused_result)) +int raise_privileges_permanently(void){ + int old_errno = errno; + int ret = raise_privileges(); + if(ret != 0){ + errno = old_errno; + return ret; + } + if(setuid(0) == -1){ + ret = errno; + } + errno = old_errno; + return ret; +} + +/* Set effective user ID to unprivileged saved user ID */ +__attribute__((warn_unused_result)) +int lower_privileges(void){ + int old_errno = errno; + int ret = 0; + if(seteuid(uid) == -1){ + ret = errno; + } + errno = old_errno; + return ret; +} + +/* Lower privileges permanently */ +__attribute__((warn_unused_result)) +int lower_privileges_permanently(void){ + int old_errno = errno; + int ret = 0; + if(setuid(uid) == -1){ + ret = errno; + } + errno = old_errno; + return ret; +} + +/* + * Initialize GPGME. + */ +__attribute__((nonnull, warn_unused_result)) +static bool init_gpgme(const char * const seckey, + const char * const pubkey, + const char * const tempdir, + mandos_context *mc){ gpgme_error_t rc; - ssize_t ret; - ssize_t new_packet_capacity = 0; - ssize_t new_packet_length = 0; gpgme_engine_info_t engine_info; + + /* + * Helper function to insert pub and seckey to the engine keyring. + */ + bool import_key(const char * const filename){ + int ret; + int fd; + gpgme_data_t pgp_data; + + fd = (int)TEMP_FAILURE_RETRY(open(filename, O_RDONLY)); + if(fd == -1){ + perror_plus("open"); + return false; + } + + /* Workaround for systems without a real-time clock; see also + Debian bug #894495: */ + do { + { + time_t currtime = time(NULL); + if(currtime != (time_t)-1){ + struct tm tm; + if(gmtime_r(&currtime, &tm) == NULL) { + perror_plus("gmtime_r"); + break; + } + if(tm.tm_year != 70 or tm.tm_mon != 0){ + break; + } + if(debug){ + fprintf_plus(stderr, "System clock is January 1970"); + } + } else { + if(debug){ + fprintf_plus(stderr, "System clock is invalid"); + } + } + } + struct stat keystat; + ret = fstat(fd, &keystat); + if(ret != 0){ + perror_plus("fstat"); + break; + } + ret = raise_privileges(); + if(ret != 0){ + errno = ret; + perror_plus("Failed to raise privileges"); + break; + } + if(debug){ + fprintf_plus(stderr, + "Setting system clock to key file mtime"); + } + time_t keytime = keystat.st_mtim.tv_sec; + if(stime(&keytime) != 0){ + perror_plus("stime"); + } + ret = lower_privileges(); + if(ret != 0){ + errno = ret; + perror_plus("Failed to lower privileges"); + } + } while(false); - if (debug){ - fprintf(stderr, "Trying to decrypt OpenPGP packet\n"); + rc = gpgme_data_new_from_fd(&pgp_data, fd); + if(rc != GPG_ERR_NO_ERROR){ + fprintf_plus(stderr, "bad gpgme_data_new_from_fd: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + return false; + } + + rc = gpgme_op_import(mc->ctx, pgp_data); + if(rc != GPG_ERR_NO_ERROR){ + fprintf_plus(stderr, "bad gpgme_op_import: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + return false; + } + { + gpgme_import_result_t import_result + = gpgme_op_import_result(mc->ctx); + if((import_result->imported < 1 + or import_result->not_imported > 0) + and import_result->unchanged == 0){ + fprintf_plus(stderr, "bad gpgme_op_import_results:\n"); + fprintf_plus(stderr, + "The total number of considered keys: %d\n", + import_result->considered); + fprintf_plus(stderr, + "The number of keys without user ID: %d\n", + import_result->no_user_id); + fprintf_plus(stderr, + "The total number of imported keys: %d\n", + import_result->imported); + fprintf_plus(stderr, "The number of imported RSA keys: %d\n", + import_result->imported_rsa); + fprintf_plus(stderr, "The number of unchanged keys: %d\n", + import_result->unchanged); + fprintf_plus(stderr, "The number of new user IDs: %d\n", + import_result->new_user_ids); + fprintf_plus(stderr, "The number of new sub keys: %d\n", + import_result->new_sub_keys); + fprintf_plus(stderr, "The number of new signatures: %d\n", + import_result->new_signatures); + fprintf_plus(stderr, "The number of new revocations: %d\n", + import_result->new_revocations); + fprintf_plus(stderr, + "The total number of secret keys read: %d\n", + import_result->secret_read); + fprintf_plus(stderr, + "The number of imported secret keys: %d\n", + import_result->secret_imported); + fprintf_plus(stderr, + "The number of unchanged secret keys: %d\n", + import_result->secret_unchanged); + fprintf_plus(stderr, "The number of keys not imported: %d\n", + import_result->not_imported); + for(gpgme_import_status_t import_status + = import_result->imports; + import_status != NULL; + import_status = import_status->next){ + fprintf_plus(stderr, "Import status for key: %s\n", + import_status->fpr); + if(import_status->result != GPG_ERR_NO_ERROR){ + fprintf_plus(stderr, "Import result: %s: %s\n", + gpgme_strsource(import_status->result), + gpgme_strerror(import_status->result)); + } + fprintf_plus(stderr, "Key status:\n"); + fprintf_plus(stderr, + import_status->status & GPGME_IMPORT_NEW + ? "The key was new.\n" + : "The key was not new.\n"); + fprintf_plus(stderr, + import_status->status & GPGME_IMPORT_UID + ? "The key contained new user IDs.\n" + : "The key did not contain new user IDs.\n"); + fprintf_plus(stderr, + import_status->status & GPGME_IMPORT_SIG + ? "The key contained new signatures.\n" + : "The key did not contain new signatures.\n"); + fprintf_plus(stderr, + import_status->status & GPGME_IMPORT_SUBKEY + ? "The key contained new sub keys.\n" + : "The key did not contain new sub keys.\n"); + fprintf_plus(stderr, + import_status->status & GPGME_IMPORT_SECRET + ? "The key contained a secret key.\n" + : "The key did not contain a secret key.\n"); + } + return false; + } + } + + ret = close(fd); + if(ret == -1){ + perror_plus("close"); + } + gpgme_data_release(pgp_data); + return true; + } + + if(debug){ + fprintf_plus(stderr, "Initializing GPGME\n"); } /* Init GPGME */ gpgme_check_version(NULL); - gpgme_engine_check_version(GPGME_PROTOCOL_OpenPGP); + rc = gpgme_engine_check_version(GPGME_PROTOCOL_OpenPGP); + if(rc != GPG_ERR_NO_ERROR){ + fprintf_plus(stderr, "bad gpgme_engine_check_version: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + return false; + } - /* Set GPGME home directory */ - rc = gpgme_get_engine_info (&engine_info); - if (rc != GPG_ERR_NO_ERROR){ - fprintf(stderr, "bad gpgme_get_engine_info: %s: %s\n", - gpgme_strsource(rc), gpgme_strerror(rc)); - return -1; + /* Set GPGME home directory for the OpenPGP engine only */ + rc = gpgme_get_engine_info(&engine_info); + if(rc != GPG_ERR_NO_ERROR){ + fprintf_plus(stderr, "bad gpgme_get_engine_info: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + return false; } while(engine_info != NULL){ if(engine_info->protocol == GPGME_PROTOCOL_OpenPGP){ gpgme_set_engine_info(GPGME_PROTOCOL_OpenPGP, - engine_info->file_name, homedir); + engine_info->file_name, tempdir); break; } engine_info = engine_info->next; } if(engine_info == NULL){ - fprintf(stderr, "Could not set home dir to %s\n", homedir); - return -1; - } - - /* Create new GPGME data buffer from packet buffer */ - rc = gpgme_data_new_from_mem(&dh_crypto, packet, packet_size, 0); - if (rc != GPG_ERR_NO_ERROR){ - fprintf(stderr, "bad gpgme_data_new_from_mem: %s: %s\n", - gpgme_strsource(rc), gpgme_strerror(rc)); + fprintf_plus(stderr, "Could not set GPGME home dir to %s\n", + tempdir); + return false; + } + + /* Create new GPGME "context" */ + rc = gpgme_new(&(mc->ctx)); + if(rc != GPG_ERR_NO_ERROR){ + fprintf_plus(stderr, "bad gpgme_new: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + return false; + } + + if(not import_key(pubkey) or not import_key(seckey)){ + return false; + } + + return true; +} + +/* + * Decrypt OpenPGP data. + * Returns -1 on error + */ +__attribute__((nonnull, warn_unused_result)) +static ssize_t pgp_packet_decrypt(const char *cryptotext, + size_t crypto_size, + char **plaintext, + mandos_context *mc){ + gpgme_data_t dh_crypto, dh_plain; + gpgme_error_t rc; + ssize_t ret; + size_t plaintext_capacity = 0; + ssize_t plaintext_length = 0; + + if(debug){ + fprintf_plus(stderr, "Trying to decrypt OpenPGP data\n"); + } + + /* Create new GPGME data buffer from memory cryptotext */ + rc = gpgme_data_new_from_mem(&dh_crypto, cryptotext, crypto_size, + 0); + if(rc != GPG_ERR_NO_ERROR){ + fprintf_plus(stderr, "bad gpgme_data_new_from_mem: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); return -1; } /* Create new empty GPGME data buffer for the plaintext */ rc = gpgme_data_new(&dh_plain); - if (rc != GPG_ERR_NO_ERROR){ - fprintf(stderr, "bad gpgme_data_new: %s: %s\n", - gpgme_strsource(rc), gpgme_strerror(rc)); - return -1; - } - - /* Create new GPGME "context" */ - rc = gpgme_new(&ctx); - if (rc != GPG_ERR_NO_ERROR){ - fprintf(stderr, "bad gpgme_new: %s: %s\n", - gpgme_strsource(rc), gpgme_strerror(rc)); - return -1; - } - - /* Decrypt data from the FILE pointer to the plaintext data - buffer */ - rc = gpgme_op_decrypt(ctx, dh_crypto, dh_plain); - if (rc != GPG_ERR_NO_ERROR){ - fprintf(stderr, "bad gpgme_op_decrypt: %s: %s\n", - gpgme_strsource(rc), gpgme_strerror(rc)); - return -1; - } + if(rc != GPG_ERR_NO_ERROR){ + fprintf_plus(stderr, "bad gpgme_data_new: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + gpgme_data_release(dh_crypto); + return -1; + } + + /* Decrypt data from the cryptotext data buffer to the plaintext + data buffer */ + rc = gpgme_op_decrypt(mc->ctx, dh_crypto, dh_plain); + if(rc != GPG_ERR_NO_ERROR){ + fprintf_plus(stderr, "bad gpgme_op_decrypt: %s: %s\n", + gpgme_strsource(rc), gpgme_strerror(rc)); + plaintext_length = -1; + if(debug){ + gpgme_decrypt_result_t result; + result = gpgme_op_decrypt_result(mc->ctx); + if(result == NULL){ + fprintf_plus(stderr, "gpgme_op_decrypt_result failed\n"); + } else { + if(result->unsupported_algorithm != NULL) { + fprintf_plus(stderr, "Unsupported algorithm: %s\n", + result->unsupported_algorithm); + } + fprintf_plus(stderr, "Wrong key usage: %s\n", + result->wrong_key_usage ? "Yes" : "No"); + if(result->file_name != NULL){ + fprintf_plus(stderr, "File name: %s\n", result->file_name); + } + for(gpgme_recipient_t r = result->recipients; r != NULL; + r = r->next){ + fprintf_plus(stderr, "Public key algorithm: %s\n", + gpgme_pubkey_algo_name(r->pubkey_algo)); + fprintf_plus(stderr, "Key ID: %s\n", r->keyid); + fprintf_plus(stderr, "Secret key available: %s\n", + r->status == GPG_ERR_NO_SECKEY ? "No" : "Yes"); + } + } + } + goto decrypt_end; + } + if(debug){ - fprintf(stderr, "Decryption of OpenPGP packet succeeded\n"); - } - - if (debug){ - gpgme_decrypt_result_t result; - result = gpgme_op_decrypt_result(ctx); - if (result == NULL){ - fprintf(stderr, "gpgme_op_decrypt_result failed\n"); - } else { - fprintf(stderr, "Unsupported algorithm: %s\n", - result->unsupported_algorithm); - fprintf(stderr, "Wrong key usage: %d\n", - result->wrong_key_usage); - if(result->file_name != NULL){ - fprintf(stderr, "File name: %s\n", result->file_name); - } - gpgme_recipient_t recipient; - recipient = result->recipients; - if(recipient){ - while(recipient != NULL){ - fprintf(stderr, "Public key algorithm: %s\n", - gpgme_pubkey_algo_name(recipient->pubkey_algo)); - fprintf(stderr, "Key ID: %s\n", recipient->keyid); - fprintf(stderr, "Secret key available: %s\n", - recipient->status == GPG_ERR_NO_SECKEY - ? "No" : "Yes"); - recipient = recipient->next; - } - } - } - } - - /* Delete the GPGME FILE pointer cryptotext data buffer */ - gpgme_data_release(dh_crypto); + fprintf_plus(stderr, "Decryption of OpenPGP data succeeded\n"); + } /* Seek back to the beginning of the GPGME plaintext data buffer */ - gpgme_data_seek(dh_plain, (off_t) 0, SEEK_SET); - - *new_packet = 0; + if(gpgme_data_seek(dh_plain, (off_t)0, SEEK_SET) == -1){ + perror_plus("gpgme_data_seek"); + plaintext_length = -1; + goto decrypt_end; + } + + *plaintext = NULL; while(true){ - if (new_packet_length + BUFFER_SIZE > new_packet_capacity){ - *new_packet = realloc(*new_packet, - (unsigned int)new_packet_capacity - + BUFFER_SIZE); - if (*new_packet == NULL){ - perror("realloc"); - return -1; - } - new_packet_capacity += BUFFER_SIZE; + plaintext_capacity = incbuffer(plaintext, + (size_t)plaintext_length, + plaintext_capacity); + if(plaintext_capacity == 0){ + perror_plus("incbuffer"); + plaintext_length = -1; + goto decrypt_end; } - ret = gpgme_data_read(dh_plain, *new_packet + new_packet_length, + ret = gpgme_data_read(dh_plain, *plaintext + plaintext_length, BUFFER_SIZE); /* Print the data, if any */ - if (ret == 0){ + if(ret == 0){ + /* EOF */ break; } if(ret < 0){ - perror("gpgme_data_read"); - return -1; - } - new_packet_length += ret; - } - - /* FIXME: check characters before printing to screen so to not print - terminal control characters */ - /* if(debug){ */ - /* fprintf(stderr, "decrypted password is: "); */ - /* fwrite(*new_packet, 1, new_packet_length, stderr); */ - /* fprintf(stderr, "\n"); */ - /* } */ + perror_plus("gpgme_data_read"); + plaintext_length = -1; + goto decrypt_end; + } + plaintext_length += ret; + } + + if(debug){ + fprintf_plus(stderr, "Decrypted password is: "); + for(ssize_t i = 0; i < plaintext_length; i++){ + fprintf(stderr, "%02hhX ", (*plaintext)[i]); + } + fprintf(stderr, "\n"); + } + + decrypt_end: + + /* Delete the GPGME cryptotext data buffer */ + gpgme_data_release(dh_crypto); /* Delete the GPGME plaintext data buffer */ gpgme_data_release(dh_plain); - return new_packet_length; -} - -static const char * safer_gnutls_strerror (int value) { - const char *ret = gnutls_strerror (value); - if (ret == NULL) - ret = "(unknown)"; - return ret; -} - -void debuggnutls(__attribute__((unused)) int level, - const char* string){ - fprintf(stderr, "%s", string); -} - -int initgnutls(encrypted_session *es){ - const char *err; + return plaintext_length; +} + +__attribute__((warn_unused_result, const)) +static const char *safe_string(const char *str){ + if(str == NULL) + return "(unknown)"; + return str; +} + +__attribute__((warn_unused_result)) +static const char *safer_gnutls_strerror(int value){ + const char *ret = gnutls_strerror(value); + return safe_string(ret); +} + +/* GnuTLS log function callback */ +__attribute__((nonnull)) +static void debuggnutls(__attribute__((unused)) int level, + const char* string){ + fprintf_plus(stderr, "GnuTLS: %s", string); +} + +__attribute__((nonnull(1, 2, 4), warn_unused_result)) +static int init_gnutls_global(const char *pubkeyfilename, + const char *seckeyfilename, + const char *dhparamsfilename, + mandos_context *mc){ int ret; if(debug){ - fprintf(stderr, "Initializing GnuTLS\n"); + fprintf_plus(stderr, "Initializing GnuTLS\n"); } - if ((ret = gnutls_global_init ()) - != GNUTLS_E_SUCCESS) { - fprintf (stderr, "global_init: %s\n", safer_gnutls_strerror(ret)); - return -1; - } - - if (debug){ + if(debug){ + /* "Use a log level over 10 to enable all debugging options." + * - GnuTLS manual + */ gnutls_global_set_log_level(11); gnutls_global_set_log_function(debuggnutls); } - /* openpgp credentials */ - if ((ret = gnutls_certificate_allocate_credentials (&es->cred)) - != GNUTLS_E_SUCCESS) { - fprintf (stderr, "memory error: %s\n", - safer_gnutls_strerror(ret)); + /* OpenPGP credentials */ + ret = gnutls_certificate_allocate_credentials(&mc->cred); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "GnuTLS memory error: %s\n", + safer_gnutls_strerror(ret)); return -1; } if(debug){ - fprintf(stderr, "Attempting to use OpenPGP certificate %s" - " and keyfile %s as GnuTLS credentials\n", CERTFILE, - KEYFILE); + fprintf_plus(stderr, "Attempting to use public key %s and" + " private key %s as GnuTLS credentials\n", + pubkeyfilename, + seckeyfilename); } +#if GNUTLS_VERSION_NUMBER >= 0x030606 + ret = gnutls_certificate_set_rawpk_key_file + (mc->cred, pubkeyfilename, seckeyfilename, + GNUTLS_X509_FMT_PEM, /* format */ + NULL, /* pass */ + /* key_usage */ + GNUTLS_KEY_DIGITAL_SIGNATURE | GNUTLS_KEY_KEY_ENCIPHERMENT, + NULL, /* names */ + 0, /* names_length */ + /* privkey_flags */ + GNUTLS_PKCS_PLAIN | GNUTLS_PKCS_NULL_PASSWORD, + 0); /* pkcs11_flags */ +#elif GNUTLS_VERSION_NUMBER < 0x030600 ret = gnutls_certificate_set_openpgp_key_file - (es->cred, CERTFILE, KEYFILE, GNUTLS_OPENPGP_FMT_BASE64); - if (ret != GNUTLS_E_SUCCESS) { - fprintf - (stderr, "Error[%d] while reading the OpenPGP key pair ('%s'," - " '%s')\n", - ret, CERTFILE, KEYFILE); - fprintf(stdout, "The Error is: %s\n", - safer_gnutls_strerror(ret)); - return -1; - } - - //GnuTLS server initialization - if ((ret = gnutls_dh_params_init (&es->dh_params)) - != GNUTLS_E_SUCCESS) { - fprintf (stderr, "Error in dh parameter initialization: %s\n", - safer_gnutls_strerror(ret)); - return -1; - } - - if ((ret = gnutls_dh_params_generate2 (es->dh_params, DH_BITS)) - != GNUTLS_E_SUCCESS) { - fprintf (stderr, "Error in prime generation: %s\n", - safer_gnutls_strerror(ret)); - return -1; - } - - gnutls_certificate_set_dh_params (es->cred, es->dh_params); - - // GnuTLS session creation - if ((ret = gnutls_init (&es->session, GNUTLS_SERVER)) - != GNUTLS_E_SUCCESS){ - fprintf(stderr, "Error in GnuTLS session initialization: %s\n", - safer_gnutls_strerror(ret)); - } - - if ((ret = gnutls_priority_set_direct (es->session, "NORMAL", &err)) - != GNUTLS_E_SUCCESS) { - fprintf(stderr, "Syntax error at: %s\n", err); - fprintf(stderr, "GnuTLS error: %s\n", - safer_gnutls_strerror(ret)); - return -1; - } - - if ((ret = gnutls_credentials_set - (es->session, GNUTLS_CRD_CERTIFICATE, es->cred)) - != GNUTLS_E_SUCCESS) { - fprintf(stderr, "Error setting a credentials set: %s\n", - safer_gnutls_strerror(ret)); + (mc->cred, pubkeyfilename, seckeyfilename, + GNUTLS_OPENPGP_FMT_BASE64); +#else +#error "Needs GnuTLS 3.6.6 or later, or before 3.6.0" +#endif + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, + "Error[%d] while reading the key pair ('%s'," + " '%s')\n", ret, pubkeyfilename, seckeyfilename); + fprintf_plus(stderr, "The GnuTLS error is: %s\n", + safer_gnutls_strerror(ret)); + goto globalfail; + } + + /* GnuTLS server initialization */ + ret = gnutls_dh_params_init(&mc->dh_params); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Error in GnuTLS DH parameter" + " initialization: %s\n", + safer_gnutls_strerror(ret)); + goto globalfail; + } + /* If a Diffie-Hellman parameters file was given, try to use it */ + if(dhparamsfilename != NULL){ + gnutls_datum_t params = { .data = NULL, .size = 0 }; + do { + int dhpfile = open(dhparamsfilename, O_RDONLY); + if(dhpfile == -1){ + perror_plus("open"); + dhparamsfilename = NULL; + break; + } + size_t params_capacity = 0; + while(true){ + params_capacity = incbuffer((char **)¶ms.data, + (size_t)params.size, + (size_t)params_capacity); + if(params_capacity == 0){ + perror_plus("incbuffer"); + free(params.data); + params.data = NULL; + dhparamsfilename = NULL; + break; + } + ssize_t bytes_read = read(dhpfile, + params.data + params.size, + BUFFER_SIZE); + /* EOF */ + if(bytes_read == 0){ + break; + } + /* check bytes_read for failure */ + if(bytes_read < 0){ + perror_plus("read"); + free(params.data); + params.data = NULL; + dhparamsfilename = NULL; + break; + } + params.size += (unsigned int)bytes_read; + } + ret = close(dhpfile); + if(ret == -1){ + perror_plus("close"); + } + if(params.data == NULL){ + dhparamsfilename = NULL; + } + if(dhparamsfilename == NULL){ + break; + } + ret = gnutls_dh_params_import_pkcs3(mc->dh_params, ¶ms, + GNUTLS_X509_FMT_PEM); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Failed to parse DH parameters in file" + " \"%s\": %s\n", dhparamsfilename, + safer_gnutls_strerror(ret)); + dhparamsfilename = NULL; + } + free(params.data); + } while(false); + } + if(dhparamsfilename == NULL){ + if(mc->dh_bits == 0){ +#if GNUTLS_VERSION_NUMBER < 0x030600 + /* Find out the optimal number of DH bits */ + /* Try to read the private key file */ + gnutls_datum_t buffer = { .data = NULL, .size = 0 }; + do { + int secfile = open(seckeyfilename, O_RDONLY); + if(secfile == -1){ + perror_plus("open"); + break; + } + size_t buffer_capacity = 0; + while(true){ + buffer_capacity = incbuffer((char **)&buffer.data, + (size_t)buffer.size, + (size_t)buffer_capacity); + if(buffer_capacity == 0){ + perror_plus("incbuffer"); + free(buffer.data); + buffer.data = NULL; + break; + } + ssize_t bytes_read = read(secfile, + buffer.data + buffer.size, + BUFFER_SIZE); + /* EOF */ + if(bytes_read == 0){ + break; + } + /* check bytes_read for failure */ + if(bytes_read < 0){ + perror_plus("read"); + free(buffer.data); + buffer.data = NULL; + break; + } + buffer.size += (unsigned int)bytes_read; + } + close(secfile); + } while(false); + /* If successful, use buffer to parse private key */ + gnutls_sec_param_t sec_param = GNUTLS_SEC_PARAM_ULTRA; + if(buffer.data != NULL){ + { + gnutls_openpgp_privkey_t privkey = NULL; + ret = gnutls_openpgp_privkey_init(&privkey); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Error initializing OpenPGP key" + " structure: %s", + safer_gnutls_strerror(ret)); + free(buffer.data); + buffer.data = NULL; + } else { + ret = gnutls_openpgp_privkey_import + (privkey, &buffer, GNUTLS_OPENPGP_FMT_BASE64, "", 0); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Error importing OpenPGP key : %s", + safer_gnutls_strerror(ret)); + privkey = NULL; + } + free(buffer.data); + buffer.data = NULL; + if(privkey != NULL){ + /* Use private key to suggest an appropriate + sec_param */ + sec_param = gnutls_openpgp_privkey_sec_param(privkey); + gnutls_openpgp_privkey_deinit(privkey); + if(debug){ + fprintf_plus(stderr, "This OpenPGP key implies using" + " a GnuTLS security parameter \"%s\".\n", + safe_string(gnutls_sec_param_get_name + (sec_param))); + } + } + } + } + if(sec_param == GNUTLS_SEC_PARAM_UNKNOWN){ + /* Err on the side of caution */ + sec_param = GNUTLS_SEC_PARAM_ULTRA; + if(debug){ + fprintf_plus(stderr, "Falling back to security parameter" + " \"%s\"\n", + safe_string(gnutls_sec_param_get_name + (sec_param))); + } + } + } + unsigned int uret = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, sec_param); + if(uret != 0){ + mc->dh_bits = uret; + if(debug){ + fprintf_plus(stderr, "A \"%s\" GnuTLS security parameter" + " implies %u DH bits; using that.\n", + safe_string(gnutls_sec_param_get_name + (sec_param)), + mc->dh_bits); + } + } else { + fprintf_plus(stderr, "Failed to get implied number of DH" + " bits for security parameter \"%s\"): %s\n", + safe_string(gnutls_sec_param_get_name + (sec_param)), + safer_gnutls_strerror(ret)); + goto globalfail; + } +#endif + } else { /* dh_bits != 0 */ + if(debug){ + fprintf_plus(stderr, "DH bits explicitly set to %u\n", + mc->dh_bits); + } + ret = gnutls_dh_params_generate2(mc->dh_params, mc->dh_bits); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Error in GnuTLS prime generation (%u" + " bits): %s\n", mc->dh_bits, + safer_gnutls_strerror(ret)); + goto globalfail; + } + gnutls_certificate_set_dh_params(mc->cred, mc->dh_params); + } + } + + return 0; + + globalfail: + + gnutls_certificate_free_credentials(mc->cred); + gnutls_dh_params_deinit(mc->dh_params); + return -1; +} + +__attribute__((nonnull, warn_unused_result)) +static int init_gnutls_session(gnutls_session_t *session, + mandos_context *mc){ + int ret; + /* GnuTLS session creation */ + do { + ret = gnutls_init(session, (GNUTLS_SERVER +#if GNUTLS_VERSION_NUMBER >= 0x030506 + | GNUTLS_NO_TICKETS +#endif +#if GNUTLS_VERSION_NUMBER >= 0x030606 + | GNUTLS_ENABLE_RAWPK +#endif + )); + if(quit_now){ + return -1; + } + } while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, + "Error in GnuTLS session initialization: %s\n", + safer_gnutls_strerror(ret)); + } + + { + const char *err; + do { + ret = gnutls_priority_set_direct(*session, mc->priority, &err); + if(quit_now){ + gnutls_deinit(*session); + return -1; + } + } while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Syntax error at: %s\n", err); + fprintf_plus(stderr, "GnuTLS error: %s\n", + safer_gnutls_strerror(ret)); + gnutls_deinit(*session); + return -1; + } + } + + do { + ret = gnutls_credentials_set(*session, GNUTLS_CRD_CERTIFICATE, + mc->cred); + if(quit_now){ + gnutls_deinit(*session); + return -1; + } + } while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Error setting GnuTLS credentials: %s\n", + safer_gnutls_strerror(ret)); + gnutls_deinit(*session); return -1; } /* ignore client certificate if any. */ - gnutls_certificate_server_set_request (es->session, - GNUTLS_CERT_IGNORE); - - gnutls_dh_set_prime_bits (es->session, DH_BITS); + gnutls_certificate_server_set_request(*session, GNUTLS_CERT_IGNORE); return 0; } -void empty_log(__attribute__((unused)) AvahiLogLevel level, - __attribute__((unused)) const char *txt){} - -int start_mandos_communication(const char *ip, uint16_t port, - unsigned int if_index){ - int ret, tcp_sd; - struct sockaddr_in6 to; - encrypted_session es; +/* Avahi log function callback */ +static void empty_log(__attribute__((unused)) AvahiLogLevel level, + __attribute__((unused)) const char *txt){} + +/* Helper function to add_local_route() and delete_local_route() */ +__attribute__((nonnull, warn_unused_result)) +static bool add_delete_local_route(const bool add, + const char *address, + AvahiIfIndex if_index){ + int ret; + char helper[] = "mandos-client-iprouteadddel"; + char add_arg[] = "add"; + char delete_arg[] = "delete"; + char debug_flag[] = "--debug"; + char *pluginhelperdir = getenv("MANDOSPLUGINHELPERDIR"); + if(pluginhelperdir == NULL){ + if(debug){ + fprintf_plus(stderr, "MANDOSPLUGINHELPERDIR environment" + " variable not set; cannot run helper\n"); + } + return false; + } + + char interface[IF_NAMESIZE]; + if(if_indextoname((unsigned int)if_index, interface) == NULL){ + perror_plus("if_indextoname"); + return false; + } + + int devnull = (int)TEMP_FAILURE_RETRY(open("/dev/null", O_RDONLY)); + if(devnull == -1){ + perror_plus("open(\"/dev/null\", O_RDONLY)"); + return false; + } + pid_t pid = fork(); + if(pid == 0){ + /* Child */ + /* Raise privileges */ + errno = raise_privileges_permanently(); + if(errno != 0){ + perror_plus("Failed to raise privileges"); + /* _exit(EX_NOPERM); */ + } else { + /* Set group */ + errno = 0; + ret = setgid(0); + if(ret == -1){ + perror_plus("setgid"); + _exit(EX_NOPERM); + } + /* Reset supplementary groups */ + errno = 0; + ret = setgroups(0, NULL); + if(ret == -1){ + perror_plus("setgroups"); + _exit(EX_NOPERM); + } + } + ret = dup2(devnull, STDIN_FILENO); + if(ret == -1){ + perror_plus("dup2(devnull, STDIN_FILENO)"); + _exit(EX_OSERR); + } + ret = close(devnull); + if(ret == -1){ + perror_plus("close"); + _exit(EX_OSERR); + } + ret = dup2(STDERR_FILENO, STDOUT_FILENO); + if(ret == -1){ + perror_plus("dup2(STDERR_FILENO, STDOUT_FILENO)"); + _exit(EX_OSERR); + } + int helperdir_fd = (int)TEMP_FAILURE_RETRY(open(pluginhelperdir, + O_RDONLY + | O_DIRECTORY + | O_PATH + | O_CLOEXEC)); + if(helperdir_fd == -1){ + perror_plus("open"); + _exit(EX_UNAVAILABLE); + } + int helper_fd = (int)TEMP_FAILURE_RETRY(openat(helperdir_fd, + helper, O_RDONLY)); + if(helper_fd == -1){ + perror_plus("openat"); + close(helperdir_fd); + _exit(EX_UNAVAILABLE); + } + close(helperdir_fd); +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + if(fexecve(helper_fd, (char *const []) + { helper, add ? add_arg : delete_arg, (char *)address, + interface, debug ? debug_flag : NULL, NULL }, + environ) == -1){ +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + perror_plus("fexecve"); + _exit(EXIT_FAILURE); + } + } + if(pid == -1){ + perror_plus("fork"); + return false; + } + int status; + pid_t pret = -1; + errno = 0; + do { + pret = waitpid(pid, &status, 0); + if(pret == -1 and errno == EINTR and quit_now){ + int errno_raising = 0; + if((errno = raise_privileges()) != 0){ + errno_raising = errno; + perror_plus("Failed to raise privileges in order to" + " kill helper program"); + } + if(kill(pid, SIGTERM) == -1){ + perror_plus("kill"); + } + if((errno_raising == 0) and (errno = lower_privileges()) != 0){ + perror_plus("Failed to lower privileges after killing" + " helper program"); + } + return false; + } + } while(pret == -1 and errno == EINTR); + if(pret == -1){ + perror_plus("waitpid"); + return false; + } + if(WIFEXITED(status)){ + if(WEXITSTATUS(status) != 0){ + fprintf_plus(stderr, "Error: iprouteadddel exited" + " with status %d\n", WEXITSTATUS(status)); + return false; + } + return true; + } + if(WIFSIGNALED(status)){ + fprintf_plus(stderr, "Error: iprouteadddel died by" + " signal %d\n", WTERMSIG(status)); + return false; + } + fprintf_plus(stderr, "Error: iprouteadddel crashed\n"); + return false; +} + +__attribute__((nonnull, warn_unused_result)) +static bool add_local_route(const char *address, + AvahiIfIndex if_index){ + if(debug){ + fprintf_plus(stderr, "Adding route to %s\n", address); + } + return add_delete_local_route(true, address, if_index); +} + +__attribute__((nonnull, warn_unused_result)) +static bool delete_local_route(const char *address, + AvahiIfIndex if_index){ + if(debug){ + fprintf_plus(stderr, "Removing route to %s\n", address); + } + return add_delete_local_route(false, address, if_index); +} + +/* Called when a Mandos server is found */ +__attribute__((nonnull, warn_unused_result)) +static int start_mandos_communication(const char *ip, in_port_t port, + AvahiIfIndex if_index, + int af, mandos_context *mc){ + int ret, tcp_sd = -1; + ssize_t sret; + struct sockaddr_storage to; char *buffer = NULL; - char *decrypted_buffer; + char *decrypted_buffer = NULL; size_t buffer_length = 0; size_t buffer_capacity = 0; - ssize_t decrypted_buffer_size; - size_t written = 0; - int retval = 0; - char interface[IF_NAMESIZE]; - - if(debug){ - fprintf(stderr, "Setting up a tcp connection to %s\n", ip); - } - - tcp_sd = socket(PF_INET6, SOCK_STREAM, 0); - if(tcp_sd < 0) { - perror("socket"); - return -1; - } - - if(if_indextoname(if_index, interface) == NULL){ - if(debug){ - perror("if_indextoname"); - } - return -1; - } - - if(debug){ - fprintf(stderr, "Binding to interface %s\n", interface); - } - - memset(&to,0,sizeof(to)); /* Spurious warning */ - to.sin6_family = AF_INET6; - ret = inet_pton(AF_INET6, ip, &to.sin6_addr); - if (ret < 0 ){ - perror("inet_pton"); - return -1; - } + size_t written; + int retval = -1; + gnutls_session_t session; + int pf; /* Protocol family */ + bool route_added = false; + + errno = 0; + + if(quit_now){ + errno = EINTR; + return -1; + } + + switch(af){ + case AF_INET6: + pf = PF_INET6; + break; + case AF_INET: + pf = PF_INET; + break; + default: + fprintf_plus(stderr, "Bad address family: %d\n", af); + errno = EINVAL; + return -1; + } + + /* If the interface is specified and we have a list of interfaces */ + if(if_index != AVAHI_IF_UNSPEC and mc->interfaces != NULL){ + /* Check if the interface is one of the interfaces we are using */ + bool match = false; + { + char *interface = NULL; + while((interface = argz_next(mc->interfaces, + mc->interfaces_size, + interface))){ + if(if_nametoindex(interface) == (unsigned int)if_index){ + match = true; + break; + } + } + } + if(not match){ + /* This interface does not match any in the list, so we don't + connect to the server */ + if(debug){ + char interface[IF_NAMESIZE]; + if(if_indextoname((unsigned int)if_index, interface) == NULL){ + perror_plus("if_indextoname"); + } else { + fprintf_plus(stderr, "Skipping server on non-used interface" + " \"%s\"\n", + if_indextoname((unsigned int)if_index, + interface)); + } + } + return -1; + } + } + + ret = init_gnutls_session(&session, mc); + if(ret != 0){ + return -1; + } + + if(debug){ + fprintf_plus(stderr, "Setting up a TCP connection to %s, port %" + PRIuMAX "\n", ip, (uintmax_t)port); + } + + tcp_sd = socket(pf, SOCK_STREAM | SOCK_CLOEXEC, 0); + if(tcp_sd < 0){ + int e = errno; + perror_plus("socket"); + errno = e; + goto mandos_end; + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + if(af == AF_INET6){ + struct sockaddr_in6 *to6 = (struct sockaddr_in6 *)&to; + *to6 = (struct sockaddr_in6){ .sin6_family = (sa_family_t)af }; + ret = inet_pton(af, ip, &to6->sin6_addr); + } else { /* IPv4 */ + struct sockaddr_in *to4 = (struct sockaddr_in *)&to; + *to4 = (struct sockaddr_in){ .sin_family = (sa_family_t)af }; + ret = inet_pton(af, ip, &to4->sin_addr); + } + if(ret < 0 ){ + int e = errno; + perror_plus("inet_pton"); + errno = e; + goto mandos_end; + } if(ret == 0){ - fprintf(stderr, "Bad address: %s\n", ip); - return -1; - } - to.sin6_port = htons(port); /* Spurious warning */ - - to.sin6_scope_id = (uint32_t)if_index; - - if(debug){ - fprintf(stderr, "Connection to: %s\n", ip); - } - - ret = connect(tcp_sd, (struct sockaddr *) &to, sizeof(to)); - if (ret < 0){ - perror("connect"); - return -1; - } - - ret = initgnutls (&es); - if (ret != 0){ - retval = -1; - return -1; - } - - gnutls_transport_set_ptr (es.session, - (gnutls_transport_ptr_t) tcp_sd); - - if(debug){ - fprintf(stderr, "Establishing TLS session with %s\n", ip); - } - - ret = gnutls_handshake (es.session); - - if (ret != GNUTLS_E_SUCCESS){ + int e = errno; + fprintf_plus(stderr, "Bad address: %s\n", ip); + errno = e; + goto mandos_end; + } + if(af == AF_INET6){ + ((struct sockaddr_in6 *)&to)->sin6_port = htons(port); + if(IN6_IS_ADDR_LINKLOCAL + (&((struct sockaddr_in6 *)&to)->sin6_addr)){ + if(if_index == AVAHI_IF_UNSPEC){ + fprintf_plus(stderr, "An IPv6 link-local address is" + " incomplete without a network interface\n"); + errno = EINVAL; + goto mandos_end; + } + /* Set the network interface number as scope */ + ((struct sockaddr_in6 *)&to)->sin6_scope_id = (uint32_t)if_index; + } + } else { + ((struct sockaddr_in *)&to)->sin_port = htons(port); + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + if(debug){ + if(af == AF_INET6 and if_index != AVAHI_IF_UNSPEC){ + char interface[IF_NAMESIZE]; + if(if_indextoname((unsigned int)if_index, interface) == NULL){ + perror_plus("if_indextoname"); + } else { + fprintf_plus(stderr, "Connection to: %s%%%s, port %" PRIuMAX + "\n", ip, interface, (uintmax_t)port); + } + } else { + fprintf_plus(stderr, "Connection to: %s, port %" PRIuMAX "\n", + ip, (uintmax_t)port); + } + char addrstr[(INET_ADDRSTRLEN > INET6_ADDRSTRLEN) ? + INET_ADDRSTRLEN : INET6_ADDRSTRLEN] = ""; + if(af == AF_INET6){ + ret = getnameinfo((struct sockaddr *)&to, + sizeof(struct sockaddr_in6), + addrstr, sizeof(addrstr), NULL, 0, + NI_NUMERICHOST); + } else { + ret = getnameinfo((struct sockaddr *)&to, + sizeof(struct sockaddr_in), + addrstr, sizeof(addrstr), NULL, 0, + NI_NUMERICHOST); + } + if(ret == EAI_SYSTEM){ + perror_plus("getnameinfo"); + } else if(ret != 0) { + fprintf_plus(stderr, "getnameinfo: %s", gai_strerror(ret)); + } else if(strcmp(addrstr, ip) != 0){ + fprintf_plus(stderr, "Canonical address form: %s\n", addrstr); + } + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + while(true){ + if(af == AF_INET6){ + ret = connect(tcp_sd, (struct sockaddr *)&to, + sizeof(struct sockaddr_in6)); + } else { + ret = connect(tcp_sd, (struct sockaddr *)&to, /* IPv4 */ + sizeof(struct sockaddr_in)); + } + if(ret < 0){ + if(((errno == ENETUNREACH) or (errno == EHOSTUNREACH)) + and if_index != AVAHI_IF_UNSPEC + and connect_to == NULL + and not route_added and + ((af == AF_INET6 and not + IN6_IS_ADDR_LINKLOCAL(&(((struct sockaddr_in6 *) + &to)->sin6_addr))) + or (af == AF_INET and + /* Not a a IPv4LL address */ + (ntohl(((struct sockaddr_in *)&to)->sin_addr.s_addr) + & 0xFFFF0000L) != 0xA9FE0000L))){ + /* Work around Avahi bug - Avahi does not announce link-local + addresses if it has a global address, so local hosts with + *only* a link-local address (e.g. Mandos clients) cannot + connect to a Mandos server announced by Avahi on a server + host with a global address. Work around this by retrying + with an explicit route added with the server's address. + + Avahi bug reference: + https://lists.freedesktop.org/archives/avahi/2010-February/001833.html + https://bugs.debian.org/587961 + */ + if(debug){ + fprintf_plus(stderr, "Mandos server unreachable, trying" + " direct route\n"); + } + int e = errno; + route_added = add_local_route(ip, if_index); + if(route_added){ + continue; + } + errno = e; + } + if(errno != ECONNREFUSED or debug){ + int e = errno; + perror_plus("connect"); + errno = e; + } + goto mandos_end; + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + break; + } + + const char *out = mandos_protocol_version; + written = 0; + while(true){ + size_t out_size = strlen(out); + ret = (int)TEMP_FAILURE_RETRY(write(tcp_sd, out + written, + out_size - written)); + if(ret == -1){ + int e = errno; + perror_plus("write"); + errno = e; + goto mandos_end; + } + written += (size_t)ret; + if(written < out_size){ + continue; + } else { + if(out == mandos_protocol_version){ + written = 0; + out = "\r\n"; + } else { + break; + } + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + } + + if(debug){ + fprintf_plus(stderr, "Establishing TLS session with %s\n", ip); + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + /* This casting via intptr_t is to eliminate warning about casting + an int to a pointer type. This is exactly how the GnuTLS Guile + function "set-session-transport-fd!" does it. */ + gnutls_transport_set_ptr(session, + (gnutls_transport_ptr_t)(intptr_t)tcp_sd); + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + do { + ret = gnutls_handshake(session); + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); + + if(ret != GNUTLS_E_SUCCESS){ if(debug){ - fprintf(stderr, "\n*** Handshake failed ***\n"); - gnutls_perror (ret); + fprintf_plus(stderr, "*** GnuTLS Handshake failed ***\n"); + gnutls_perror(ret); } - retval = -1; - goto exit; + errno = EPROTO; + goto mandos_end; } - //Retrieve OpenPGP packet that contains the wanted password + /* Read OpenPGP packet that contains the wanted password */ if(debug){ - fprintf(stderr, "Retrieving pgp encrypted password from %s\n", - ip); + fprintf_plus(stderr, "Retrieving OpenPGP encrypted password from" + " %s\n", ip); } - + while(true){ - if (buffer_length + BUFFER_SIZE > buffer_capacity){ - buffer = realloc(buffer, buffer_capacity + BUFFER_SIZE); - if (buffer == NULL){ - perror("realloc"); - goto exit; - } - buffer_capacity += BUFFER_SIZE; - } - - ret = gnutls_record_recv - (es.session, buffer+buffer_length, BUFFER_SIZE); - if (ret == 0){ + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + buffer_capacity = incbuffer(&buffer, buffer_length, + buffer_capacity); + if(buffer_capacity == 0){ + int e = errno; + perror_plus("incbuffer"); + errno = e; + goto mandos_end; + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + sret = gnutls_record_recv(session, buffer+buffer_length, + BUFFER_SIZE); + if(sret == 0){ break; } - if (ret < 0){ - switch(ret){ + if(sret < 0){ + switch(sret){ case GNUTLS_E_INTERRUPTED: case GNUTLS_E_AGAIN: break; case GNUTLS_E_REHANDSHAKE: - ret = gnutls_handshake (es.session); - if (ret < 0){ - fprintf(stderr, "\n*** Handshake failed ***\n"); - gnutls_perror (ret); - retval = -1; - goto exit; + do { + ret = gnutls_handshake(session); + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); + if(ret < 0){ + fprintf_plus(stderr, "*** GnuTLS Re-handshake failed " + "***\n"); + gnutls_perror(ret); + errno = EPROTO; + goto mandos_end; } break; default: - fprintf(stderr, "Unknown error while reading data from" - " encrypted session with mandos server\n"); - retval = -1; - gnutls_bye (es.session, GNUTLS_SHUT_RDWR); - goto exit; + fprintf_plus(stderr, "Unknown error while reading data from" + " encrypted session with Mandos server\n"); + gnutls_bye(session, GNUTLS_SHUT_RDWR); + errno = EIO; + goto mandos_end; } } else { - buffer_length += (size_t) ret; - } - } - - if (buffer_length > 0){ - decrypted_buffer_size = pgp_packet_decrypt(buffer, - buffer_length, - &decrypted_buffer, - CERT_ROOT); - if (decrypted_buffer_size >= 0){ - while(written < decrypted_buffer_size){ - ret = (int)fwrite (decrypted_buffer + written, 1, - (size_t)decrypted_buffer_size - written, - stdout); + buffer_length += (size_t) sret; + } + } + + if(debug){ + fprintf_plus(stderr, "Closing TLS session\n"); + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + do { + ret = gnutls_bye(session, GNUTLS_SHUT_RDWR); + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); + + if(buffer_length > 0){ + ssize_t decrypted_buffer_size; + decrypted_buffer_size = pgp_packet_decrypt(buffer, buffer_length, + &decrypted_buffer, mc); + if(decrypted_buffer_size >= 0){ + + clearerr(stdout); + written = 0; + while(written < (size_t) decrypted_buffer_size){ + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + + ret = (int)fwrite(decrypted_buffer + written, 1, + (size_t)decrypted_buffer_size - written, + stdout); if(ret == 0 and ferror(stdout)){ + int e = errno; if(debug){ - fprintf(stderr, "Error writing encrypted data: %s\n", - strerror(errno)); + fprintf_plus(stderr, "Error writing encrypted data: %s\n", + strerror(errno)); } - retval = -1; - break; + errno = e; + goto mandos_end; } written += (size_t)ret; } - free(decrypted_buffer); - } else { + ret = fflush(stdout); + if(ret != 0){ + int e = errno; + if(debug){ + fprintf_plus(stderr, "Error writing encrypted data: %s\n", + strerror(errno)); + } + errno = e; + goto mandos_end; + } + retval = 0; + } + } + + /* Shutdown procedure */ + + mandos_end: + { + if(route_added){ + if(not delete_local_route(ip, if_index)){ + fprintf_plus(stderr, "Failed to delete local route to %s on" + " interface %d", ip, if_index); + } + } + int e = errno; + free(decrypted_buffer); + free(buffer); + if(tcp_sd >= 0){ + ret = close(tcp_sd); + } + if(ret == -1){ + if(e == 0){ + e = errno; + } + perror_plus("close"); + } + gnutls_deinit(session); + errno = e; + if(quit_now){ + errno = EINTR; retval = -1; } } - - //shutdown procedure - - if(debug){ - fprintf(stderr, "Closing TLS session\n"); - } - - free(buffer); - gnutls_bye (es.session, GNUTLS_SHUT_RDWR); - exit: - close(tcp_sd); - gnutls_deinit (es.session); - gnutls_certificate_free_credentials (es.cred); - gnutls_global_deinit (); return retval; } -static AvahiSimplePoll *simple_poll = NULL; -static AvahiServer *server = NULL; - -static void resolve_callback( - AvahiSServiceResolver *r, - AvahiIfIndex interface, - AVAHI_GCC_UNUSED AvahiProtocol protocol, - AvahiResolverEvent event, - const char *name, - const char *type, - const char *domain, - const char *host_name, - const AvahiAddress *address, - uint16_t port, - AVAHI_GCC_UNUSED AvahiStringList *txt, - AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, - AVAHI_GCC_UNUSED void* userdata) { - - assert(r); /* Spurious warning */ +static void resolve_callback(AvahiSServiceResolver *r, + AvahiIfIndex interface, + AvahiProtocol proto, + AvahiResolverEvent event, + const char *name, + const char *type, + const char *domain, + const char *host_name, + const AvahiAddress *address, + uint16_t port, + AVAHI_GCC_UNUSED AvahiStringList *txt, + AVAHI_GCC_UNUSED AvahiLookupResultFlags + flags, + void *mc){ + if(r == NULL){ + return; + } /* Called whenever a service has been resolved successfully or timed out */ - switch (event) { + if(quit_now){ + avahi_s_service_resolver_free(r); + return; + } + + switch(event){ default: case AVAHI_RESOLVER_FAILURE: - fprintf(stderr, "(Resolver) Failed to resolve service '%s' of" - " type '%s' in domain '%s': %s\n", name, type, domain, - avahi_strerror(avahi_server_errno(server))); + fprintf_plus(stderr, "(Avahi Resolver) Failed to resolve service " + "'%s' of type '%s' in domain '%s': %s\n", name, type, + domain, + avahi_strerror(avahi_server_errno + (((mandos_context*)mc)->server))); break; case AVAHI_RESOLVER_FOUND: @@ -553,174 +1696,1654 @@ char ip[AVAHI_ADDRESS_STR_MAX]; avahi_address_snprint(ip, sizeof(ip), address); if(debug){ - fprintf(stderr, "Mandos server \"%s\" found on %s (%s) on" - " port %d\n", name, host_name, ip, port); + fprintf_plus(stderr, "Mandos server \"%s\" found on %s (%s, %" + PRIdMAX ") on port %" PRIu16 "\n", name, + host_name, ip, (intmax_t)interface, port); } - int ret = start_mandos_communication(ip, port, - (unsigned int) interface); - if (ret == 0){ - exit(EXIT_SUCCESS); + int ret = start_mandos_communication(ip, (in_port_t)port, + interface, + avahi_proto_to_af(proto), + mc); + if(ret == 0){ + avahi_simple_poll_quit(simple_poll); + } else { + if(not add_server(ip, (in_port_t)port, interface, + avahi_proto_to_af(proto), + &((mandos_context*)mc)->current_server)){ + fprintf_plus(stderr, "Failed to add server \"%s\" to server" + " list\n", name); + } } } } avahi_s_service_resolver_free(r); } -static void browse_callback( - AvahiSServiceBrowser *b, - AvahiIfIndex interface, - AvahiProtocol protocol, - AvahiBrowserEvent event, - const char *name, - const char *type, - const char *domain, - AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, - void* userdata) { - - AvahiServer *s = userdata; - assert(b); /* Spurious warning */ - - /* Called whenever a new services becomes available on the LAN or - is removed from the LAN */ - - switch (event) { +static void browse_callback(AvahiSServiceBrowser *b, + AvahiIfIndex interface, + AvahiProtocol protocol, + AvahiBrowserEvent event, + const char *name, + const char *type, + const char *domain, + AVAHI_GCC_UNUSED AvahiLookupResultFlags + flags, + void *mc){ + if(b == NULL){ + return; + } + + /* Called whenever a new services becomes available on the LAN or + is removed from the LAN */ + + if(quit_now){ + return; + } + + switch(event){ + default: + case AVAHI_BROWSER_FAILURE: + + fprintf_plus(stderr, "(Avahi browser) %s\n", + avahi_strerror(avahi_server_errno + (((mandos_context*)mc)->server))); + avahi_simple_poll_quit(simple_poll); + return; + + case AVAHI_BROWSER_NEW: + /* We ignore the returned Avahi resolver object. In the callback + function we free it. If the Avahi server is terminated before + the callback function is called the Avahi server will free the + resolver for us. */ + + if(avahi_s_service_resolver_new(((mandos_context*)mc)->server, + interface, protocol, name, type, + domain, protocol, 0, + resolve_callback, mc) == NULL) + fprintf_plus(stderr, "Avahi: Failed to resolve service '%s':" + " %s\n", name, + avahi_strerror(avahi_server_errno + (((mandos_context*)mc)->server))); + break; + + case AVAHI_BROWSER_REMOVE: + break; + + case AVAHI_BROWSER_ALL_FOR_NOW: + case AVAHI_BROWSER_CACHE_EXHAUSTED: + if(debug){ + fprintf_plus(stderr, "No Mandos server found, still" + " searching...\n"); + } + break; + } +} + +/* Signal handler that stops main loop after SIGTERM */ +static void handle_sigterm(int sig){ + if(quit_now){ + return; + } + quit_now = 1; + signal_received = sig; + int old_errno = errno; + /* set main loop to exit */ + if(simple_poll != NULL){ + avahi_simple_poll_quit(simple_poll); + } + errno = old_errno; +} + +__attribute__((nonnull, warn_unused_result)) +bool get_flags(const char *ifname, struct ifreq *ifr){ + int ret; + int old_errno; + + int s = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); + if(s < 0){ + old_errno = errno; + perror_plus("socket"); + errno = old_errno; + return false; + } + strncpy(ifr->ifr_name, ifname, IF_NAMESIZE); + ifr->ifr_name[IF_NAMESIZE-1] = '\0'; /* NUL terminate */ + ret = ioctl(s, SIOCGIFFLAGS, ifr); + if(ret == -1){ + if(debug){ + old_errno = errno; + perror_plus("ioctl SIOCGIFFLAGS"); + errno = old_errno; + } + if((close(s) == -1) and debug){ + old_errno = errno; + perror_plus("close"); + errno = old_errno; + } + return false; + } + if((close(s) == -1) and debug){ + old_errno = errno; + perror_plus("close"); + errno = old_errno; + } + return true; +} + +__attribute__((nonnull, warn_unused_result)) +bool good_flags(const char *ifname, const struct ifreq *ifr){ + + /* Reject the loopback device */ + if(ifr->ifr_flags & IFF_LOOPBACK){ + if(debug){ + fprintf_plus(stderr, "Rejecting loopback interface \"%s\"\n", + ifname); + } + return false; + } + /* Accept point-to-point devices only if connect_to is specified */ + if(connect_to != NULL and (ifr->ifr_flags & IFF_POINTOPOINT)){ + if(debug){ + fprintf_plus(stderr, "Accepting point-to-point interface" + " \"%s\"\n", ifname); + } + return true; + } + /* Otherwise, reject non-broadcast-capable devices */ + if(not (ifr->ifr_flags & IFF_BROADCAST)){ + if(debug){ + fprintf_plus(stderr, "Rejecting non-broadcast interface" + " \"%s\"\n", ifname); + } + return false; + } + /* Reject non-ARP interfaces (including dummy interfaces) */ + if(ifr->ifr_flags & IFF_NOARP){ + if(debug){ + fprintf_plus(stderr, "Rejecting non-ARP interface \"%s\"\n", + ifname); + } + return false; + } + + /* Accept this device */ + if(debug){ + fprintf_plus(stderr, "Interface \"%s\" is good\n", ifname); + } + return true; +} + +/* + * This function determines if a directory entry in /sys/class/net + * corresponds to an acceptable network device. + * (This function is passed to scandir(3) as a filter function.) + */ +__attribute__((nonnull, warn_unused_result)) +int good_interface(const struct dirent *if_entry){ + if(if_entry->d_name[0] == '.'){ + return 0; + } + + struct ifreq ifr; + if(not get_flags(if_entry->d_name, &ifr)){ + if(debug){ + fprintf_plus(stderr, "Failed to get flags for interface " + "\"%s\"\n", if_entry->d_name); + } + return 0; + } + + if(not good_flags(if_entry->d_name, &ifr)){ + return 0; + } + return 1; +} + +/* + * This function determines if a network interface is up. + */ +__attribute__((nonnull, warn_unused_result)) +bool interface_is_up(const char *interface){ + struct ifreq ifr; + if(not get_flags(interface, &ifr)){ + if(debug){ + fprintf_plus(stderr, "Failed to get flags for interface " + "\"%s\"\n", interface); + } + return false; + } + + return (bool)(ifr.ifr_flags & IFF_UP); +} + +/* + * This function determines if a network interface is running + */ +__attribute__((nonnull, warn_unused_result)) +bool interface_is_running(const char *interface){ + struct ifreq ifr; + if(not get_flags(interface, &ifr)){ + if(debug){ + fprintf_plus(stderr, "Failed to get flags for interface " + "\"%s\"\n", interface); + } + return false; + } + + return (bool)(ifr.ifr_flags & IFF_RUNNING); +} + +__attribute__((nonnull, pure, warn_unused_result)) +int notdotentries(const struct dirent *direntry){ + /* Skip "." and ".." */ + if(direntry->d_name[0] == '.' + and (direntry->d_name[1] == '\0' + or (direntry->d_name[1] == '.' + and direntry->d_name[2] == '\0'))){ + return 0; + } + return 1; +} + +/* Is this directory entry a runnable program? */ +__attribute__((nonnull, warn_unused_result)) +int runnable_hook(const struct dirent *direntry){ + int ret; + size_t sret; + struct stat st; + + if((direntry->d_name)[0] == '\0'){ + /* Empty name? */ + return 0; + } + + sret = strspn(direntry->d_name, "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "_.-"); + if((direntry->d_name)[sret] != '\0'){ + /* Contains non-allowed characters */ + if(debug){ + fprintf_plus(stderr, "Ignoring hook \"%s\" with bad name\n", + direntry->d_name); + } + return 0; + } + + ret = fstatat(hookdir_fd, direntry->d_name, &st, 0); + if(ret == -1){ + if(debug){ + perror_plus("Could not stat hook"); + } + return 0; + } + if(not (S_ISREG(st.st_mode))){ + /* Not a regular file */ + if(debug){ + fprintf_plus(stderr, "Ignoring hook \"%s\" - not a file\n", + direntry->d_name); + } + return 0; + } + if(not (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))){ + /* Not executable */ + if(debug){ + fprintf_plus(stderr, "Ignoring hook \"%s\" - not executable\n", + direntry->d_name); + } + return 0; + } + if(debug){ + fprintf_plus(stderr, "Hook \"%s\" is acceptable\n", + direntry->d_name); + } + return 1; +} + +__attribute__((nonnull, warn_unused_result)) +int avahi_loop_with_timeout(AvahiSimplePoll *s, int retry_interval, + mandos_context *mc){ + int ret; + struct timespec now; + struct timespec waited_time; + intmax_t block_time; + + while(true){ + if(mc->current_server == NULL){ + if(debug){ + fprintf_plus(stderr, "Wait until first server is found." + " No timeout!\n"); + } + ret = avahi_simple_poll_iterate(s, -1); + } else { + if(debug){ + fprintf_plus(stderr, "Check current_server if we should run" + " it, or wait\n"); + } + /* the current time */ + ret = clock_gettime(CLOCK_MONOTONIC, &now); + if(ret == -1){ + perror_plus("clock_gettime"); + return -1; + } + /* Calculating in ms how long time between now and server + who we visted longest time ago. Now - last seen. */ + waited_time.tv_sec = (now.tv_sec + - mc->current_server->last_seen.tv_sec); + waited_time.tv_nsec = (now.tv_nsec + - mc->current_server->last_seen.tv_nsec); + /* total time is 10s/10,000ms. + Converting to s from ms by dividing by 1,000, + and ns to ms by dividing by 1,000,000. */ + block_time = ((retry_interval + - ((intmax_t)waited_time.tv_sec * 1000)) + - ((intmax_t)waited_time.tv_nsec / 1000000)); + + if(debug){ + fprintf_plus(stderr, "Blocking for %" PRIdMAX " ms\n", + block_time); + } + + if(block_time <= 0){ + ret = start_mandos_communication(mc->current_server->ip, + mc->current_server->port, + mc->current_server->if_index, + mc->current_server->af, mc); + if(ret == 0){ + avahi_simple_poll_quit(s); + return 0; + } + ret = clock_gettime(CLOCK_MONOTONIC, + &mc->current_server->last_seen); + if(ret == -1){ + perror_plus("clock_gettime"); + return -1; + } + mc->current_server = mc->current_server->next; + block_time = 0; /* Call avahi to find new Mandos + servers, but don't block */ + } + + ret = avahi_simple_poll_iterate(s, (int)block_time); + } + if(ret != 0){ + if(ret > 0 or errno != EINTR){ + return (ret != 1) ? ret : 0; + } + } + } +} + +__attribute__((nonnull)) +void run_network_hooks(const char *mode, const char *interface, + const float delay){ + struct dirent **direntries = NULL; + if(hookdir_fd == -1){ + hookdir_fd = open(hookdir, O_RDONLY | O_DIRECTORY | O_PATH + | O_CLOEXEC); + if(hookdir_fd == -1){ + if(errno == ENOENT){ + if(debug){ + fprintf_plus(stderr, "Network hook directory \"%s\" not" + " found\n", hookdir); + } + } else { + perror_plus("open"); + } + return; + } + } + int devnull = (int)TEMP_FAILURE_RETRY(open("/dev/null", O_RDONLY)); + if(devnull == -1){ + perror_plus("open(\"/dev/null\", O_RDONLY)"); + return; + } + int numhooks = scandirat(hookdir_fd, ".", &direntries, + runnable_hook, alphasort); + if(numhooks == -1){ + perror_plus("scandir"); + close(devnull); + return; + } + struct dirent *direntry; + int ret; + for(int i = 0; i < numhooks; i++){ + direntry = direntries[i]; + if(debug){ + fprintf_plus(stderr, "Running network hook \"%s\"\n", + direntry->d_name); + } + pid_t hook_pid = fork(); + if(hook_pid == 0){ + /* Child */ + /* Raise privileges */ + errno = raise_privileges_permanently(); + if(errno != 0){ + perror_plus("Failed to raise privileges"); + _exit(EX_NOPERM); + } + /* Set group */ + errno = 0; + ret = setgid(0); + if(ret == -1){ + perror_plus("setgid"); + _exit(EX_NOPERM); + } + /* Reset supplementary groups */ + errno = 0; + ret = setgroups(0, NULL); + if(ret == -1){ + perror_plus("setgroups"); + _exit(EX_NOPERM); + } + ret = setenv("MANDOSNETHOOKDIR", hookdir, 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + ret = setenv("DEVICE", interface, 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + ret = setenv("VERBOSITY", debug ? "1" : "0", 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + ret = setenv("MODE", mode, 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + char *delaystring; + ret = asprintf(&delaystring, "%f", (double)delay); + if(ret == -1){ + perror_plus("asprintf"); + _exit(EX_OSERR); + } + ret = setenv("DELAY", delaystring, 1); + if(ret == -1){ + free(delaystring); + perror_plus("setenv"); + _exit(EX_OSERR); + } + free(delaystring); + if(connect_to != NULL){ + ret = setenv("CONNECT", connect_to, 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + } + int hook_fd = (int)TEMP_FAILURE_RETRY(openat(hookdir_fd, + direntry->d_name, + O_RDONLY)); + if(hook_fd == -1){ + perror_plus("openat"); + _exit(EXIT_FAILURE); + } + if(close(hookdir_fd) == -1){ + perror_plus("close"); + _exit(EXIT_FAILURE); + } + ret = dup2(devnull, STDIN_FILENO); + if(ret == -1){ + perror_plus("dup2(devnull, STDIN_FILENO)"); + _exit(EX_OSERR); + } + ret = close(devnull); + if(ret == -1){ + perror_plus("close"); + _exit(EX_OSERR); + } + ret = dup2(STDERR_FILENO, STDOUT_FILENO); + if(ret == -1){ + perror_plus("dup2(STDERR_FILENO, STDOUT_FILENO)"); + _exit(EX_OSERR); + } + if(fexecve(hook_fd, (char *const []){ direntry->d_name, NULL }, + environ) == -1){ + perror_plus("fexecve"); + _exit(EXIT_FAILURE); + } + } else { + if(hook_pid == -1){ + perror_plus("fork"); + free(direntry); + continue; + } + int status; + if(TEMP_FAILURE_RETRY(waitpid(hook_pid, &status, 0)) == -1){ + perror_plus("waitpid"); + free(direntry); + continue; + } + if(WIFEXITED(status)){ + if(WEXITSTATUS(status) != 0){ + fprintf_plus(stderr, "Warning: network hook \"%s\" exited" + " with status %d\n", direntry->d_name, + WEXITSTATUS(status)); + free(direntry); + continue; + } + } else if(WIFSIGNALED(status)){ + fprintf_plus(stderr, "Warning: network hook \"%s\" died by" + " signal %d\n", direntry->d_name, + WTERMSIG(status)); + free(direntry); + continue; + } else { + fprintf_plus(stderr, "Warning: network hook \"%s\"" + " crashed\n", direntry->d_name); + free(direntry); + continue; + } + } + if(debug){ + fprintf_plus(stderr, "Network hook \"%s\" ran successfully\n", + direntry->d_name); + } + free(direntry); + } + free(direntries); + if(close(hookdir_fd) == -1){ + perror_plus("close"); + } else { + hookdir_fd = -1; + } + close(devnull); +} + +__attribute__((nonnull, warn_unused_result)) +int bring_up_interface(const char *const interface, + const float delay){ + int old_errno = errno; + int ret; + struct ifreq network; + unsigned int if_index = if_nametoindex(interface); + if(if_index == 0){ + fprintf_plus(stderr, "No such interface: \"%s\"\n", interface); + errno = old_errno; + return ENXIO; + } + + if(quit_now){ + errno = old_errno; + return EINTR; + } + + if(not interface_is_up(interface)){ + int ret_errno = 0; + int ioctl_errno = 0; + if(not get_flags(interface, &network)){ + ret_errno = errno; + fprintf_plus(stderr, "Failed to get flags for interface " + "\"%s\"\n", interface); + errno = old_errno; + return ret_errno; + } + network.ifr_flags |= IFF_UP; /* set flag */ + + int sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); + if(sd == -1){ + ret_errno = errno; + perror_plus("socket"); + errno = old_errno; + return ret_errno; + } + + if(quit_now){ + ret = close(sd); + if(ret == -1){ + perror_plus("close"); + } + errno = old_errno; + return EINTR; + } + + if(debug){ + fprintf_plus(stderr, "Bringing up interface \"%s\"\n", + interface); + } + + /* Raise privileges */ + ret_errno = raise_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to raise privileges"); + } + +#ifdef __linux__ + int ret_linux; + bool restore_loglevel = false; + if(ret_errno == 0){ + /* Lower kernel loglevel to KERN_NOTICE to avoid KERN_INFO + messages about the network interface to mess up the prompt */ + ret_linux = klogctl(8, NULL, 5); + if(ret_linux == -1){ + perror_plus("klogctl"); + } else { + restore_loglevel = true; + } + } +#endif /* __linux__ */ + int ret_setflags = ioctl(sd, SIOCSIFFLAGS, &network); + ioctl_errno = errno; +#ifdef __linux__ + if(restore_loglevel){ + ret_linux = klogctl(7, NULL, 0); + if(ret_linux == -1){ + perror_plus("klogctl"); + } + } +#endif /* __linux__ */ + + /* If raise_privileges() succeeded above */ + if(ret_errno == 0){ + /* Lower privileges */ + ret_errno = lower_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to lower privileges"); + } + } + + /* Close the socket */ + ret = close(sd); + if(ret == -1){ + perror_plus("close"); + } + + if(ret_setflags == -1){ + errno = ioctl_errno; + perror_plus("ioctl SIOCSIFFLAGS +IFF_UP"); + errno = old_errno; + return ioctl_errno; + } + } else if(debug){ + fprintf_plus(stderr, "Interface \"%s\" is already up; good\n", + interface); + } + + /* Sleep checking until interface is running. + Check every 0.25s, up to total time of delay */ + for(int i = 0; i < delay * 4; i++){ + if(interface_is_running(interface)){ + break; + } + struct timespec sleeptime = { .tv_nsec = 250000000 }; + ret = nanosleep(&sleeptime, NULL); + if(ret == -1 and errno != EINTR){ + perror_plus("nanosleep"); + } + } + + errno = old_errno; + return 0; +} + +__attribute__((nonnull, warn_unused_result)) +int take_down_interface(const char *const interface){ + int old_errno = errno; + struct ifreq network; + unsigned int if_index = if_nametoindex(interface); + if(if_index == 0){ + fprintf_plus(stderr, "No such interface: \"%s\"\n", interface); + errno = old_errno; + return ENXIO; + } + if(interface_is_up(interface)){ + int ret_errno = 0; + int ioctl_errno = 0; + if(not get_flags(interface, &network) and debug){ + ret_errno = errno; + fprintf_plus(stderr, "Failed to get flags for interface " + "\"%s\"\n", interface); + errno = old_errno; + return ret_errno; + } + network.ifr_flags &= ~(short)IFF_UP; /* clear flag */ + + int sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); + if(sd == -1){ + ret_errno = errno; + perror_plus("socket"); + errno = old_errno; + return ret_errno; + } + + if(debug){ + fprintf_plus(stderr, "Taking down interface \"%s\"\n", + interface); + } + + /* Raise privileges */ + ret_errno = raise_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to raise privileges"); + } + + int ret_setflags = ioctl(sd, SIOCSIFFLAGS, &network); + ioctl_errno = errno; + + /* If raise_privileges() succeeded above */ + if(ret_errno == 0){ + /* Lower privileges */ + ret_errno = lower_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to lower privileges"); + } + } + + /* Close the socket */ + int ret = close(sd); + if(ret == -1){ + perror_plus("close"); + } + + if(ret_setflags == -1){ + errno = ioctl_errno; + perror_plus("ioctl SIOCSIFFLAGS -IFF_UP"); + errno = old_errno; + return ioctl_errno; + } + } else if(debug){ + fprintf_plus(stderr, "Interface \"%s\" is already down; odd\n", + interface); + } + + errno = old_errno; + return 0; +} + +int main(int argc, char *argv[]){ + mandos_context mc = { .server = NULL, .dh_bits = 0, +#if GNUTLS_VERSION_NUMBER >= 0x030606 + .priority = "SECURE128:!CTYPE-X.509" + ":+CTYPE-RAWPK:!RSA:!VERS-ALL:+VERS-TLS1.3" + ":%PROFILE_ULTRA", +#elif GNUTLS_VERSION_NUMBER < 0x030600 + .priority = "SECURE256:!CTYPE-X.509" + ":+CTYPE-OPENPGP:!RSA:+SIGN-DSA-SHA256", +#else +#error "Needs GnuTLS 3.6.6 or later, or before 3.6.0" +#endif + .current_server = NULL, .interfaces = NULL, + .interfaces_size = 0 }; + AvahiSServiceBrowser *sb = NULL; + error_t ret_errno; + int ret; + intmax_t tmpmax; + char *tmp; + int exitcode = EXIT_SUCCESS; + char *interfaces_to_take_down = NULL; + size_t interfaces_to_take_down_size = 0; + char run_tempdir[] = "/run/tmp/mandosXXXXXX"; + char old_tempdir[] = "/tmp/mandosXXXXXX"; + char *tempdir = NULL; + AvahiIfIndex if_index = AVAHI_IF_UNSPEC; + const char *seckey = PATHDIR "/" SECKEY; + const char *pubkey = PATHDIR "/" PUBKEY; +#if GNUTLS_VERSION_NUMBER >= 0x030606 + const char *tls_privkey = PATHDIR "/" TLS_PRIVKEY; + const char *tls_pubkey = PATHDIR "/" TLS_PUBKEY; +#endif + const char *dh_params_file = NULL; + char *interfaces_hooks = NULL; + + bool gnutls_initialized = false; + bool gpgme_initialized = false; + float delay = 2.5f; + double retry_interval = 10; /* 10s between trying a server and + retrying the same server again */ + + struct sigaction old_sigterm_action = { .sa_handler = SIG_DFL }; + struct sigaction sigterm_action = { .sa_handler = handle_sigterm }; + + uid = getuid(); + gid = getgid(); + + /* Lower any group privileges we might have, just to be safe */ + errno = 0; + ret = setgid(gid); + if(ret == -1){ + perror_plus("setgid"); + } + + /* Lower user privileges (temporarily) */ + errno = 0; + ret = seteuid(uid); + if(ret == -1){ + perror_plus("seteuid"); + } + + if(quit_now){ + goto end; + } + + { + struct argp_option options[] = { + { .name = "debug", .key = 128, + .doc = "Debug mode", .group = 3 }, + { .name = "connect", .key = 'c', + .arg = "ADDRESS:PORT", + .doc = "Connect directly to a specific Mandos server", + .group = 1 }, + { .name = "interface", .key = 'i', + .arg = "NAME", + .doc = "Network interface that will be used to search for" + " Mandos servers", + .group = 1 }, + { .name = "seckey", .key = 's', + .arg = "FILE", + .doc = "OpenPGP secret key file base name", + .group = 1 }, + { .name = "pubkey", .key = 'p', + .arg = "FILE", + .doc = "OpenPGP public key file base name", + .group = 1 }, + { .name = "tls-privkey", .key = 't', + .arg = "FILE", +#if GNUTLS_VERSION_NUMBER >= 0x030606 + .doc = "TLS private key file base name", +#else + .doc = "Dummy; ignored (requires GnuTLS 3.6.6)", +#endif + .group = 1 }, + { .name = "tls-pubkey", .key = 'T', + .arg = "FILE", +#if GNUTLS_VERSION_NUMBER >= 0x030606 + .doc = "TLS public key file base name", +#else + .doc = "Dummy; ignored (requires GnuTLS 3.6.6)", +#endif + .group = 1 }, + { .name = "dh-bits", .key = 129, + .arg = "BITS", + .doc = "Bit length of the prime number used in the" + " Diffie-Hellman key exchange", + .group = 2 }, + { .name = "dh-params", .key = 134, + .arg = "FILE", + .doc = "PEM-encoded PKCS#3 file with pre-generated parameters" + " for the Diffie-Hellman key exchange", + .group = 2 }, + { .name = "priority", .key = 130, + .arg = "STRING", + .doc = "GnuTLS priority string for the TLS handshake", + .group = 1 }, + { .name = "delay", .key = 131, + .arg = "SECONDS", + .doc = "Maximum delay to wait for interface startup", + .group = 2 }, + { .name = "retry", .key = 132, + .arg = "SECONDS", + .doc = "Retry interval used when denied by the Mandos server", + .group = 2 }, + { .name = "network-hook-dir", .key = 133, + .arg = "DIR", + .doc = "Directory where network hooks are located", + .group = 2 }, + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + { .name = "help", .key = '?', + .doc = "Give this help list", .group = -1 }, + { .name = "usage", .key = -3, + .doc = "Give a short usage message", .group = -1 }, + { .name = "version", .key = 'V', + .doc = "Print program version", .group = -1 }, + { .name = NULL } + }; + + error_t parse_opt(int key, char *arg, + struct argp_state *state){ + errno = 0; + switch(key){ + case 128: /* --debug */ + debug = true; + break; + case 'c': /* --connect */ + connect_to = arg; + break; + case 'i': /* --interface */ + ret_errno = argz_add_sep(&mc.interfaces, &mc.interfaces_size, + arg, (int)','); + if(ret_errno != 0){ + argp_error(state, "%s", strerror(ret_errno)); + } + break; + case 's': /* --seckey */ + seckey = arg; + break; + case 'p': /* --pubkey */ + pubkey = arg; + break; + case 't': /* --tls-privkey */ +#if GNUTLS_VERSION_NUMBER >= 0x030606 + tls_privkey = arg; +#endif + break; + case 'T': /* --tls-pubkey */ +#if GNUTLS_VERSION_NUMBER >= 0x030606 + tls_pubkey = arg; +#endif + break; + case 129: /* --dh-bits */ + errno = 0; + tmpmax = strtoimax(arg, &tmp, 10); + if(errno != 0 or tmp == arg or *tmp != '\0' + or tmpmax != (typeof(mc.dh_bits))tmpmax){ + argp_error(state, "Bad number of DH bits"); + } + mc.dh_bits = (typeof(mc.dh_bits))tmpmax; + break; + case 134: /* --dh-params */ + dh_params_file = arg; + break; + case 130: /* --priority */ + mc.priority = arg; + break; + case 131: /* --delay */ + errno = 0; + delay = strtof(arg, &tmp); + if(errno != 0 or tmp == arg or *tmp != '\0'){ + argp_error(state, "Bad delay"); + } + case 132: /* --retry */ + errno = 0; + retry_interval = strtod(arg, &tmp); + if(errno != 0 or tmp == arg or *tmp != '\0' + or (retry_interval * 1000) > INT_MAX + or retry_interval < 0){ + argp_error(state, "Bad retry interval"); + } + break; + case 133: /* --network-hook-dir */ + hookdir = arg; + break; + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + case '?': /* --help */ + argp_state_help(state, state->out_stream, + (ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR) + & ~(unsigned int)ARGP_HELP_EXIT_OK); + __builtin_unreachable(); + case -3: /* --usage */ + argp_state_help(state, state->out_stream, + ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR); + __builtin_unreachable(); + case 'V': /* --version */ + fprintf_plus(state->out_stream, "%s\n", argp_program_version); + exit(argp_err_exit_status); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + struct argp argp = { .options = options, .parser = parse_opt, + .args_doc = "", + .doc = "Mandos client -- Get and decrypt" + " passwords from a Mandos server" }; + ret_errno = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_HELP, 0, NULL); + switch(ret_errno){ + case 0: + break; + case ENOMEM: default: - case AVAHI_BROWSER_FAILURE: - - fprintf(stderr, "(Browser) %s\n", - avahi_strerror(avahi_server_errno(server))); - avahi_simple_poll_quit(simple_poll); - return; - - case AVAHI_BROWSER_NEW: - /* We ignore the returned resolver object. In the callback - function we free it. If the server is terminated before - the callback function is called the server will free - the resolver for us. */ - - if (!(avahi_s_service_resolver_new(s, interface, protocol, name, - type, domain, - AVAHI_PROTO_INET6, 0, - resolve_callback, s))) - fprintf(stderr, "Failed to resolve service '%s': %s\n", name, - avahi_strerror(avahi_server_errno(s))); - break; - - case AVAHI_BROWSER_REMOVE: - break; - - case AVAHI_BROWSER_ALL_FOR_NOW: - case AVAHI_BROWSER_CACHE_EXHAUSTED: - break; - } -} - -int main(AVAHI_GCC_UNUSED int argc, AVAHI_GCC_UNUSED char*argv[]) { + errno = ret_errno; + perror_plus("argp_parse"); + exitcode = EX_OSERR; + goto end; + case EINVAL: + exitcode = EX_USAGE; + goto end; + } + } + + { + /* Work around Debian bug #633582: + */ + + /* Re-raise privileges */ + ret = raise_privileges(); + if(ret != 0){ + errno = ret; + perror_plus("Failed to raise privileges"); + } else { + struct stat st; + + if(strcmp(seckey, PATHDIR "/" SECKEY) == 0){ + int seckey_fd = open(seckey, O_RDONLY); + if(seckey_fd == -1){ + perror_plus("open"); + } else { + ret = (int)TEMP_FAILURE_RETRY(fstat(seckey_fd, &st)); + if(ret == -1){ + perror_plus("fstat"); + } else { + if(S_ISREG(st.st_mode) + and st.st_uid == 0 and st.st_gid == 0){ + ret = fchown(seckey_fd, uid, gid); + if(ret == -1){ + perror_plus("fchown"); + } + } + } + close(seckey_fd); + } + } + + if(strcmp(pubkey, PATHDIR "/" PUBKEY) == 0){ + int pubkey_fd = open(pubkey, O_RDONLY); + if(pubkey_fd == -1){ + perror_plus("open"); + } else { + ret = (int)TEMP_FAILURE_RETRY(fstat(pubkey_fd, &st)); + if(ret == -1){ + perror_plus("fstat"); + } else { + if(S_ISREG(st.st_mode) + and st.st_uid == 0 and st.st_gid == 0){ + ret = fchown(pubkey_fd, uid, gid); + if(ret == -1){ + perror_plus("fchown"); + } + } + } + close(pubkey_fd); + } + } + + if(dh_params_file != NULL + and strcmp(dh_params_file, PATHDIR "/dhparams.pem" ) == 0){ + int dhparams_fd = open(dh_params_file, O_RDONLY); + if(dhparams_fd == -1){ + perror_plus("open"); + } else { + ret = (int)TEMP_FAILURE_RETRY(fstat(dhparams_fd, &st)); + if(ret == -1){ + perror_plus("fstat"); + } else { + if(S_ISREG(st.st_mode) + and st.st_uid == 0 and st.st_gid == 0){ + ret = fchown(dhparams_fd, uid, gid); + if(ret == -1){ + perror_plus("fchown"); + } + } + } + close(dhparams_fd); + } + } + + /* Lower privileges */ + ret = lower_privileges(); + if(ret != 0){ + errno = ret; + perror_plus("Failed to lower privileges"); + } + } + } + + /* Remove invalid interface names (except "none") */ + { + char *interface = NULL; + while((interface = argz_next(mc.interfaces, mc.interfaces_size, + interface))){ + if(strcmp(interface, "none") != 0 + and if_nametoindex(interface) == 0){ + if(interface[0] != '\0'){ + fprintf_plus(stderr, "Not using nonexisting interface" + " \"%s\"\n", interface); + } + argz_delete(&mc.interfaces, &mc.interfaces_size, interface); + interface = NULL; + } + } + } + + /* Run network hooks */ + { + if(mc.interfaces != NULL){ + interfaces_hooks = malloc(mc.interfaces_size); + if(interfaces_hooks == NULL){ + perror_plus("malloc"); + goto end; + } + memcpy(interfaces_hooks, mc.interfaces, mc.interfaces_size); + argz_stringify(interfaces_hooks, mc.interfaces_size, (int)','); + } + run_network_hooks("start", interfaces_hooks != NULL ? + interfaces_hooks : "", delay); + } + + if(not debug){ + avahi_set_log_function(empty_log); + } + + /* Initialize Avahi early so avahi_simple_poll_quit() can be called + from the signal handler */ + /* Initialize the pseudo-RNG for Avahi */ + srand((unsigned int) time(NULL)); + simple_poll = avahi_simple_poll_new(); + if(simple_poll == NULL){ + fprintf_plus(stderr, + "Avahi: Failed to create simple poll object.\n"); + exitcode = EX_UNAVAILABLE; + goto end; + } + + sigemptyset(&sigterm_action.sa_mask); + ret = sigaddset(&sigterm_action.sa_mask, SIGINT); + if(ret == -1){ + perror_plus("sigaddset"); + exitcode = EX_OSERR; + goto end; + } + ret = sigaddset(&sigterm_action.sa_mask, SIGHUP); + if(ret == -1){ + perror_plus("sigaddset"); + exitcode = EX_OSERR; + goto end; + } + ret = sigaddset(&sigterm_action.sa_mask, SIGTERM); + if(ret == -1){ + perror_plus("sigaddset"); + exitcode = EX_OSERR; + goto end; + } + /* Need to check if the handler is SIG_IGN before handling: + | [[info:libc:Initial Signal Actions]] | + | [[info:libc:Basic Signal Handling]] | + */ + ret = sigaction(SIGINT, NULL, &old_sigterm_action); + if(ret == -1){ + perror_plus("sigaction"); + return EX_OSERR; + } + if(old_sigterm_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &sigterm_action, NULL); + if(ret == -1){ + perror_plus("sigaction"); + exitcode = EX_OSERR; + goto end; + } + } + ret = sigaction(SIGHUP, NULL, &old_sigterm_action); + if(ret == -1){ + perror_plus("sigaction"); + return EX_OSERR; + } + if(old_sigterm_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &sigterm_action, NULL); + if(ret == -1){ + perror_plus("sigaction"); + exitcode = EX_OSERR; + goto end; + } + } + ret = sigaction(SIGTERM, NULL, &old_sigterm_action); + if(ret == -1){ + perror_plus("sigaction"); + return EX_OSERR; + } + if(old_sigterm_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &sigterm_action, NULL); + if(ret == -1){ + perror_plus("sigaction"); + exitcode = EX_OSERR; + goto end; + } + } + + /* If no interfaces were specified, make a list */ + if(mc.interfaces == NULL){ + struct dirent **direntries = NULL; + /* Look for any good interfaces */ + ret = scandir(sys_class_net, &direntries, good_interface, + alphasort); + if(ret >= 1){ + /* Add all found interfaces to interfaces list */ + for(int i = 0; i < ret; ++i){ + ret_errno = argz_add(&mc.interfaces, &mc.interfaces_size, + direntries[i]->d_name); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("argz_add"); + free(direntries[i]); + continue; + } + if(debug){ + fprintf_plus(stderr, "Will use interface \"%s\"\n", + direntries[i]->d_name); + } + free(direntries[i]); + } + free(direntries); + } else { + if(ret == 0){ + free(direntries); + } + fprintf_plus(stderr, "Could not find a network interface\n"); + exitcode = EXIT_FAILURE; + goto end; + } + } + + /* Bring up interfaces which are down, and remove any "none"s */ + { + char *interface = NULL; + while((interface = argz_next(mc.interfaces, mc.interfaces_size, + interface))){ + /* If interface name is "none", stop bringing up interfaces. + Also remove all instances of "none" from the list */ + if(strcmp(interface, "none") == 0){ + argz_delete(&mc.interfaces, &mc.interfaces_size, + interface); + interface = NULL; + while((interface = argz_next(mc.interfaces, + mc.interfaces_size, interface))){ + if(strcmp(interface, "none") == 0){ + argz_delete(&mc.interfaces, &mc.interfaces_size, + interface); + interface = NULL; + } + } + break; + } + bool interface_was_up = interface_is_up(interface); + errno = bring_up_interface(interface, delay); + if(not interface_was_up){ + if(errno != 0){ + fprintf_plus(stderr, "Failed to bring up interface \"%s\":" + " %s\n", interface, strerror(errno)); + } else { + errno = argz_add(&interfaces_to_take_down, + &interfaces_to_take_down_size, + interface); + if(errno != 0){ + perror_plus("argz_add"); + } + } + } + } + if(debug and (interfaces_to_take_down == NULL)){ + fprintf_plus(stderr, "No interfaces were brought up\n"); + } + } + + /* If we only got one interface, explicitly use only that one */ + if(argz_count(mc.interfaces, mc.interfaces_size) == 1){ + if(debug){ + fprintf_plus(stderr, "Using only interface \"%s\"\n", + mc.interfaces); + } + if_index = (AvahiIfIndex)if_nametoindex(mc.interfaces); + } + + if(quit_now){ + goto end; + } + +#if GNUTLS_VERSION_NUMBER >= 0x030606 + ret = init_gnutls_global(tls_pubkey, tls_privkey, dh_params_file, &mc); +#elif GNUTLS_VERSION_NUMBER < 0x030600 + ret = init_gnutls_global(pubkey, seckey, dh_params_file, &mc); +#else +#error "Needs GnuTLS 3.6.6 or later, or before 3.6.0" +#endif + if(ret == -1){ + fprintf_plus(stderr, "init_gnutls_global failed\n"); + exitcode = EX_UNAVAILABLE; + goto end; + } else { + gnutls_initialized = true; + } + + if(quit_now){ + goto end; + } + + /* Try /run/tmp before /tmp */ + tempdir = mkdtemp(run_tempdir); + if(tempdir == NULL and errno == ENOENT){ + if(debug){ + fprintf_plus(stderr, "Tempdir %s did not work, trying %s\n", + run_tempdir, old_tempdir); + } + tempdir = mkdtemp(old_tempdir); + } + if(tempdir == NULL){ + perror_plus("mkdtemp"); + goto end; + } + + if(quit_now){ + goto end; + } + + if(not init_gpgme(pubkey, seckey, tempdir, &mc)){ + fprintf_plus(stderr, "init_gpgme failed\n"); + exitcode = EX_UNAVAILABLE; + goto end; + } else { + gpgme_initialized = true; + } + + if(quit_now){ + goto end; + } + + if(connect_to != NULL){ + /* Connect directly, do not use Zeroconf */ + /* (Mainly meant for debugging) */ + char *address = strrchr(connect_to, ':'); + + if(address == NULL){ + fprintf_plus(stderr, "No colon in address\n"); + exitcode = EX_USAGE; + goto end; + } + + if(quit_now){ + goto end; + } + + in_port_t port; + errno = 0; + tmpmax = strtoimax(address+1, &tmp, 10); + if(errno != 0 or tmp == address+1 or *tmp != '\0' + or tmpmax != (in_port_t)tmpmax){ + fprintf_plus(stderr, "Bad port number\n"); + exitcode = EX_USAGE; + goto end; + } + + if(quit_now){ + goto end; + } + + port = (in_port_t)tmpmax; + *address = '\0'; + /* Colon in address indicates IPv6 */ + int af; + if(strchr(connect_to, ':') != NULL){ + af = AF_INET6; + /* Accept [] around IPv6 address - see RFC 5952 */ + if(connect_to[0] == '[' and address[-1] == ']') + { + connect_to++; + address[-1] = '\0'; + } + } else { + af = AF_INET; + } + address = connect_to; + + if(quit_now){ + goto end; + } + + while(not quit_now){ + ret = start_mandos_communication(address, port, if_index, af, + &mc); + if(quit_now or ret == 0){ + break; + } + if(debug){ + fprintf_plus(stderr, "Retrying in %d seconds\n", + (int)retry_interval); + } + sleep((unsigned int)retry_interval); + } + + if(not quit_now){ + exitcode = EXIT_SUCCESS; + } + + goto end; + } + + if(quit_now){ + goto end; + } + + { AvahiServerConfig config; - AvahiSServiceBrowser *sb = NULL; - int error; - int ret; - int returncode = EXIT_SUCCESS; - const char *interface = "eth0"; - - while (true){ - static struct option long_options[] = { - {"debug", no_argument, (int *)&debug, 1}, - {"interface", required_argument, 0, 'i'}, - {0, 0, 0, 0} }; - - int option_index = 0; - ret = getopt_long (argc, argv, "i:", long_options, - &option_index); - - if (ret == -1){ - break; - } - - switch(ret){ - case 0: - break; - case 'i': - interface = optarg; - break; - default: - exit(EXIT_FAILURE); - } - } - - if (not debug){ - avahi_set_log_function(empty_log); - } - - /* Initialize the psuedo-RNG */ - srand((unsigned int) time(NULL)); - - /* Allocate main loop object */ - if (!(simple_poll = avahi_simple_poll_new())) { - fprintf(stderr, "Failed to create simple poll object.\n"); - - goto exit; - } - - /* Do not publish any local records */ + /* Do not publish any local Zeroconf records */ avahi_server_config_init(&config); config.publish_hinfo = 0; config.publish_addresses = 0; config.publish_workstation = 0; config.publish_domain = 0; - + /* Allocate a new server */ - server = avahi_server_new(avahi_simple_poll_get(simple_poll), - &config, NULL, NULL, &error); - - /* Free the configuration data */ + mc.server = avahi_server_new(avahi_simple_poll_get(simple_poll), + &config, NULL, NULL, &ret); + + /* Free the Avahi configuration data */ avahi_server_config_free(&config); - - /* Check if creating the server object succeeded */ - if (!server) { - fprintf(stderr, "Failed to create server: %s\n", - avahi_strerror(error)); - returncode = EXIT_FAILURE; - goto exit; - } - - /* Create the service browser */ - sb = avahi_s_service_browser_new(server, - (AvahiIfIndex) - if_nametoindex(interface), - AVAHI_PROTO_INET6, - "_mandos._tcp", NULL, 0, - browse_callback, server); - if (!sb) { - fprintf(stderr, "Failed to create service browser: %s\n", - avahi_strerror(avahi_server_errno(server))); - returncode = EXIT_FAILURE; - goto exit; - } - - /* Run the main loop */ - - if (debug){ - fprintf(stderr, "Starting avahi loop search\n"); - } - - avahi_simple_poll_loop(simple_poll); - - exit: - - if (debug){ - fprintf(stderr, "%s exiting\n", argv[0]); - } - - /* Cleanup things */ - if (sb) - avahi_s_service_browser_free(sb); - - if (server) - avahi_server_free(server); - - if (simple_poll) - avahi_simple_poll_free(simple_poll); - - return returncode; + } + + /* Check if creating the Avahi server object succeeded */ + if(mc.server == NULL){ + fprintf_plus(stderr, "Failed to create Avahi server: %s\n", + avahi_strerror(ret)); + exitcode = EX_UNAVAILABLE; + goto end; + } + + if(quit_now){ + goto end; + } + + /* Create the Avahi service browser */ + sb = avahi_s_service_browser_new(mc.server, if_index, + AVAHI_PROTO_UNSPEC, "_mandos._tcp", + NULL, 0, browse_callback, + (void *)&mc); + if(sb == NULL){ + fprintf_plus(stderr, "Failed to create service browser: %s\n", + avahi_strerror(avahi_server_errno(mc.server))); + exitcode = EX_UNAVAILABLE; + goto end; + } + + if(quit_now){ + goto end; + } + + /* Run the main loop */ + + if(debug){ + fprintf_plus(stderr, "Starting Avahi loop search\n"); + } + + ret = avahi_loop_with_timeout(simple_poll, + (int)(retry_interval * 1000), &mc); + if(debug){ + fprintf_plus(stderr, "avahi_loop_with_timeout exited %s\n", + (ret == 0) ? "successfully" : "with error"); + } + + end: + + if(debug){ + if(signal_received){ + fprintf_plus(stderr, "%s exiting due to signal %d: %s\n", + argv[0], signal_received, + strsignal(signal_received)); + } else { + fprintf_plus(stderr, "%s exiting\n", argv[0]); + } + } + + /* Cleanup things */ + free(mc.interfaces); + + if(sb != NULL) + avahi_s_service_browser_free(sb); + + if(mc.server != NULL) + avahi_server_free(mc.server); + + if(simple_poll != NULL) + avahi_simple_poll_free(simple_poll); + + if(gnutls_initialized){ + gnutls_certificate_free_credentials(mc.cred); + gnutls_dh_params_deinit(mc.dh_params); + } + + if(gpgme_initialized){ + gpgme_release(mc.ctx); + } + + /* Cleans up the circular linked list of Mandos servers the client + has seen */ + if(mc.current_server != NULL){ + mc.current_server->prev->next = NULL; + while(mc.current_server != NULL){ + server *next = mc.current_server->next; +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + free((char *)(mc.current_server->ip)); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + free(mc.current_server); + mc.current_server = next; + } + } + + /* Re-raise privileges */ + { + ret = raise_privileges(); + if(ret != 0){ + errno = ret; + perror_plus("Failed to raise privileges"); + } else { + + /* Run network hooks */ + run_network_hooks("stop", interfaces_hooks != NULL ? + interfaces_hooks : "", delay); + + /* Take down the network interfaces which were brought up */ + { + char *interface = NULL; + while((interface = argz_next(interfaces_to_take_down, + interfaces_to_take_down_size, + interface))){ + ret = take_down_interface(interface); + if(ret != 0){ + errno = ret; + perror_plus("Failed to take down interface"); + } + } + if(debug and (interfaces_to_take_down == NULL)){ + fprintf_plus(stderr, "No interfaces needed to be taken" + " down\n"); + } + } + } + + ret = lower_privileges_permanently(); + if(ret != 0){ + errno = ret; + perror_plus("Failed to lower privileges permanently"); + } + } + + free(interfaces_to_take_down); + free(interfaces_hooks); + + void clean_dir_at(int base, const char * const dirname, + uintmax_t level){ + struct dirent **direntries = NULL; + int dret; + int dir_fd = (int)TEMP_FAILURE_RETRY(openat(base, dirname, + O_RDONLY + | O_NOFOLLOW + | O_DIRECTORY + | O_PATH)); + if(dir_fd == -1){ + perror_plus("open"); + return; + } + int numentries = scandirat(dir_fd, ".", &direntries, + notdotentries, alphasort); + if(numentries >= 0){ + for(int i = 0; i < numentries; i++){ + if(debug){ + fprintf_plus(stderr, "Unlinking \"%s/%s\"\n", + dirname, direntries[i]->d_name); + } + dret = unlinkat(dir_fd, direntries[i]->d_name, 0); + if(dret == -1){ + if(errno == EISDIR){ + dret = unlinkat(dir_fd, direntries[i]->d_name, + AT_REMOVEDIR); + } + if((dret == -1) and (errno == ENOTEMPTY) + and (strcmp(direntries[i]->d_name, "private-keys-v1.d") + == 0) and (level == 0)){ + /* Recurse only in this special case */ + clean_dir_at(dir_fd, direntries[i]->d_name, level+1); + dret = 0; + } + if((dret == -1) and (errno != ENOENT)){ + fprintf_plus(stderr, "unlink(\"%s/%s\"): %s\n", dirname, + direntries[i]->d_name, strerror(errno)); + } + } + free(direntries[i]); + } + + /* need to clean even if 0 because man page doesn't specify */ + free(direntries); + dret = unlinkat(base, dirname, AT_REMOVEDIR); + if(dret == -1 and errno != ENOENT){ + perror_plus("rmdir"); + } + } else { + perror_plus("scandirat"); + } + close(dir_fd); + } + + /* Removes the GPGME temp directory and all files inside */ + if(tempdir != NULL){ + clean_dir_at(-1, tempdir, 0); + } + + if(quit_now){ + sigemptyset(&old_sigterm_action.sa_mask); + old_sigterm_action.sa_handler = SIG_DFL; + ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, + &old_sigterm_action, + NULL)); + if(ret == -1){ + perror_plus("sigaction"); + } + do { + ret = raise(signal_received); + } while(ret != 0 and errno == EINTR); + if(ret != 0){ + perror_plus("raise"); + abort(); + } + TEMP_FAILURE_RETRY(pause()); + } + + return exitcode; } === added file 'plugins.d/mandos-client.xml' --- plugins.d/mandos-client.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/mandos-client.xml 2019-07-27 10:11:45 +0000 @@ -0,0 +1,1016 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + + Client for Mandos + + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + + + + + DESCRIPTION + + &COMMANDNAME; is a client program that + communicates with mandos8 + to get a password. In slightly more detail, this client program + brings up network interfaces, uses the interfaces’ IPv6 + link-local addresses to get network connectivity, uses Zeroconf + to find servers on the local network, and communicates with + servers using TLS with a raw public key to ensure authenticity + and confidentiality. This client program keeps running, trying + all servers on the network, until it receives a satisfactory + reply or a TERM signal. After all servers have been tried, all + servers are periodically retried. If no servers are found it + will wait indefinitely for new servers to appear. + + + The network interfaces are selected like this: If any interfaces + are specified using the option, + those interface are used. Otherwise, + &COMMANDNAME; will use all interfaces that + are not loopback interfaces, are not point-to-point interfaces, + are capable of broadcasting and do not have the NOARP flag (see + netdevice + 7). (If the + option is used, point-to-point + interfaces and non-broadcast interfaces are accepted.) If any + used interfaces are not up and running, they are first taken up + (and later taken down again on program exit). + + + Before network interfaces are selected, all network + hooks are run; see . + + + This program is not meant to be run directly; it is really meant + to run as a plugin of the Mandos + plugin-runner + 8mandos, which runs in the + initial RAM disk environment because it is + specified as a keyscript in the + crypttab5 + file. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OPTIONS + + This program is commonly not invoked from the command line; it + is normally started by the Mandos + plugin runner, see plugin-runner8mandos + . Any command line options this program accepts + are therefore normally provided by the plugin runner, and not + directly. + + + + + + + + + Do not use Zeroconf to locate servers. Connect directly + to only one specified Mandos + server. Note that an IPv6 address has colon characters in + it, so the last colon character is + assumed to separate the address from the port number. + + + Normally, Zeroconf would be used to locate Mandos servers, + in which case this option would only be used when testing + and debugging. + + + + + + + + + + Comma separated list of network interfaces that will be + brought up and scanned for Mandos servers to connect to. + The default is the empty string, which will automatically + use all appropriate interfaces. + + + If the option is used, and + exactly one interface name is specified (except + none), this specifies + the interface to use to connect to the address given. + + + Note that since this program will normally run in the + initial RAM disk environment, the interface must be an + interface which exists at that stage. Thus, the interface + can normally not be a pseudo-interface such as + br0 or tun0; such interfaces + will not exist until much later in the boot process, and + can not be used by this program, unless created by a + network hook — see . + + + NAME can be the string + none; this will make + &COMMANDNAME; only bring up interfaces + specified before this string. This + is not recommended, and only meant for advanced users. + + + + + + + + + + OpenPGP public key file name. The default name is + /conf/conf.d/mandos/pubkey.txt. + + + + + + + + + + OpenPGP secret key file name. The default name is + /conf/conf.d/mandos/seckey.txt. + + + + + + + + + + TLS raw public key file name. The default name is + /conf/conf.d/mandos/tls-pubkey.pem. + + + + + + + + + + TLS secret key file name. The default name is + /conf/conf.d/mandos/tls-privkey.pem. + + + + + + + + + + + + + + + + Sets the number of bits to use for the prime number in the + TLS Diffie-Hellman key exchange. The default value is + selected automatically based on the GnuTLS security + profile set in its priority string. Note that if the + option is used, the values + from that file will be used instead. + + + + + + + + + Specifies a PEM-encoded PKCS#3 file to read the parameters + needed by the TLS Diffie-Hellman key exchange from. If + this option is not given, or if the file for some reason + could not be used, the parameters will be generated on + startup, which will take some time and processing power. + Those using servers running under time, power or processor + constraints may want to generate such a file in advance + and use this option. + + + + + + + + + After bringing a network interface up, the program waits + for the interface to arrive in a running + state before proceeding. During this time, the kernel log + level will be lowered to reduce clutter on the system + console, alleviating any other plugins which might be + using the system console. This option sets the upper + limit of seconds to wait. The default is 2.5 seconds. + + + + + + + + + All Mandos servers are tried repeatedly until a password + is received. This value specifies, in seconds, how long + between each successive try for the same + server. The default is 10 seconds. + + + + + + + + + Network hook directory. The default directory is + /lib/mandos/network-hooks.d. + + + + + + + + + Enable debug mode. This will enable a lot of output to + standard error about what the program is doing. The + program will still perform all other functions normally. + + + It will also enable debug mode in the Avahi and GnuTLS + libraries, making them print large amounts of debugging + output. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + + + + + + OVERVIEW + + + This program is the client part. It is a plugin started by + plugin-runner + 8mandos which will run in + an initial RAM disk environment. + + + This program could, theoretically, be used as a keyscript in + /etc/crypttab, but it would then be + impossible to enter a password for the encrypted root disk at + the console, since this program does not read from the console + at all. This is why a separate plugin runner ( + plugin-runner + 8mandos) is used to run + both this program and others in in parallel, + one of which ( + password-prompt + 8mandos) will prompt for + passwords on the system console. + + + + + EXIT STATUS + + This program will exit with a successful (zero) exit status if a + server could be found and the password received from it could be + successfully decrypted and output on standard output. The + program will exit with a non-zero exit status only if a critical + error occurs. Otherwise, it will forever connect to any + discovered Mandos servers, trying to + get a decryptable password and print it. + + + + + ENVIRONMENT + + + MANDOSPLUGINHELPERDIR + + + This environment variable will be assumed to contain the + directory containing any helper executables. The use and + nature of these helper executables, if any, is purposely + not documented. + + + + + + This program does not use any other environment variables, not + even the ones provided by cryptsetup8 + . + + + + + NETWORK HOOKS + + If a network interface like a bridge or tunnel is required to + find a Mandos server, this requires the interface to be up and + running before &COMMANDNAME; starts looking + for Mandos servers. This can be accomplished by creating a + network hook program, and placing it in a special + directory. + + + Before the network is used (and again before program exit), any + runnable programs found in the network hook directory are run + with the argument start or + stop. This should bring up or + down, respectively, any network interface which + &COMMANDNAME; should use. + + + REQUIREMENTS + + A network hook must be an executable file, and its name must + consist entirely of upper and lower case letters, digits, + underscores, periods, and hyphens. + + + A network hook will receive one argument, which can be one of + the following: + + + + start + + + This should make the network hook create (if necessary) + and bring up a network interface. + + + + + stop + + + This should make the network hook take down a network + interface, and delete it if it did not exist previously. + + + + + files + + + This should make the network hook print, one + file per line, all the files needed for it to + run. (These files will be copied into the initial RAM + filesystem.) Typical use is for a network hook which is + a shell script to print its needed binaries. + + + It is not necessary to print any non-executable files + already in the network hook directory, these will be + copied implicitly if they otherwise satisfy the name + requirements. + + + + + modules + + + This should make the network hook print, on + separate lines, all the kernel modules needed + for it to run. (These modules will be copied into the + initial RAM filesystem.) For instance, a tunnel + interface needs the + tun module. + + + + + + The network hook will be provided with a number of environment + variables: + + + + MANDOSNETHOOKDIR + + + The network hook directory, specified to + &COMMANDNAME; by the + option. Note: this + should always be used by the + network hook to refer to itself or any files in the hook + directory it may require. + + + + + DEVICE + + + The network interfaces, as specified to + &COMMANDNAME; by the + option, combined to one + string and separated by commas. If this is set, and + does not contain the interface a hook will bring up, + there is no reason for a hook to continue. + + + + + MODE + + + This will be the same as the first argument; + i.e. start, + stop, + files, or + modules. + + + + + VERBOSITY + + + This will be the 1 if + the option is passed to + &COMMANDNAME;, otherwise + 0. + + + + + DELAY + + + This will be the same as the + option passed to &COMMANDNAME;. Is + only set if MODE is + start or + stop. + + + + + CONNECT + + + This will be the same as the + option passed to &COMMANDNAME;. Is + only set if is passed and + MODE is + start or + stop. + + + + + + A hook may not read from standard input, and should be + restrictive in printing to standard output or standard error + unless VERBOSITY is + 1. + + + + + + FILES + + + /conf/conf.d/mandos/pubkey.txt + /conf/conf.d/mandos/seckey.txt + + + OpenPGP public and private key files, in ASCII + Armor format. These are the default file names, + they can be changed with the and + options. + + + + + /conf/conf.d/mandos/tls-pubkey.pem + /conf/conf.d/mandos/tls-privkey.pem + + + Public and private raw key files, in PEM + format. These are the default file names, they can be + changed with the and + options. + + + + + /lib/mandos/network-hooks.d + + + Directory where network hooks are located. Change this + with the option. See + . + + + + + + + + BUGS + + + + + EXAMPLE + + Note that normally, command line options will not be given + directly, but via options for the Mandos plugin-runner + 8mandos. + + + + Normal invocation needs no options, if the network interfaces + can be automatically determined: + + + &COMMANDNAME; + + + + + Search for Mandos servers (and connect to them) using one + specific interface: + + + + &COMMANDNAME; --interface eth1 + + + + + Run in debug mode, and use custom keys: + + + + +&COMMANDNAME; --debug --pubkey keydir/pubkey.txt --seckey keydir/seckey.txt --tls-pubkey keydir/tls-pubkey.pem --tls-privkey keydir/tls-privkey.pem + + + + + + Run in debug mode, with custom keys, and do not use Zeroconf + to locate a server; connect directly to the IPv6 link-local + address fe80::aede:48ff:fe71:f6f2, port 4711, + using interface eth2: + + + + +&COMMANDNAME; --debug --pubkey keydir/pubkey.txt --seckey keydir/seckey.txt --tls-pubkey keydir/tls-pubkey.pem --tls-privkey keydir/tls-privkey.pem --connect fe80::aede:48ff:fe71:f6f2:4711 --interface eth2 + + + + + + + SECURITY + + This program assumes that it is set-uid to root, and will switch + back to the original (and presumably non-privileged) user and + group after bringing up the network interface. + + + To use this program for its intended purpose (see ), the password for the root file system will + have to be given out to be stored in a server computer, after + having been encrypted using an OpenPGP key. This encrypted data + which will be stored in a server can only be decrypted by the + OpenPGP key, and the data will only be given out to those + clients who can prove they actually have that key. This key, + however, is stored unencrypted on the client side in its initial + RAM disk image file system. This is normally + readable by all, but this is normally fixed during installation + of this program; file permissions are set so that no-one is able + to read that file. + + + The only remaining weak point is that someone with physical + access to the client hard drive might turn off the client + computer, read the OpenPGP and TLS keys directly from the hard + drive, and communicate with the server. To safeguard against + this, the server is supposed to notice the client disappearing + and stop giving out the encrypted data. Therefore, it is + important to set the timeout and checker interval values tightly + on the server. See mandos8. + + + It will also help if the checker program on the server is + configured to request something from the client which can not be + spoofed by someone else on the network, like SSH server key + fingerprints, and unlike unencrypted ICMP + echo (ping) replies. + + + Note: This makes it completely insecure to + have Mandos clients which dual-boot + to another operating system which is not + trusted to keep the initial RAM disk image + confidential. + + + + + SEE ALSO + + intro + 8mandos, + cryptsetup + 8, + crypttab + 5, + mandos + 8, + password-prompt + 8mandos, + plugin-runner + 8mandos + + + + + Zeroconf + + + + Zeroconf is the network protocol standard used for finding + Mandos servers on the local network. + + + + + + Avahi + + + + Avahi is the library this program calls to find Zeroconf + services. + + + + + + GnuTLS + + + + GnuTLS is the library this client uses to implement TLS for + communicating securely with the server, and at the same time + send the public key to the server. + + + + + + GPGME + + + + GPGME is the library used to decrypt the OpenPGP data sent + by the server. + + + + + + RFC 4291: IP Version 6 Addressing + Architecture + + + + + Section 2.2: Text Representation of + Addresses + + + + Section 2.5.5.2: IPv4-Mapped IPv6 + Address + + + + Section 2.5.6, Link-Local IPv6 Unicast + Addresses + + + This client uses IPv6 link-local addresses, which are + immediately usable since a link-local addresses is + automatically assigned to a network interface when it + is brought up. + + + + + + + + + RFC 5246: The Transport Layer Security (TLS) + Protocol Version 1.2 + + + + TLS 1.2 is the protocol implemented by GnuTLS. + + + + + + RFC 4880: OpenPGP Message Format + + + + The data received from the server is binary encrypted + OpenPGP data. + + + + + + RFC 7250: Using Raw Public Keys in Transport + Layer Security (TLS) and Datagram Transport Layer Security + (DTLS) + + + + This is implemented by GnuTLS in version 3.6.6 and is, if + present, used by this program so that raw public keys can be + used. + + + + + + RFC 6091: Using OpenPGP Keys for Transport Layer + Security + + + + This is implemented by GnuTLS before version 3.6.0 and is, + if present, used by this program so that OpenPGP keys can be + used. + + + + + +
+ + + + + + === renamed file 'plugins.d/passprompt.c' => 'plugins.d/password-prompt.c' --- plugins.d/passprompt.c 2008-07-21 15:34:44 +0000 +++ plugins.d/password-prompt.c 2019-07-27 10:11:45 +0000 @@ -1,176 +1,581 @@ -/* -*- coding: utf-8 -*- */ +/* -*- coding: utf-8; mode: c; mode: orgtbl -*- */ /* - * Passprompt - Read a password from the terminal and print it - * - * Copyright © 2007-2008 Teddy Hogeborn and Björn Påhlsson. - * - * 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 + * Password-prompt - Read a password from the terminal and print it + * + * Copyright © 2008-2019 Teddy Hogeborn + * Copyright © 2008-2019 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos 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. + * + * Mandos 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 - * . + * along with Mandos. If not, see . * - * Contact the authors at and - * . + * Contact the authors at . */ -#define _GNU_SOURCE /* getline() */ -#define _FORTIFY_SOURCE 2 +#define _GNU_SOURCE /* getline(), asprintf() */ -#include /* struct termios, tcsetattr(), +#include /* struct termios, tcsetattr(), TCSAFLUSH, tcgetattr(), ECHO */ -#include /* struct termios, tcsetattr(), - STDIN_FILENO, TCSAFLUSH, - tcgetattr(), ECHO */ +#include /* access(), struct termios, + tcsetattr(), STDIN_FILENO, + TCSAFLUSH, tcgetattr(), ECHO, + readlink() */ #include /* sig_atomic_t, raise(), struct sigaction, sigemptyset(), sigaction(), sigaddset(), SIGINT, - SIGQUIT, SIGHUP, SIGTERM */ -#include /* NULL, size_t */ -#include /* ssize_t */ + SIGQUIT, SIGHUP, SIGTERM, + raise() */ +#include /* NULL, size_t, ssize_t */ +#include /* ssize_t, struct dirent, pid_t, + ssize_t, open() */ #include /* EXIT_SUCCESS, EXIT_FAILURE, - getopt_long */ + getenv(), free() */ +#include /* scandir(), alphasort() */ #include /* fprintf(), stderr, getline(), - stdin, feof(), perror(), fputc(), - stdout, getopt_long */ -#include /* errno, EINVAL */ + stdin, feof(), fputc(), vfprintf(), + vasprintf() */ +#include /* errno, EBADF, ENOTTY, EINVAL, + EFAULT, EFBIG, EIO, ENOSPC, EINTR + */ +#include /* error() */ #include /* or, not */ #include /* bool, false, true */ -#include /* strlen, rindex, strncmp, strcmp */ -#include /* getopt_long */ +#include /* strtoumax() */ +#include /* struct stat, lstat(), open() */ +#include /* strlen, rindex, memcmp, strerror() + */ +#include /* struct argp_option, struct + argp_state, struct argp, + argp_parse(), error_t, + ARGP_KEY_ARG, ARGP_KEY_END, + ARGP_ERR_UNKNOWN */ +#include /* EX_SOFTWARE, EX_OSERR, + EX_UNAVAILABLE, EX_IOERR, EX_OK */ +#include /* open() */ +#include /* va_list, va_start(), ... */ -volatile bool quit_now = false; +volatile sig_atomic_t quit_now = 0; +int signal_received; bool debug = false; - -void termination_handler(__attribute__((unused))int signum){ - quit_now = true; -} +const char *argp_program_version = "password-prompt " VERSION; +const char *argp_program_bug_address = ""; + +/* Needed for conflict resolution */ +const char plymouth_name[] = "plymouthd"; + +/* Function to use when printing errors */ +__attribute__((format (gnu_printf, 3, 4))) +void error_plus(int status, int errnum, const char *formatstring, + ...){ + va_list ap; + char *text; + int ret; + + va_start(ap, formatstring); + ret = vasprintf(&text, formatstring, ap); + if(ret == -1){ + fprintf(stderr, "Mandos plugin %s: ", + program_invocation_short_name); + vfprintf(stderr, formatstring, ap); + fprintf(stderr, ": %s\n", strerror(errnum)); + error(status, errno, "vasprintf while printing error"); + return; + } + fprintf(stderr, "Mandos plugin "); + error(status, errnum, "%s", text); + free(text); +} + +static void termination_handler(int signum){ + if(quit_now){ + return; + } + quit_now = 1; + signal_received = signum; +} + +bool conflict_detection(void){ + + /* plymouth conflicts with password-prompt since both want to read + from the terminal. Password-prompt will exit if it detects + plymouth since plymouth performs the same functionality. + */ + if(access("/run/plymouth/pid", R_OK) == 0){ + return true; + } + + __attribute__((nonnull)) + int is_plymouth(const struct dirent *proc_entry){ + int ret; + int cl_fd; + { + uintmax_t proc_id; + char *tmp; + errno = 0; + proc_id = strtoumax(proc_entry->d_name, &tmp, 10); + + if(errno != 0 or *tmp != '\0' + or proc_id != (uintmax_t)((pid_t)proc_id)){ + return 0; + } + } + + char *cmdline_filename; + ret = asprintf(&cmdline_filename, "/proc/%s/cmdline", + proc_entry->d_name); + if(ret == -1){ + error_plus(0, errno, "asprintf"); + return 0; + } + + /* Open /proc//cmdline */ + cl_fd = open(cmdline_filename, O_RDONLY); + free(cmdline_filename); + if(cl_fd == -1){ + if(errno != ENOENT){ + error_plus(0, errno, "open"); + } + return 0; + } + + char *cmdline = NULL; + { + size_t cmdline_len = 0; + size_t cmdline_allocated = 0; + char *tmp; + const size_t blocksize = 1024; + ssize_t sret; + do { + /* Allocate more space? */ + if(cmdline_len + blocksize + 1 > cmdline_allocated){ + tmp = realloc(cmdline, cmdline_allocated + blocksize + 1); + if(tmp == NULL){ + error_plus(0, errno, "realloc"); + free(cmdline); + close(cl_fd); + return 0; + } + cmdline = tmp; + cmdline_allocated += blocksize; + } + + /* Read data */ + sret = read(cl_fd, cmdline + cmdline_len, + cmdline_allocated - cmdline_len); + if(sret == -1){ + error_plus(0, errno, "read"); + free(cmdline); + close(cl_fd); + return 0; + } + cmdline_len += (size_t)sret; + } while(sret != 0); + ret = close(cl_fd); + if(ret == -1){ + error_plus(0, errno, "close"); + free(cmdline); + return 0; + } + cmdline[cmdline_len] = '\0'; /* Make sure it is terminated */ + } + /* we now have cmdline */ + + /* get basename */ + char *cmdline_base = strrchr(cmdline, '/'); + if(cmdline_base != NULL){ + cmdline_base += 1; /* skip the slash */ + } else { + cmdline_base = cmdline; + } + + if(strcmp(cmdline_base, plymouth_name) != 0){ + if(debug){ + fprintf(stderr, "\"%s\" is not \"%s\"\n", cmdline_base, + plymouth_name); + } + free(cmdline); + return 0; + } + if(debug){ + fprintf(stderr, "\"%s\" equals \"%s\"\n", cmdline_base, + plymouth_name); + } + free(cmdline); + return 1; + } + + struct dirent **direntries = NULL; + int ret; + ret = scandir("/proc", &direntries, is_plymouth, alphasort); + if(ret == -1){ + error_plus(1, errno, "scandir"); + } + { + int i = ret; + while(i--){ + free(direntries[i]); + } + } + free(direntries); + return ret > 0; +} + int main(int argc, char **argv){ - ssize_t ret; + ssize_t sret; + int ret; size_t n; struct termios t_new, t_old; char *buffer = NULL; char *prefix = NULL; + char *prompt = NULL; int status = EXIT_SUCCESS; struct sigaction old_action, new_action = { .sa_handler = termination_handler, .sa_flags = 0 }; - - while (true){ - static struct option long_options[] = { - {"debug", no_argument, (int *)&debug, 1}, - {"prefix", required_argument, 0, 'p'}, - {0, 0, 0, 0} }; - - int option_index = 0; - ret = getopt_long (argc, argv, "p:", long_options, &option_index); - - if (ret == -1){ - break; + { + struct argp_option options[] = { + { .name = "prefix", .key = 'p', + .arg = "PREFIX", .flags = 0, + .doc = "Prefix shown before the prompt", .group = 2 }, + { .name = "prompt", .key = 129, + .arg = "PROMPT", .flags = 0, + .doc = "The prompt to show", .group = 2 }, + { .name = "debug", .key = 128, + .doc = "Debug mode", .group = 3 }, + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + { .name = "help", .key = '?', + .doc = "Give this help list", .group = -1 }, + { .name = "usage", .key = -3, + .doc = "Give a short usage message", .group = -1 }, + { .name = "version", .key = 'V', + .doc = "Print program version", .group = -1 }, + { .name = NULL } + }; + + __attribute__((nonnull(3))) + error_t parse_opt (int key, char *arg, struct argp_state *state){ + errno = 0; + switch (key){ + case 'p': /* --prefix */ + prefix = arg; + break; + case 128: /* --debug */ + debug = true; + break; + case 129: /* --prompt */ + prompt = arg; + break; + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + case '?': /* --help */ + argp_state_help(state, state->out_stream, + (ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR) + & ~(unsigned int)ARGP_HELP_EXIT_OK); + __builtin_unreachable(); + case -3: /* --usage */ + argp_state_help(state, state->out_stream, + ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR); + __builtin_unreachable(); + case 'V': /* --version */ + fprintf(state->out_stream, "%s\n", argp_program_version); + exit(argp_err_exit_status); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return errno; } - + + struct argp argp = { .options = options, .parser = parse_opt, + .args_doc = "", + .doc = "Mandos password-prompt -- Read and" + " output a password" }; + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_HELP, NULL, NULL); switch(ret){ case 0: break; - case 'p': - prefix = optarg; - break; + case ENOMEM: default: - fprintf(stderr, "bad arguments\n"); - exit(EXIT_FAILURE); + errno = ret; + error_plus(0, errno, "argp_parse"); + return EX_OSERR; + case EINVAL: + return EX_USAGE; } } - - if (debug){ + + if(debug){ fprintf(stderr, "Starting %s\n", argv[0]); } - if (debug){ + + if(conflict_detection()){ + if(debug){ + fprintf(stderr, "Stopping %s because of conflict\n", argv[0]); + } + return EXIT_FAILURE; + } + + if(debug){ fprintf(stderr, "Storing current terminal attributes\n"); } - if (tcgetattr(STDIN_FILENO, &t_old) != 0){ - return EXIT_FAILURE; + if(tcgetattr(STDIN_FILENO, &t_old) != 0){ + int e = errno; + error_plus(0, errno, "tcgetattr"); + switch(e){ + case EBADF: + case ENOTTY: + return EX_UNAVAILABLE; + default: + return EX_OSERR; + } } sigemptyset(&new_action.sa_mask); - sigaddset(&new_action.sa_mask, SIGINT); - sigaddset(&new_action.sa_mask, SIGQUIT); - sigaddset(&new_action.sa_mask, SIGHUP); - sigaddset(&new_action.sa_mask, SIGTERM); - sigaction(SIGINT, NULL, &old_action); - if (old_action.sa_handler != SIG_IGN) - sigaction(SIGINT, &new_action, NULL); - sigaction(SIGQUIT, NULL, &old_action); - if (old_action.sa_handler != SIG_IGN) - sigaction(SIGQUIT, &new_action, NULL); - sigaction(SIGHUP, NULL, &old_action); - if (old_action.sa_handler != SIG_IGN) - sigaction(SIGHUP, &new_action, NULL); - sigaction(SIGTERM, NULL, &old_action); - if (old_action.sa_handler != SIG_IGN) - sigaction(SIGTERM, &new_action, NULL); - - - if (debug){ + ret = sigaddset(&new_action.sa_mask, SIGINT); + if(ret == -1){ + error_plus(0, errno, "sigaddset"); + return EX_OSERR; + } + ret = sigaddset(&new_action.sa_mask, SIGHUP); + if(ret == -1){ + error_plus(0, errno, "sigaddset"); + return EX_OSERR; + } + ret = sigaddset(&new_action.sa_mask, SIGTERM); + if(ret == -1){ + error_plus(0, errno, "sigaddset"); + return EX_OSERR; + } + /* Need to check if the handler is SIG_IGN before handling: + | [[info:libc:Initial Signal Actions]] | + | [[info:libc:Basic Signal Handling]] | + */ + ret = sigaction(SIGINT, NULL, &old_action); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + return EX_OSERR; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &new_action, NULL); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + return EX_OSERR; + } + } + ret = sigaction(SIGHUP, NULL, &old_action); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + return EX_OSERR; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &new_action, NULL); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + return EX_OSERR; + } + } + ret = sigaction(SIGTERM, NULL, &old_action); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + return EX_OSERR; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &new_action, NULL); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + return EX_OSERR; + } + } + + + if(debug){ fprintf(stderr, "Removing echo flag from terminal attributes\n"); } t_new = t_old; - t_new.c_lflag &= ~ECHO; - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_new) != 0){ - perror("tcsetattr-echo"); - return EXIT_FAILURE; + t_new.c_lflag &= ~(tcflag_t)ECHO; + if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_new) != 0){ + int e = errno; + error_plus(0, errno, "tcsetattr-echo"); + switch(e){ + case EBADF: + case ENOTTY: + return EX_UNAVAILABLE; + case EINVAL: + default: + return EX_OSERR; + } } - - if (debug){ + + if(debug){ fprintf(stderr, "Waiting for input from stdin \n"); } while(true){ - if (quit_now){ + if(quit_now){ + if(debug){ + fprintf(stderr, "Interrupted by signal, exiting.\n"); + } status = EXIT_FAILURE; break; } if(prefix){ - fprintf(stderr, "%s Password: ", prefix); + fprintf(stderr, "%s ", prefix); + } + if(prompt != NULL){ + fprintf(stderr, "%s: ", prompt); } else { - fprintf(stderr, "Password: "); - } - ret = getline(&buffer, &n, stdin); - if (ret > 0){ - fprintf(stdout, "%s", buffer); + const char *cryptsource = getenv("CRYPTTAB_SOURCE"); + const char *crypttarget = getenv("CRYPTTAB_NAME"); + /* Before cryptsetup 1.1.0~rc2 */ + if(cryptsource == NULL){ + cryptsource = getenv("cryptsource"); + } + if(crypttarget == NULL){ + crypttarget = getenv("crypttarget"); + } + const char *const prompt1 = "Unlocking the disk"; + const char *const prompt2 = "Enter passphrase"; + if(cryptsource == NULL){ + if(crypttarget == NULL){ + fprintf(stderr, "%s to unlock the disk: ", prompt2); + } else { + fprintf(stderr, "%s (%s)\n%s: ", prompt1, crypttarget, + prompt2); + } + } else { + if(crypttarget == NULL){ + fprintf(stderr, "%s %s\n%s: ", prompt1, cryptsource, + prompt2); + } else { + fprintf(stderr, "%s %s (%s)\n%s: ", prompt1, cryptsource, + crypttarget, prompt2); + } + } + } + sret = getline(&buffer, &n, stdin); + if(sret > 0){ status = EXIT_SUCCESS; + /* Make n = data size instead of allocated buffer size */ + n = (size_t)sret; + /* Strip final newline */ + if(n > 0 and buffer[n-1] == '\n'){ + buffer[n-1] = '\0'; /* not strictly necessary */ + n--; + } + size_t written = 0; + while(written < n){ + sret = write(STDOUT_FILENO, buffer + written, n - written); + if(sret < 0){ + int e = errno; + error_plus(0, errno, "write"); + switch(e){ + case EBADF: + case EFAULT: + case EINVAL: + case EFBIG: + case EIO: + case ENOSPC: + default: + status = EX_IOERR; + break; + case EINTR: + status = EXIT_FAILURE; + break; + } + break; + } + written += (size_t)sret; + } + sret = close(STDOUT_FILENO); + if(sret == -1){ + int e = errno; + error_plus(0, errno, "close"); + switch(e){ + case EBADF: + status = EX_OSFILE; + break; + case EIO: + default: + status = EX_IOERR; + break; + } + } break; } - // ret == 0 makes no other sence than to retry to read from stdin - if (ret < 0){ - if (errno != EINTR and not feof(stdin)){ - perror("getline"); - status = EXIT_FAILURE; - break; + if(sret < 0){ + int e = errno; + if(errno != EINTR){ + if(not feof(stdin)){ + error_plus(0, errno, "getline"); + switch(e){ + case EBADF: + status = EX_UNAVAILABLE; + break; + case EIO: + case EINVAL: + default: + status = EX_IOERR; + break; + } + break; + } else { + clearerr(stdin); + } } } + /* if(sret == 0), then the only sensible thing to do is to retry + to read from stdin */ fputc('\n', stderr); + if(debug and not quit_now){ + /* If quit_now is nonzero, we were interrupted by a signal, and + will print that later, so no need to show this too. */ + fprintf(stderr, "getline() returned 0, retrying.\n"); + } } - - if (debug){ + + free(buffer); + + if(debug){ fprintf(stderr, "Restoring terminal attributes\n"); } - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_old) != 0){ - perror("tcsetattr+echo"); - } - - if (debug){ - fprintf(stderr, "%s is exiting\n", argv[0]); + if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_old) != 0){ + error_plus(0, errno, "tcsetattr+echo"); + } + + if(quit_now){ + sigemptyset(&old_action.sa_mask); + old_action.sa_handler = SIG_DFL; + ret = sigaction(signal_received, &old_action, NULL); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + } + raise(signal_received); + } + + if(debug){ + fprintf(stderr, "%s is exiting with status %d\n", argv[0], + status); + } + if(status == EXIT_SUCCESS or status == EX_OK){ + fputc('\n', stderr); } return status; === added file 'plugins.d/password-prompt.xml' --- plugins.d/password-prompt.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/password-prompt.xml 2019-07-27 10:11:45 +0000 @@ -0,0 +1,334 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Prompt for a password and output it. + + + + + &COMMANDNAME; + + + PREFIX + + + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + + + + + DESCRIPTION + + All &COMMANDNAME; does is prompt for a + password and output any given password to standard output. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + This program is little more than a getpass3 + wrapper, although actual use of that function is not guaranteed + or implied. + + + This program tries to detect if a Plymouth daemon + (plymouthd8) + is running, by looking for a + /run/plymouth/pid file or a process named + plymouthd. If it is detected, + this process will immediately exit without doing anything. + + + + + OPTIONS + + This program is commonly not invoked from the command line; it + is normally started by the Mandos + plugin runner, see plugin-runner8mandos + . Any command line options this program accepts + are therefore normally provided by the plugin runner, and not + directly. + + + + + + + + + Prefix string shown before the password prompt. + + + + + + + + + The password prompt. Using this option will make this + program ignore the CRYPTTAB_SOURCE and + CRYPTTAB_NAME environment variables. + + + + + + + + + Enable debug mode. This will enable a lot of output to + standard error about what the program is doing. The + program will still perform all other functions normally. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program has encountered an error, and any output so far could be + corrupt and/or truncated, and should therefore be ignored. + + + + + ENVIRONMENT + + + CRYPTTAB_SOURCE + CRYPTTAB_NAME + + + If set, and if the option is not + used, these environment variables will be assumed to + contain the source device name and the target device + mapper name, respectively, and will be shown as part of + the prompt. + + + These variables will normally be inherited from + plugin-runner + 8mandos, which might + have in turn inherited them from its calling process. + + + This behavior is meant to exactly mirror the behavior of + askpass, the default password prompter + from initramfs-tools. + + + + + + + + BUGS + + + + + EXAMPLE + + Note that normally, command line options will not be given + directly, but via options for the Mandos plugin-runner + 8mandos. + + + + Normal invocation needs no options: + + + &COMMANDNAME; + + + + + Show a prefix before the prompt; in this case, a host name. + It might be useful to be reminded of which host needs a + password, in case of KVM switches, etc. + + + + +&COMMANDNAME; --prefix=host.example.org: + + + + + + Run in debug mode. + + + + &COMMANDNAME; --debug + + + + + + SECURITY + + On its own, this program is very simple, and does not exactly + present any security risks. The one thing that could be + considered worthy of note is this: This program is meant to be + run by plugin-runner8mandos + , and will, when run standalone, outside, in a + normal environment, immediately output on its standard output + any presumably secret password it just received. Therefore, + when running this program standalone (which should never + normally be done), take care not to type in any real secret + password by force of habit, since it would then immediately be + shown as output. + + + To further alleviate any risk of being locked out of a system, + the plugin-runner + 8mandos has a fallback + mode which does the same thing as this program, only with less + features. + + + + + SEE ALSO + + intro + 8mandos, + mandos-client + 8mandos, + plugin-runner + 8mandos, + plymouthd + 8 + + +
+ + + + + === added file 'plugins.d/plymouth.c' --- plugins.d/plymouth.c 1970-01-01 00:00:00 +0000 +++ plugins.d/plymouth.c 2019-07-27 10:11:45 +0000 @@ -0,0 +1,613 @@ +/* -*- coding: utf-8 -*- */ +/* + * Plymouth - Read a password from Plymouth and output it + * + * Copyright © 2010-2019 Teddy Hogeborn + * Copyright © 2010-2019 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos 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. + * + * Mandos 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 Mandos. If not, see . + * + * Contact the authors at . + */ + +#define _GNU_SOURCE /* asprintf(), TEMP_FAILURE_RETRY() */ +#include /* sig_atomic_t, struct sigaction, + sigemptyset(), sigaddset(), SIGINT, + SIGHUP, SIGTERM, sigaction(), + kill(), SIG_IGN */ +#include /* bool, false, true */ +#include /* open(), O_RDONLY */ +#include /* and, or, not*/ +#include /* size_t, ssize_t, pid_t, struct + dirent, waitpid() */ +#include /* waitpid() */ +#include /* NULL */ +#include /* strchr(), memcmp() */ +#include /* asprintf(), perror(), fopen(), + fscanf(), vasprintf(), fprintf(), + vfprintf() */ +#include /* close(), readlink(), read(), + fork(), setsid(), chdir(), dup2(), + STDERR_FILENO, execv(), access() */ +#include /* free(), EXIT_FAILURE, realloc(), + EXIT_SUCCESS, malloc(), _exit(), + getenv() */ +#include /* scandir(), alphasort() */ +#include /* intmax_t, strtoumax(), SCNuMAX */ +#include /* struct stat, lstat() */ +#include /* EX_OSERR, EX_UNAVAILABLE */ +#include /* error() */ +#include /* TEMP_FAILURE_RETRY */ +#include /* argz_count(), argz_extract() */ +#include /* va_list, va_start(), ... */ +#include + +sig_atomic_t interrupted_by_signal = 0; +const char *argp_program_version = "plymouth " VERSION; +const char *argp_program_bug_address = ""; + +/* Used by Ubuntu 11.04 (Natty Narwahl) */ +const char plymouth_old_old_pid[] = "/dev/.initramfs/plymouth.pid"; +/* Used by Ubuntu 11.10 (Oneiric Ocelot) */ +const char plymouth_old_pid[] = "/run/initramfs/plymouth.pid"; +/* Used by Debian 9 (stretch) */ +const char plymouth_pid[] = "/run/plymouth/pid"; + +const char plymouth_path[] = "/bin/plymouth"; +const char plymouthd_path[] = "/sbin/plymouthd"; +const char *plymouthd_default_argv[] = {"/sbin/plymouthd", + "--mode=boot", + "--attach-to-session", + NULL }; +bool debug = false; + +static void termination_handler(__attribute__((unused))int signum){ + if(interrupted_by_signal){ + return; + } + interrupted_by_signal = 1; +} + +__attribute__((format (gnu_printf, 2, 3), nonnull)) +int fprintf_plus(FILE *stream, const char *format, ...){ + va_list ap; + va_start (ap, format); + fprintf(stream, "Mandos plugin %s: ", program_invocation_short_name); + return vfprintf(stream, format, ap); +} + +/* Function to use when printing errors */ +__attribute__((format (gnu_printf, 3, 4))) +void error_plus(int status, int errnum, const char *formatstring, + ...){ + va_list ap; + char *text; + int ret; + + va_start(ap, formatstring); + ret = vasprintf(&text, formatstring, ap); + if(ret == -1){ + fprintf(stderr, "Mandos plugin %s: ", + program_invocation_short_name); + vfprintf(stderr, formatstring, ap); + fprintf(stderr, ": "); + fprintf(stderr, "%s\n", strerror(errnum)); + error(status, errno, "vasprintf while printing error"); + return; + } + fprintf(stderr, "Mandos plugin "); + error(status, errnum, "%s", text); + free(text); +} + +/* Create prompt string */ +char *makeprompt(void){ + int ret = 0; + char *prompt; + const char *const cryptsource = getenv("cryptsource"); + const char *const crypttarget = getenv("crypttarget"); + const char prompt_start[] = "Unlocking the disk"; + const char prompt_end[] = "Enter passphrase"; + + if(cryptsource == NULL){ + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s\n%s", prompt_start, prompt_end); + } else { + ret = asprintf(&prompt, "%s (%s)\n%s", prompt_start, + crypttarget, prompt_end); + } + } else { + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s %s\n%s", prompt_start, cryptsource, + prompt_end); + } else { + ret = asprintf(&prompt, "%s %s (%s)\n%s", prompt_start, + cryptsource, crypttarget, prompt_end); + } + } + if(ret == -1){ + return NULL; + } + return prompt; +} + +void kill_and_wait(pid_t pid){ + TEMP_FAILURE_RETRY(kill(pid, SIGTERM)); + TEMP_FAILURE_RETRY(waitpid(pid, NULL, 0)); +} + +bool become_a_daemon(void){ + int ret = setuid(geteuid()); + if(ret == -1){ + error_plus(0, errno, "setuid"); + } + + setsid(); + ret = chdir("/"); + if(ret == -1){ + error_plus(0, errno, "chdir"); + return false; + } + ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */ + if(ret == -1){ + error_plus(0, errno, "dup2"); + return false; + } + return true; +} + +__attribute__((nonnull (2, 3))) +bool exec_and_wait(pid_t *pid_return, const char *path, + const char * const * const argv, bool interruptable, + bool daemonize){ + int status; + int ret; + pid_t pid; + if(debug){ + for(const char * const *arg = argv; *arg != NULL; arg++){ + fprintf_plus(stderr, "exec_and_wait arg: %s\n", *arg); + } + fprintf_plus(stderr, "exec_and_wait end of args\n"); + } + + pid = fork(); + if(pid == -1){ + error_plus(0, errno, "fork"); + return false; + } + if(pid == 0){ + /* Child */ + if(daemonize){ + if(not become_a_daemon()){ + _exit(EX_OSERR); + } + } + + char **new_argv = malloc(sizeof(const char *)); + if(new_argv == NULL){ + error_plus(0, errno, "malloc"); + _exit(EX_OSERR); + } + char **tmp; + int i = 0; + for (; argv[i] != NULL; i++){ + tmp = realloc(new_argv, sizeof(const char *) * ((size_t)i + 2)); + if(tmp == NULL){ + error_plus(0, errno, "realloc"); + free(new_argv); + _exit(EX_OSERR); + } + new_argv = tmp; + new_argv[i] = strdup(argv[i]); + } + new_argv[i] = NULL; + + execv(path, (char *const *)new_argv); + error_plus(0, errno, "execv"); + _exit(EXIT_FAILURE); + } + if(pid_return != NULL){ + *pid_return = pid; + } + do { + ret = waitpid(pid, &status, 0); + } while(ret == -1 and errno == EINTR + and ((not interrupted_by_signal) + or (not interruptable))); + if(interrupted_by_signal and interruptable){ + if(debug){ + fprintf_plus(stderr, "Interrupted by signal\n"); + } + return false; + } + if(ret == -1){ + error_plus(0, errno, "waitpid"); + return false; + } + if(debug){ + if(WIFEXITED(status)){ + fprintf_plus(stderr, "exec_and_wait exited: %d\n", + WEXITSTATUS(status)); + } else if(WIFSIGNALED(status)) { + fprintf_plus(stderr, "exec_and_wait signaled: %d\n", + WTERMSIG(status)); + } + } + if(WIFEXITED(status) and (WEXITSTATUS(status) == 0)){ + return true; + } + return false; +} + +__attribute__((nonnull)) +int is_plymouth(const struct dirent *proc_entry){ + int ret; + { + uintmax_t proc_id; + char *tmp; + errno = 0; + proc_id = strtoumax(proc_entry->d_name, &tmp, 10); + + if(errno != 0 or *tmp != '\0' + or proc_id != (uintmax_t)((pid_t)proc_id)){ + return 0; + } + } + char exe_target[sizeof(plymouthd_path)]; + char *exe_link; + ret = asprintf(&exe_link, "/proc/%s/exe", proc_entry->d_name); + if(ret == -1){ + error_plus(0, errno, "asprintf"); + return 0; + } + + struct stat exe_stat; + ret = lstat(exe_link, &exe_stat); + if(ret == -1){ + free(exe_link); + if(errno != ENOENT){ + error_plus(0, errno, "lstat"); + } + return 0; + } + + if(not S_ISLNK(exe_stat.st_mode) + or exe_stat.st_uid != 0 + or exe_stat.st_gid != 0){ + free(exe_link); + return 0; + } + + ssize_t sret = readlink(exe_link, exe_target, sizeof(exe_target)); + free(exe_link); + if((sret != (ssize_t)sizeof(plymouthd_path)-1) or + (memcmp(plymouthd_path, exe_target, + sizeof(plymouthd_path)-1) != 0)){ + return 0; + } + return 1; +} + +pid_t get_pid(void){ + int ret; + uintmax_t proc_id = 0; + FILE *pidfile = fopen(plymouth_pid, "r"); + /* Try the new pid file location */ + if(pidfile != NULL){ + ret = fscanf(pidfile, "%" SCNuMAX, &proc_id); + if(ret != 1){ + proc_id = 0; + } + fclose(pidfile); + } + /* Try the old pid file location */ + if(proc_id == 0){ + pidfile = fopen(plymouth_old_pid, "r"); + if(pidfile != NULL){ + ret = fscanf(pidfile, "%" SCNuMAX, &proc_id); + if(ret != 1){ + proc_id = 0; + } + fclose(pidfile); + } + } + /* Try the old old pid file location */ + if(proc_id == 0){ + pidfile = fopen(plymouth_old_old_pid, "r"); + if(pidfile != NULL){ + ret = fscanf(pidfile, "%" SCNuMAX, &proc_id); + if(ret != 1){ + proc_id = 0; + } + fclose(pidfile); + } + } + /* Look for a plymouth process */ + if(proc_id == 0){ + struct dirent **direntries = NULL; + ret = scandir("/proc", &direntries, is_plymouth, alphasort); + if(ret == -1){ + error_plus(0, errno, "scandir"); + } + if(ret > 0){ + for(int i = ret-1; i >= 0; i--){ + if(proc_id == 0){ + ret = sscanf(direntries[i]->d_name, "%" SCNuMAX, &proc_id); + if(ret < 0){ + error_plus(0, errno, "sscanf"); + } + } + free(direntries[i]); + } + } + /* scandir might preallocate for this variable (man page unclear). + even if ret == 0, therefore we need to free it. */ + free(direntries); + } + pid_t pid; + pid = (pid_t)proc_id; + if((uintmax_t)pid == proc_id){ + return pid; + } + + return 0; +} + +char **getargv(pid_t pid){ + int cl_fd; + char *cmdline_filename; + ssize_t sret; + int ret; + + ret = asprintf(&cmdline_filename, "/proc/%" PRIuMAX "/cmdline", + (uintmax_t)pid); + if(ret == -1){ + error_plus(0, errno, "asprintf"); + return NULL; + } + + /* Open /proc//cmdline */ + cl_fd = open(cmdline_filename, O_RDONLY); + free(cmdline_filename); + if(cl_fd == -1){ + error_plus(0, errno, "open"); + return NULL; + } + + size_t cmdline_allocated = 0; + size_t cmdline_len = 0; + char *cmdline = NULL; + char *tmp; + const size_t blocksize = 1024; + do { + /* Allocate more space? */ + if(cmdline_len + blocksize > cmdline_allocated){ + tmp = realloc(cmdline, cmdline_allocated + blocksize); + if(tmp == NULL){ + error_plus(0, errno, "realloc"); + free(cmdline); + close(cl_fd); + return NULL; + } + cmdline = tmp; + cmdline_allocated += blocksize; + } + + /* Read data */ + sret = read(cl_fd, cmdline + cmdline_len, + cmdline_allocated - cmdline_len); + if(sret == -1){ + error_plus(0, errno, "read"); + free(cmdline); + close(cl_fd); + return NULL; + } + cmdline_len += (size_t)sret; + } while(sret != 0); + ret = close(cl_fd); + if(ret == -1){ + error_plus(0, errno, "close"); + free(cmdline); + return NULL; + } + + /* we got cmdline and cmdline_len, ignore rest... */ + char **argv = malloc((argz_count(cmdline, cmdline_len) + 1) + * sizeof(char *)); /* Get number of args */ + if(argv == NULL){ + error_plus(0, errno, "argv = malloc()"); + free(cmdline); + return NULL; + } + argz_extract(cmdline, cmdline_len, argv); /* Create argv */ + return argv; +} + +int main(__attribute__((unused))int argc, + __attribute__((unused))char **argv){ + char *prompt = NULL; + char *prompt_arg; + pid_t plymouth_command_pid; + int ret; + bool bret; + + { + struct argp_option options[] = { + { .name = "prompt", .key = 128, .arg = "PROMPT", + .doc = "The prompt to show" }, + { .name = "debug", .key = 129, + .doc = "Debug mode" }, + { .name = NULL } + }; + + __attribute__((nonnull(3))) + error_t parse_opt (int key, char *arg, __attribute__((unused)) + struct argp_state *state){ + errno = 0; + switch (key){ + case 128: /* --prompt */ + prompt = arg; + if(debug){ + fprintf_plus(stderr, "Custom prompt \"%s\"\n", prompt); + } + break; + case 129: /* --debug */ + debug = true; + break; + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + struct argp argp = { .options = options, .parser = parse_opt, + .args_doc = "", + .doc = "Mandos plymouth -- Read and" + " output a password" }; + ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error_plus(0, errno, "argp_parse"); + return EX_OSERR; + case EINVAL: + error_plus(0, errno, "argp_parse"); + return EX_USAGE; + } + } + + /* test -x /bin/plymouth */ + ret = access(plymouth_path, X_OK); + if(ret == -1){ + /* Plymouth is probably not installed. Don't print an error + message, just exit. */ + if(debug){ + fprintf_plus(stderr, "Plymouth (%s) not found\n", + plymouth_path); + } + exit(EX_UNAVAILABLE); + } + + { /* Add signal handlers */ + struct sigaction old_action, + new_action = { .sa_handler = termination_handler, + .sa_flags = 0 }; + sigemptyset(&new_action.sa_mask); + for(int *sig = (int[]){ SIGINT, SIGHUP, SIGTERM, 0 }; + *sig != 0; sig++){ + ret = sigaddset(&new_action.sa_mask, *sig); + if(ret == -1){ + error_plus(EX_OSERR, errno, "sigaddset"); + } + ret = sigaction(*sig, NULL, &old_action); + if(ret == -1){ + error_plus(EX_OSERR, errno, "sigaction"); + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(*sig, &new_action, NULL); + if(ret == -1){ + error_plus(EX_OSERR, errno, "sigaction"); + } + } + } + } + + /* plymouth --ping */ + bret = exec_and_wait(&plymouth_command_pid, plymouth_path, + (const char *[]) + { plymouth_path, "--ping", NULL }, + true, false); + if(not bret){ + if(interrupted_by_signal){ + kill_and_wait(plymouth_command_pid); + exit(EXIT_FAILURE); + } + /* Plymouth is probably not running. Don't print an error + message, just exit. */ + if(debug){ + fprintf_plus(stderr, "Plymouth not running\n"); + } + exit(EX_UNAVAILABLE); + } + + if(prompt != NULL){ + ret = asprintf(&prompt_arg, "--prompt=%s", prompt); + } else { + char *made_prompt = makeprompt(); + ret = asprintf(&prompt_arg, "--prompt=%s", made_prompt); + free(made_prompt); + } + if(ret == -1){ + error_plus(EX_OSERR, errno, "asprintf"); + } + + /* plymouth ask-for-password --prompt="$prompt" */ + if(debug){ + fprintf_plus(stderr, "Prompting for password via Plymouth\n"); + } + bret = exec_and_wait(&plymouth_command_pid, + plymouth_path, (const char *[]) + { plymouth_path, "ask-for-password", + prompt_arg, NULL }, + true, false); + free(prompt_arg); + if(bret){ + exit(EXIT_SUCCESS); + } + if(not interrupted_by_signal){ + /* exec_and_wait failed for some other reason */ + exit(EXIT_FAILURE); + } + kill_and_wait(plymouth_command_pid); + + char **plymouthd_argv = NULL; + pid_t pid = get_pid(); + if(pid == 0){ + error_plus(0, 0, "plymouthd pid not found"); + } else { + plymouthd_argv = getargv(pid); + } + + bret = exec_and_wait(NULL, plymouth_path, (const char *[]) + { plymouth_path, "quit", NULL }, + false, false); + if(not bret){ + if(plymouthd_argv != NULL){ + free(*plymouthd_argv); + free(plymouthd_argv); + } + exit(EXIT_FAILURE); + } + bret = exec_and_wait(NULL, plymouthd_path, + (plymouthd_argv != NULL) + ? (const char * const *)plymouthd_argv + : plymouthd_default_argv, + false, true); + if(plymouthd_argv != NULL){ + free(*plymouthd_argv); + free(plymouthd_argv); + } + if(not bret){ + exit(EXIT_FAILURE); + } + exec_and_wait(NULL, plymouth_path, (const char *[]) + { plymouth_path, "show-splash", NULL }, + false, false); + exit(EXIT_FAILURE); +} === added file 'plugins.d/plymouth.xml' --- plugins.d/plymouth.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/plymouth.xml 2019-07-27 10:11:45 +0000 @@ -0,0 +1,370 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Mandos plugin to use plymouth to get a + password. + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + &COMMANDNAME; + + + + + + + + + DESCRIPTION + + This program prompts for a password using + plymouth8 + and outputs any given password to standard + output. If no plymouth8 + process can be found, this program will immediately exit with an + exit code indicating failure. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + If this program is killed (presumably by + plugin-runner + 8mandos because some other + plugin provided the password), it cannot tell + plymouth8 + to abort requesting a password, because + plymouth + 8 does not support this. + Therefore, this program will then kill the + running plymouth + 8 process and start a + new one using the same command line + arguments as the old one was using. + + + + + OPTIONS + + This program is commonly not invoked from the command line; it + is normally started by the Mandos + plugin runner, see plugin-runner8mandos + . Any command line options this program accepts + are therefore normally provided by the plugin runner, and not + directly. + + + + + + + + The password prompt. Note that using this option will + make this program ignore the cryptsource + and crypttarget environment variables. + + + + + + + + + Enable debug mode. This will enable a lot of output to + standard error about what the program is doing. The + program will still perform all other functions normally. + + + + + + + + + + Gives a help message about options and their meanings. + + + + + + + + + Gives a short usage message. + + + + + + + + + + Prints the program version. + + + + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program was interrupted or encountered an error, and any output + so far could be corrupt and/or truncated, and should therefore + be ignored. + + + + + ENVIRONMENT + + + cryptsource + crypttarget + + + If set, and if the option is not + used, these environment variables will be assumed to + contain the source device name and the target device + mapper name, respectively, and will be shown as part of + the prompt. + + + These variables will normally be inherited from + plugin-runner + 8mandos, which might + have in turn inherited them from its calling process. + + + This behavior is meant to exactly mirror the behavior of + askpass, the default password prompter + from initramfs-tools. + + + + + + + + FILES + + + /bin/plymouth + + + This is the command run to retrieve a password from + plymouth + 8. + + + + + /proc + + + To find the running plymouth8 + , this directory will be searched for + numeric entries which will be assumed to be directories. + In all those directories, the exe and + cmdline entries will be used to + determine the name of the running binary, effective user + and group ID, and the command line + arguments. See proc5 + . + + + + + /sbin/plymouthd + + + This is the name of the binary which will be searched for + in the process list. See plymouth8 + . + + + + + + + + BUGS + + Killing the plymouth8 + daemon and starting a new one is ugly, but necessary as long as + it does not support aborting a password request. + + + + + + EXAMPLE + + Note that normally, this program will not be invoked directly, + but instead started by the Mandos plugin-runner8mandos + . + + + + Normal invocation needs no options: + + + &COMMANDNAME; + + + + + Show a different prompt. + + + &COMMANDNAME; --prompt=Password + + + + + + SECURITY + + If this program is killed by a signal, it will kill the process + ID which at the start of this program was + determined to run plymouth8 + as root (see also ). There is a very + slight risk that, in the time between those events, that process + ID was freed and then taken up by another + process; the wrong process would then be killed. Now, this + program can only be killed by the user who started it; see + plugin-runner + 8mandos. This program + should therefore be started by a completely separate + non-privileged user, and no other programs should be allowed to + run as that special user. This means that it is not recommended + to use the user "nobody" to start this program, as other + possibly less trusted programs could be running as "nobody", and + they would then be able to kill this program, triggering the + killing of the process ID which may or may not + be plymouth + 8. + + + The only other thing that could be considered worthy of note is + this: This program is meant to be run by + plugin-runner8mandos, and will, when run + standalone, outside, in a normal environment, immediately output + on its standard output any presumably secret password it just + received. Therefore, when running this program standalone + (which should never normally be done), take care not to type in + any real secret password by force of habit, since it would then + immediately be shown as output. + + + + + SEE ALSO + + intro + 8mandos, + plugin-runner + 8mandos, + proc + 5, + plymouth + 8 + + +
+ + + + + === added file 'plugins.d/splashy.c' --- plugins.d/splashy.c 1970-01-01 00:00:00 +0000 +++ plugins.d/splashy.c 2018-02-08 10:23:55 +0000 @@ -0,0 +1,472 @@ +/* -*- coding: utf-8 -*- */ +/* + * Splashy - Read a password from splashy and output it + * + * Copyright © 2008-2018 Teddy Hogeborn + * Copyright © 2008-2018 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos 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. + * + * Mandos 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 Mandos. If not, see . + * + * Contact the authors at . + */ + +#define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), asprintf() */ +#include /* sig_atomic_t, struct sigaction, + sigemptyset(), sigaddset(), SIGINT, + SIGHUP, SIGTERM, sigaction, + SIG_IGN, kill(), SIGKILL */ +#include /* NULL */ +#include /* getenv() */ +#include /* asprintf(), vasprintf(), vprintf(), + fprintf() */ +#include /* EXIT_FAILURE, free(), + EXIT_SUCCESS */ +#include /* pid_t, DIR, struct dirent, + ssize_t */ +#include /* opendir(), readdir(), closedir() */ +#include /* intmax_t, strtoimax() */ +#include /* struct stat, lstat(), S_ISLNK */ +#include /* not, or, and */ +#include /* readlink(), fork(), execl(), + sleep(), dup2() STDERR_FILENO, + STDOUT_FILENO, _exit(), + pause() */ +#include /* memcmp(), strerror() */ +#include /* errno, EACCES, ENOTDIR, ELOOP, + ENOENT, ENAMETOOLONG, EMFILE, + ENFILE, ENOMEM, ENOEXEC, EINVAL, + E2BIG, EFAULT, EIO, ETXTBSY, + EISDIR, ELIBBAD, EPERM, EINTR, + ECHILD */ +#include /* error() */ +#include /* waitpid(), WIFEXITED(), + WEXITSTATUS() */ +#include /* EX_OSERR, EX_OSFILE, + EX_UNAVAILABLE */ +#include /* va_list, va_start(), ... */ + +sig_atomic_t interrupted_by_signal = 0; +int signal_received; + +/* Function to use when printing errors */ +__attribute__((format (gnu_printf, 3, 4))) +void error_plus(int status, int errnum, const char *formatstring, + ...){ + va_list ap; + char *text; + int ret; + + va_start(ap, formatstring); + ret = vasprintf(&text, formatstring, ap); + if(ret == -1){ + fprintf(stderr, "Mandos plugin %s: ", + program_invocation_short_name); + vfprintf(stderr, formatstring, ap); + fprintf(stderr, ": "); + fprintf(stderr, "%s\n", strerror(errnum)); + error(status, errno, "vasprintf while printing error"); + return; + } + fprintf(stderr, "Mandos plugin "); + error(status, errnum, "%s", text); + free(text); +} + + +static void termination_handler(int signum){ + if(interrupted_by_signal){ + return; + } + interrupted_by_signal = 1; + signal_received = signum; +} + +int main(__attribute__((unused))int argc, + __attribute__((unused))char **argv){ + int ret = 0; + char *prompt = NULL; + DIR *proc_dir = NULL; + pid_t splashy_pid = 0; + pid_t splashy_command_pid = 0; + int exitstatus = EXIT_FAILURE; + + /* Create prompt string */ + { + const char *const cryptsource = getenv("cryptsource"); + const char *const crypttarget = getenv("crypttarget"); + const char *const prompt_start = "getpass " + "Enter passphrase to unlock the disk"; + + if(cryptsource == NULL){ + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s: ", prompt_start); + } else { + ret = asprintf(&prompt, "%s (%s): ", prompt_start, + crypttarget); + } + } else { + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource); + } else { + ret = asprintf(&prompt, "%s %s (%s): ", prompt_start, + cryptsource, crypttarget); + } + } + if(ret == -1){ + prompt = NULL; + exitstatus = EX_OSERR; + goto failure; + } + } + + /* Find splashy process */ + { + const char splashy_name[] = "/sbin/splashy"; + proc_dir = opendir("/proc"); + if(proc_dir == NULL){ + int e = errno; + error_plus(0, errno, "opendir"); + switch(e){ + case EACCES: + case ENOTDIR: + case ELOOP: + case ENOENT: + default: + exitstatus = EX_OSFILE; + break; + case ENAMETOOLONG: + case EMFILE: + case ENFILE: + case ENOMEM: + exitstatus = EX_OSERR; + break; + } + goto failure; + } + for(struct dirent *proc_ent = readdir(proc_dir); + proc_ent != NULL; + proc_ent = readdir(proc_dir)){ + pid_t pid; + { + intmax_t tmpmax; + char *tmp; + errno = 0; + tmpmax = strtoimax(proc_ent->d_name, &tmp, 10); + if(errno != 0 or tmp == proc_ent->d_name or *tmp != '\0' + or tmpmax != (pid_t)tmpmax){ + /* Not a process */ + continue; + } + pid = (pid_t)tmpmax; + } + /* Find the executable name by doing readlink() on the + /proc//exe link */ + char exe_target[sizeof(splashy_name)]; + ssize_t sret; + { + char *exe_link; + ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name); + if(ret == -1){ + error_plus(0, errno, "asprintf"); + exitstatus = EX_OSERR; + goto failure; + } + + /* Check that it refers to a symlink owned by root:root */ + struct stat exe_stat; + ret = lstat(exe_link, &exe_stat); + if(ret == -1){ + if(errno == ENOENT){ + free(exe_link); + continue; + } + int e = errno; + error_plus(0, errno, "lstat"); + free(exe_link); + switch(e){ + case EACCES: + case ENOTDIR: + case ELOOP: + default: + exitstatus = EX_OSFILE; + break; + case ENAMETOOLONG: + exitstatus = EX_OSERR; + break; + } + goto failure; + } + if(not S_ISLNK(exe_stat.st_mode) + or exe_stat.st_uid != 0 + or exe_stat.st_gid != 0){ + free(exe_link); + continue; + } + + sret = readlink(exe_link, exe_target, sizeof(exe_target)); + free(exe_link); + } + if((sret == ((ssize_t)sizeof(exe_target)-1)) + and (memcmp(splashy_name, exe_target, + sizeof(exe_target)-1) == 0)){ + splashy_pid = pid; + break; + } + } + closedir(proc_dir); + proc_dir = NULL; + } + if(splashy_pid == 0){ + exitstatus = EX_UNAVAILABLE; + goto failure; + } + + /* Set up the signal handler */ + { + struct sigaction old_action, + new_action = { .sa_handler = termination_handler, + .sa_flags = 0 }; + sigemptyset(&new_action.sa_mask); + ret = sigaddset(&new_action.sa_mask, SIGINT); + if(ret == -1){ + error_plus(0, errno, "sigaddset"); + exitstatus = EX_OSERR; + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGHUP); + if(ret == -1){ + error_plus(0, errno, "sigaddset"); + exitstatus = EX_OSERR; + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGTERM); + if(ret == -1){ + error_plus(0, errno, "sigaddset"); + exitstatus = EX_OSERR; + goto failure; + } + ret = sigaction(SIGINT, NULL, &old_action); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &new_action, NULL); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + } + ret = sigaction(SIGHUP, NULL, &old_action); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &new_action, NULL); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + } + ret = sigaction(SIGTERM, NULL, &old_action); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &new_action, NULL); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + exitstatus = EX_OSERR; + goto failure; + } + } + } + + if(interrupted_by_signal){ + goto failure; + } + + /* Fork off the splashy command to prompt for password */ + splashy_command_pid = fork(); + if(splashy_command_pid != 0 and interrupted_by_signal){ + goto failure; + } + if(splashy_command_pid == -1){ + error_plus(0, errno, "fork"); + exitstatus = EX_OSERR; + goto failure; + } + /* Child */ + if(splashy_command_pid == 0){ + if(not interrupted_by_signal){ + const char splashy_command[] = "/sbin/splashy_update"; + execl(splashy_command, splashy_command, prompt, (char *)NULL); + int e = errno; + error_plus(0, errno, "execl"); + switch(e){ + case EACCES: + case ENOENT: + case ENOEXEC: + case EINVAL: + _exit(EX_UNAVAILABLE); + case ENAMETOOLONG: + case E2BIG: + case ENOMEM: + case EFAULT: + case EIO: + case EMFILE: + case ENFILE: + case ETXTBSY: + default: + _exit(EX_OSERR); + case ENOTDIR: + case ELOOP: + case EISDIR: +#ifdef ELIBBAD + case ELIBBAD: /* Linux only */ +#endif + case EPERM: + _exit(EX_OSFILE); + } + } + free(prompt); + _exit(EXIT_FAILURE); + } + + /* Parent */ + free(prompt); + prompt = NULL; + + if(interrupted_by_signal){ + goto failure; + } + + /* Wait for command to complete */ + { + int status; + do { + ret = waitpid(splashy_command_pid, &status, 0); + } while(ret == -1 and errno == EINTR + and not interrupted_by_signal); + if(interrupted_by_signal){ + goto failure; + } + if(ret == -1){ + error_plus(0, errno, "waitpid"); + if(errno == ECHILD){ + splashy_command_pid = 0; + } + } else { + /* The child process has exited */ + splashy_command_pid = 0; + if(WIFEXITED(status) and WEXITSTATUS(status) == 0){ + return EXIT_SUCCESS; + } + } + } + + failure: + + free(prompt); + + if(proc_dir != NULL){ + TEMP_FAILURE_RETRY(closedir(proc_dir)); + } + + if(splashy_command_pid != 0){ + TEMP_FAILURE_RETRY(kill(splashy_command_pid, SIGTERM)); + + TEMP_FAILURE_RETRY(kill(splashy_pid, SIGTERM)); + sleep(2); + while(TEMP_FAILURE_RETRY(kill(splashy_pid, 0)) == 0){ + TEMP_FAILURE_RETRY(kill(splashy_pid, SIGKILL)); + sleep(1); + } + pid_t new_splashy_pid = (pid_t)TEMP_FAILURE_RETRY(fork()); + if(new_splashy_pid == 0){ + /* Child; will become new splashy process */ + + /* Make the effective user ID (root) the only user ID instead of + the real user ID (_mandos) */ + ret = setuid(geteuid()); + if(ret == -1){ + error_plus(0, errno, "setuid"); + } + + setsid(); + ret = chdir("/"); + if(ret == -1){ + error_plus(0, errno, "chdir"); + } +/* if(fork() != 0){ */ +/* _exit(EXIT_SUCCESS); */ +/* } */ + ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace stdout */ + if(ret == -1){ + error_plus(0, errno, "dup2"); + _exit(EX_OSERR); + } + + execl("/sbin/splashy", "/sbin/splashy", "boot", (char *)NULL); + { + int e = errno; + error_plus(0, errno, "execl"); + switch(e){ + case EACCES: + case ENOENT: + case ENOEXEC: + default: + _exit(EX_UNAVAILABLE); + case ENAMETOOLONG: + case E2BIG: + case ENOMEM: + _exit(EX_OSERR); + case ENOTDIR: + case ELOOP: + _exit(EX_OSFILE); + } + } + } + } + + if(interrupted_by_signal){ + struct sigaction signal_action; + sigemptyset(&signal_action.sa_mask); + signal_action.sa_handler = SIG_DFL; + ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, + &signal_action, NULL)); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + } + do { + ret = raise(signal_received); + } while(ret != 0 and errno == EINTR); + if(ret != 0){ + error_plus(0, errno, "raise"); + abort(); + } + TEMP_FAILURE_RETRY(pause()); + } + + return exitstatus; +} === added file 'plugins.d/splashy.xml' --- plugins.d/splashy.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/splashy.xml 2019-07-27 10:11:45 +0000 @@ -0,0 +1,284 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Mandos plugin to use splashy to get a + password. + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + This program prompts for a password using + splashy_update + 8 and outputs any given + password to standard output. If no splashy8 + process can be found, this program will immediately exit with an + exit code indicating failure. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + If this program is killed (presumably by + plugin-runner + 8mandos because some other + plugin provided the password), it cannot tell + splashy8 + to abort requesting a password, because + splashy + 8 does not support this. + Therefore, this program will then kill the + running splashy + 8 process and start a + new one, using boot as the only argument. + + + + + OPTIONS + + This program takes no options. + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program was interrupted or encountered an error, and any output + so far could be corrupt and/or truncated, and should therefore + be ignored. + + + + + ENVIRONMENT + + + cryptsource + crypttarget + + + If set, these environment variables will be assumed to + contain the source device name and the target device + mapper name, respectively, and will be shown as part of + the prompt. + + + These variables will normally be inherited from + plugin-runner + 8mandos, which might + have in turn inherited them from its calling process. + + + This behavior is meant to exactly mirror the behavior of + askpass, the default password prompter. + + + + + + + + FILES + + + /sbin/splashy_update + + + This is the command run to retrieve a password from + splashy + 8. See + splashy_update8 + . + + + + + /proc + + + To find the running splashy8 + , this directory will be searched for + numeric entries which will be assumed to be directories. + In all those directories, the exe + entry will be used to determine the name of the running + binary and the effective user and group + ID of the process. See + proc5. + + + + + /sbin/splashy + + + This is the name of the binary which will be searched for + in the process list. See splashy8 + . + + + + + + + + BUGS + + Killing splashy + 8 and starting a new one + is ugly, but necessary as long as it does not support aborting a + password request. + + + + + + EXAMPLE + + Note that normally, this program will not be invoked directly, + but instead started by the Mandos plugin-runner8mandos + . + + + + This program takes no options. + + + &COMMANDNAME; + + + + + + SECURITY + + If this program is killed by a signal, it will kill the process + ID which at the start of this program was + determined to run splashy8 + as root (see also ). There is a very + slight risk that, in the time between those events, that process + ID was freed and then taken up by another + process; the wrong process would then be killed. Now, this + program can only be killed by the user who started it; see + plugin-runner + 8mandos. This program + should therefore be started by a completely separate + non-privileged user, and no other programs should be allowed to + run as that special user. This means that it is not recommended + to use the user "nobody" to start this program, as other + possibly less trusted programs could be running as "nobody", and + they would then be able to kill this program, triggering the + killing of the process ID which may or may not + be splashy + 8. + + + The only other thing that could be considered worthy of note is + this: This program is meant to be run by + plugin-runner8mandos, and will, when run + standalone, outside, in a normal environment, immediately output + on its standard output any presumably secret password it just + received. Therefore, when running this program standalone + (which should never normally be done), take care not to type in + any real secret password by force of habit, since it would then + immediately be shown as output. + + + + + SEE ALSO + + intro + 8mandos, + plugin-runner + 8mandos, + proc + 5, + splashy + 8, + splashy_update + 8 + + +
+ + + + + === added file 'plugins.d/usplash.c' --- plugins.d/usplash.c 1970-01-01 00:00:00 +0000 +++ plugins.d/usplash.c 2018-02-08 10:23:55 +0000 @@ -0,0 +1,685 @@ +/* -*- coding: utf-8 -*- */ +/* + * Usplash - Read a password from usplash and output it + * + * Copyright © 2008-2018 Teddy Hogeborn + * Copyright © 2008-2018 Björn Påhlsson + * + * This file is part of Mandos. + * + * Mandos 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. + * + * Mandos 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 Mandos. If not, see . + * + * Contact the authors at . + */ + +#define _GNU_SOURCE /* asprintf(), TEMP_FAILURE_RETRY() */ +#include /* sig_atomic_t, struct sigaction, + sigemptyset(), sigaddset(), SIGINT, + SIGHUP, SIGTERM, sigaction(), + SIG_IGN, kill(), SIGKILL */ +#include /* bool, false, true */ +#include /* open(), O_WRONLY, O_RDONLY */ +#include /* and, or, not*/ +#include /* errno, EINTR */ +#include +#include /* size_t, ssize_t, pid_t, DIR, struct + dirent */ +#include /* NULL */ +#include /* strlen(), memcmp(), strerror() */ +#include /* asprintf(), vasprintf(), vprintf(), + fprintf() */ +#include /* close(), write(), readlink(), + read(), STDOUT_FILENO, sleep(), + fork(), setuid(), geteuid(), + setsid(), chdir(), dup2(), + STDERR_FILENO, execv() */ +#include /* free(), EXIT_FAILURE, realloc(), + EXIT_SUCCESS, malloc(), _exit(), + getenv() */ +#include /* opendir(), readdir(), closedir() */ +#include /* intmax_t, strtoimax() */ +#include /* struct stat, lstat(), S_ISLNK */ +#include /* EX_OSERR, EX_UNAVAILABLE */ +#include /* argz_count(), argz_extract() */ +#include /* va_list, va_start(), ... */ + +sig_atomic_t interrupted_by_signal = 0; +int signal_received; +const char usplash_name[] = "/sbin/usplash"; + +/* Function to use when printing errors */ +__attribute__((format (gnu_printf, 3, 4))) +void error_plus(int status, int errnum, const char *formatstring, + ...){ + va_list ap; + char *text; + int ret; + + va_start(ap, formatstring); + ret = vasprintf(&text, formatstring, ap); + if(ret == -1){ + fprintf(stderr, "Mandos plugin %s: ", + program_invocation_short_name); + vfprintf(stderr, formatstring, ap); + fprintf(stderr, ": "); + fprintf(stderr, "%s\n", strerror(errnum)); + error(status, errno, "vasprintf while printing error"); + return; + } + fprintf(stderr, "Mandos plugin "); + error(status, errnum, "%s", text); + free(text); +} + +static void termination_handler(int signum){ + if(interrupted_by_signal){ + return; + } + interrupted_by_signal = 1; + signal_received = signum; +} + +static bool usplash_write(int *fifo_fd_r, + const char *cmd, const char *arg){ + /* + * usplash_write(&fd, "TIMEOUT", "15") will write "TIMEOUT 15\0" + * usplash_write(&fd, "PULSATE", NULL) will write "PULSATE\0" + * SEE ALSO + * usplash_write(8) + */ + int ret; + if(*fifo_fd_r == -1){ + ret = open("/dev/.initramfs/usplash_fifo", O_WRONLY); + if(ret == -1){ + return false; + } + *fifo_fd_r = ret; + } + + const char *cmd_line; + size_t cmd_line_len; + char *cmd_line_alloc = NULL; + if(arg == NULL){ + cmd_line = cmd; + cmd_line_len = strlen(cmd) + 1; + } else { + do { + ret = asprintf(&cmd_line_alloc, "%s %s", cmd, arg); + if(ret == -1){ + int e = errno; + close(*fifo_fd_r); + errno = e; + return false; + } + } while(ret == -1); + cmd_line = cmd_line_alloc; + cmd_line_len = (size_t)ret + 1; + } + + size_t written = 0; + ssize_t sret = 0; + while(written < cmd_line_len){ + sret = write(*fifo_fd_r, cmd_line + written, + cmd_line_len - written); + if(sret == -1){ + int e = errno; + close(*fifo_fd_r); + free(cmd_line_alloc); + errno = e; + return false; + } + written += (size_t)sret; + } + free(cmd_line_alloc); + + return true; +} + +/* Create prompt string */ +char *makeprompt(void){ + int ret = 0; + char *prompt; + const char *const cryptsource = getenv("cryptsource"); + const char *const crypttarget = getenv("crypttarget"); + const char prompt_start[] = "Enter passphrase to unlock the disk"; + + if(cryptsource == NULL){ + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s: ", prompt_start); + } else { + ret = asprintf(&prompt, "%s (%s): ", prompt_start, + crypttarget); + } + } else { + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource); + } else { + ret = asprintf(&prompt, "%s %s (%s): ", prompt_start, + cryptsource, crypttarget); + } + } + if(ret == -1){ + return NULL; + } + return prompt; +} + +pid_t find_usplash(char **cmdline_r, size_t *cmdline_len_r){ + int ret = 0; + ssize_t sret = 0; + char *cmdline = NULL; + size_t cmdline_len = 0; + DIR *proc_dir = opendir("/proc"); + if(proc_dir == NULL){ + error_plus(0, errno, "opendir"); + return -1; + } + errno = 0; + for(struct dirent *proc_ent = readdir(proc_dir); + proc_ent != NULL; + proc_ent = readdir(proc_dir)){ + pid_t pid; + { + intmax_t tmpmax; + char *tmp; + tmpmax = strtoimax(proc_ent->d_name, &tmp, 10); + if(errno != 0 or tmp == proc_ent->d_name or *tmp != '\0' + or tmpmax != (pid_t)tmpmax){ + /* Not a process */ + errno = 0; + continue; + } + pid = (pid_t)tmpmax; + } + /* Find the executable name by doing readlink() on the + /proc//exe link */ + char exe_target[sizeof(usplash_name)]; + { + /* create file name string */ + char *exe_link; + ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name); + if(ret == -1){ + error_plus(0, errno, "asprintf"); + goto fail_find_usplash; + } + + /* Check that it refers to a symlink owned by root:root */ + struct stat exe_stat; + ret = lstat(exe_link, &exe_stat); + if(ret == -1){ + if(errno == ENOENT){ + free(exe_link); + continue; + } + error_plus(0, errno, "lstat"); + free(exe_link); + goto fail_find_usplash; + } + if(not S_ISLNK(exe_stat.st_mode) + or exe_stat.st_uid != 0 + or exe_stat.st_gid != 0){ + free(exe_link); + continue; + } + + sret = readlink(exe_link, exe_target, sizeof(exe_target)); + free(exe_link); + } + /* Compare executable name */ + if((sret != ((ssize_t)sizeof(exe_target)-1)) + or (memcmp(usplash_name, exe_target, + sizeof(exe_target)-1) != 0)){ + /* Not it */ + continue; + } + /* Found usplash */ + /* Read and save the command line of usplash in "cmdline" */ + { + /* Open /proc//cmdline */ + int cl_fd; + { + char *cmdline_filename; + ret = asprintf(&cmdline_filename, "/proc/%s/cmdline", + proc_ent->d_name); + if(ret == -1){ + error_plus(0, errno, "asprintf"); + goto fail_find_usplash; + } + cl_fd = open(cmdline_filename, O_RDONLY); + free(cmdline_filename); + if(cl_fd == -1){ + error_plus(0, errno, "open"); + goto fail_find_usplash; + } + } + size_t cmdline_allocated = 0; + char *tmp; + const size_t blocksize = 1024; + do { + /* Allocate more space? */ + if(cmdline_len + blocksize > cmdline_allocated){ + tmp = realloc(cmdline, cmdline_allocated + blocksize); + if(tmp == NULL){ + error_plus(0, errno, "realloc"); + close(cl_fd); + goto fail_find_usplash; + } + cmdline = tmp; + cmdline_allocated += blocksize; + } + /* Read data */ + sret = read(cl_fd, cmdline + cmdline_len, + cmdline_allocated - cmdline_len); + if(sret == -1){ + error_plus(0, errno, "read"); + close(cl_fd); + goto fail_find_usplash; + } + cmdline_len += (size_t)sret; + } while(sret != 0); + ret = close(cl_fd); + if(ret == -1){ + error_plus(0, errno, "close"); + goto fail_find_usplash; + } + } + /* Close directory */ + ret = closedir(proc_dir); + if(ret == -1){ + error_plus(0, errno, "closedir"); + goto fail_find_usplash; + } + /* Success */ + *cmdline_r = cmdline; + *cmdline_len_r = cmdline_len; + return pid; + } + + fail_find_usplash: + + free(cmdline); + if(proc_dir != NULL){ + int e = errno; + closedir(proc_dir); + errno = e; + } + return 0; +} + +int main(__attribute__((unused))int argc, + __attribute__((unused))char **argv){ + int ret = 0; + ssize_t sret; + int fifo_fd = -1; + int outfifo_fd = -1; + char *buf = NULL; + size_t buf_len = 0; + pid_t usplash_pid = -1; + bool usplash_accessed = false; + int status = EXIT_FAILURE; /* Default failure exit status */ + + char *prompt = makeprompt(); + if(prompt == NULL){ + status = EX_OSERR; + goto failure; + } + + /* Find usplash process */ + char *cmdline = NULL; + size_t cmdline_len = 0; + usplash_pid = find_usplash(&cmdline, &cmdline_len); + if(usplash_pid == 0){ + status = EX_UNAVAILABLE; + goto failure; + } + + /* Set up the signal handler */ + { + struct sigaction old_action, + new_action = { .sa_handler = termination_handler, + .sa_flags = 0 }; + sigemptyset(&new_action.sa_mask); + ret = sigaddset(&new_action.sa_mask, SIGINT); + if(ret == -1){ + error_plus(0, errno, "sigaddset"); + status = EX_OSERR; + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGHUP); + if(ret == -1){ + error_plus(0, errno, "sigaddset"); + status = EX_OSERR; + goto failure; + } + ret = sigaddset(&new_action.sa_mask, SIGTERM); + if(ret == -1){ + error_plus(0, errno, "sigaddset"); + status = EX_OSERR; + goto failure; + } + ret = sigaction(SIGINT, NULL, &old_action); + if(ret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGINT, &new_action, NULL); + if(ret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + } + ret = sigaction(SIGHUP, NULL, &old_action); + if(ret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGHUP, &new_action, NULL); + if(ret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + } + ret = sigaction(SIGTERM, NULL, &old_action); + if(ret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(SIGTERM, &new_action, NULL); + if(ret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "sigaction"); + status = EX_OSERR; + } + goto failure; + } + } + } + + usplash_accessed = true; + /* Write command to FIFO */ + if(not usplash_write(&fifo_fd, "TIMEOUT", "0")){ + if(errno != EINTR){ + error_plus(0, errno, "usplash_write"); + status = EX_OSERR; + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + if(not usplash_write(&fifo_fd, "INPUTQUIET", prompt)){ + if(errno != EINTR){ + error_plus(0, errno, "usplash_write"); + status = EX_OSERR; + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + free(prompt); + prompt = NULL; + + /* Read reply from usplash */ + /* Open FIFO */ + outfifo_fd = open("/dev/.initramfs/usplash_outfifo", O_RDONLY); + if(outfifo_fd == -1){ + if(errno != EINTR){ + error_plus(0, errno, "open"); + status = EX_OSERR; + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + /* Read from FIFO */ + size_t buf_allocated = 0; + const size_t blocksize = 1024; + do { + /* Allocate more space */ + if(buf_len + blocksize > buf_allocated){ + char *tmp = realloc(buf, buf_allocated + blocksize); + if(tmp == NULL){ + if(errno != EINTR){ + error_plus(0, errno, "realloc"); + status = EX_OSERR; + } + goto failure; + } + buf = tmp; + buf_allocated += blocksize; + } + sret = read(outfifo_fd, buf + buf_len, + buf_allocated - buf_len); + if(sret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "read"); + status = EX_OSERR; + } + close(outfifo_fd); + goto failure; + } + if(interrupted_by_signal){ + break; + } + + buf_len += (size_t)sret; + } while(sret != 0); + ret = close(outfifo_fd); + if(ret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "close"); + status = EX_OSERR; + } + goto failure; + } + outfifo_fd = -1; + + if(interrupted_by_signal){ + goto failure; + } + + if(not usplash_write(&fifo_fd, "TIMEOUT", "15")){ + if(errno != EINTR){ + error_plus(0, errno, "usplash_write"); + status = EX_OSERR; + } + goto failure; + } + + if(interrupted_by_signal){ + goto failure; + } + + ret = close(fifo_fd); + if(ret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "close"); + status = EX_OSERR; + } + goto failure; + } + fifo_fd = -1; + + /* Print password to stdout */ + size_t written = 0; + while(written < buf_len){ + do { + sret = write(STDOUT_FILENO, buf + written, buf_len - written); + if(sret == -1){ + if(errno != EINTR){ + error_plus(0, errno, "write"); + status = EX_OSERR; + } + goto failure; + } + } while(sret == -1); + + if(interrupted_by_signal){ + goto failure; + } + written += (size_t)sret; + } + free(buf); + buf = NULL; + + if(interrupted_by_signal){ + goto failure; + } + + free(cmdline); + return EXIT_SUCCESS; + + failure: + + free(buf); + + free(prompt); + + /* If usplash was never accessed, we can stop now */ + if(not usplash_accessed){ + return status; + } + + /* Close FIFO */ + if(fifo_fd != -1){ + ret = close(fifo_fd); + if(ret == -1 and errno != EINTR){ + error_plus(0, errno, "close"); + } + fifo_fd = -1; + } + + /* Close output FIFO */ + if(outfifo_fd != -1){ + ret = close(outfifo_fd); + if(ret == -1){ + error_plus(0, errno, "close"); + } + } + + /* Create argv for new usplash*/ + char **cmdline_argv = malloc((argz_count(cmdline, cmdline_len) + 1) + * sizeof(char *)); /* Count args */ + if(cmdline_argv == NULL){ + error_plus(0, errno, "malloc"); + return status; + } + argz_extract(cmdline, cmdline_len, cmdline_argv); /* Create argv */ + + /* Kill old usplash */ + kill(usplash_pid, SIGTERM); + sleep(2); + while(kill(usplash_pid, 0) == 0){ + kill(usplash_pid, SIGKILL); + sleep(1); + } + + pid_t new_usplash_pid = fork(); + if(new_usplash_pid == 0){ + /* Child; will become new usplash process */ + + /* Make the effective user ID (root) the only user ID instead of + the real user ID (_mandos) */ + ret = setuid(geteuid()); + if(ret == -1){ + error_plus(0, errno, "setuid"); + } + + setsid(); + ret = chdir("/"); + if(ret == -1){ + error_plus(0, errno, "chdir"); + _exit(EX_OSERR); + } +/* if(fork() != 0){ */ +/* _exit(EXIT_SUCCESS); */ +/* } */ + ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */ + if(ret == -1){ + error_plus(0, errno, "dup2"); + _exit(EX_OSERR); + } + + execv(usplash_name, cmdline_argv); + if(not interrupted_by_signal){ + error_plus(0, errno, "execv"); + } + free(cmdline); + free(cmdline_argv); + _exit(EX_OSERR); + } + free(cmdline); + free(cmdline_argv); + sleep(2); + if(not usplash_write(&fifo_fd, "PULSATE", NULL)){ + if(errno != EINTR){ + error_plus(0, errno, "usplash_write"); + } + } + + /* Close FIFO (again) */ + if(fifo_fd != -1){ + ret = close(fifo_fd); + if(ret == -1 and errno != EINTR){ + error_plus(0, errno, "close"); + } + fifo_fd = -1; + } + + if(interrupted_by_signal){ + struct sigaction signal_action = { .sa_handler = SIG_DFL }; + sigemptyset(&signal_action.sa_mask); + ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, + &signal_action, NULL)); + if(ret == -1){ + error_plus(0, errno, "sigaction"); + } + do { + ret = raise(signal_received); + } while(ret != 0 and errno == EINTR); + if(ret != 0){ + error_plus(0, errno, "raise"); + abort(); + } + TEMP_FAILURE_RETRY(pause()); + } + + return status; +} === added file 'plugins.d/usplash.xml' --- plugins.d/usplash.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/usplash.xml 2019-07-27 10:11:45 +0000 @@ -0,0 +1,298 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@recompile.se +
+
+ + Teddy + Hogeborn +
+ teddy@recompile.se +
+
+
+ + 2008 + 2009 + 2010 + 2011 + 2012 + 2013 + 2014 + 2015 + 2016 + 2017 + 2018 + 2019 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Mandos plugin to use usplash to get a + password. + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + This program prompts for a password using + usplash8 + and outputs any given password to standard + output. If no usplash8 + process can be found, this program will immediately exit with an + exit code indicating failure. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + If this program is killed (presumably by + plugin-runner + 8mandos because some other + plugin provided the password), it cannot tell + usplash8 + to abort requesting a password, because + usplash + 8 does not support this. + Therefore, this program will then kill the + running usplash + 8 process and start a + new one using the same command line + arguments as the old one was using. + + + + + OPTIONS + + This program takes no options. + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program was interrupted or encountered an error, and any output + so far could be corrupt and/or truncated, and should therefore + be ignored. + + + + + ENVIRONMENT + + + cryptsource + crypttarget + + + If set, these environment variables will be assumed to + contain the source device name and the target device + mapper name, respectively, and will be shown as part of + the prompt. + + + These variables will normally be inherited from + plugin-runner + 8mandos, which might + have in turn inherited them from its calling process. + + + This behavior is meant to exactly mirror the behavior of + askpass, the default password prompter. + + + + + + + + FILES + + + /dev/.initramfs/usplash_fifo + + + This is the FIFO to where this program + will write the commands for usplash8 + . See fifo7 + . + + + + + /dev/.initramfs/usplash_outfifo + + + This is the FIFO where this program + will read the password from usplash8 + . See fifo7 + . + + + + + /proc + + + To find the running usplash8 + , this directory will be searched for + numeric entries which will be assumed to be directories. + In all those directories, the exe and + cmdline entries will be used to + determine the name of the running binary, effective user + and group ID, and the command line + arguments. See proc5 + . + + + + + /sbin/usplash + + + This is the name of the binary which will be searched for + in the process list. See usplash8 + . + + + + + + + + BUGS + + Killing usplash + 8 and starting a new one + is ugly, but necessary as long as it does not support aborting a + password request. + + + + + + EXAMPLE + + Note that normally, this program will not be invoked directly, + but instead started by the Mandos plugin-runner8mandos + . + + + + This program takes no options. + + + &COMMANDNAME; + + + + + + SECURITY + + If this program is killed by a signal, it will kill the process + ID which at the start of this program was + determined to run usplash8 + as root (see also ). There is a very + slight risk that, in the time between those events, that process + ID was freed and then taken up by another + process; the wrong process would then be killed. Now, this + program can only be killed by the user who started it; see + plugin-runner + 8mandos. This program + should therefore be started by a completely separate + non-privileged user, and no other programs should be allowed to + run as that special user. This means that it is not recommended + to use the user "nobody" to start this program, as other + possibly less trusted programs could be running as "nobody", and + they would then be able to kill this program, triggering the + killing of the process ID which may or may not + be usplash + 8. + + + The only other thing that could be considered worthy of note is + this: This program is meant to be run by + plugin-runner8mandos, and will, when run + standalone, outside, in a normal environment, immediately output + on its standard output any presumably secret password it just + received. Therefore, when running this program standalone + (which should never normally be done), take care not to type in + any real secret password by force of habit, since it would then + immediately be shown as output. + + + + + SEE ALSO + + intro + 8mandos, + fifo + 7, + plugin-runner + 8mandos, + proc + 5, + usplash + 8 + + +
+ + + + + === added file 'sysusers.d-mandos.conf' --- sysusers.d-mandos.conf 1970-01-01 00:00:00 +0000 +++ sysusers.d-mandos.conf 2019-08-18 00:42:22 +0000 @@ -0,0 +1,3 @@ +# This file will be installed as mandos.conf and/or mandos-client.conf +# in the /usr/lib/sysusers.d directory. See sysusers.d(5) +u _mandos - "Mandos password system" === added file 'tmpfiles.d-mandos.conf' --- tmpfiles.d-mandos.conf 1970-01-01 00:00:00 +0000 +++ tmpfiles.d-mandos.conf 2016-03-19 03:51:23 +0000 @@ -0,0 +1,1 @@ +d /var/lib/mandos 700 _mandos _mandos