From 82e6a091225664c757f56b38f806401910889773 Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 2 Jul 2013 15:30:02 -0300 Subject: Move the key manager from leap_client repo to its own repo. --- keymanager/LICENSE | 698 +++++++++++++++++++++ keymanager/README.rst | 6 + keymanager/setup.py | 58 ++ keymanager/src/leap/__init__.py | 6 + keymanager/src/leap/keymanager/__init__.py | 336 ++++++++++ keymanager/src/leap/keymanager/errors.py | 86 +++ keymanager/src/leap/keymanager/gpg.py | 397 ++++++++++++ keymanager/src/leap/keymanager/keys.py | 285 +++++++++ keymanager/src/leap/keymanager/openpgp.py | 636 +++++++++++++++++++ keymanager/src/leap/keymanager/tests/__init__.py | 0 .../src/leap/keymanager/tests/test_keymanager.py | 676 ++++++++++++++++++++ 11 files changed, 3184 insertions(+) create mode 100644 keymanager/LICENSE create mode 100644 keymanager/README.rst create mode 100644 keymanager/setup.py create mode 100644 keymanager/src/leap/__init__.py create mode 100644 keymanager/src/leap/keymanager/__init__.py create mode 100644 keymanager/src/leap/keymanager/errors.py create mode 100644 keymanager/src/leap/keymanager/gpg.py create mode 100644 keymanager/src/leap/keymanager/keys.py create mode 100644 keymanager/src/leap/keymanager/openpgp.py create mode 100644 keymanager/src/leap/keymanager/tests/__init__.py create mode 100644 keymanager/src/leap/keymanager/tests/test_keymanager.py diff --git a/keymanager/LICENSE b/keymanager/LICENSE new file mode 100644 index 0000000..5f7cfba --- /dev/null +++ b/keymanager/LICENSE @@ -0,0 +1,698 @@ + 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 +. + +-------------------------------------------------------------------------------- + +Leap-client also uses third party icons: + +--- +data/images/Arrow-Up-32.png +data/images/Arrow-Down-32.png + +Author: Liam McKay +License: GNU General Public License - http://en.wikipedia.org/wiki/GNU_General_Public_License +WebSite: http://wefunction.com/ +IconPackage: WooFunction icon pack - http://www.iconspedia.com/pack/woofunction-icons-4136/ +--- +data/images/Globe.png + +Author: Everaldo Coelho +License: LGPL - http://www.gnu.org/licenses/lgpl.html +WebSite: http://www.everaldo.com/ +--- +data/images/oxygen-icons/ + +License: LGPL - http://www.gnu.org/licenses/lgpl.html +Website: http://www.oxygen-icons.org/ diff --git a/keymanager/README.rst b/keymanager/README.rst new file mode 100644 index 0000000..f542214 --- /dev/null +++ b/keymanager/README.rst @@ -0,0 +1,6 @@ +LEAP's Key Manager +================== + +The Key Manager is a Nicknym agent for LEAP client: + + https://leap.se/pt/docs/design/nicknym diff --git a/keymanager/setup.py b/keymanager/setup.py new file mode 100644 index 0000000..83aeddb --- /dev/null +++ b/keymanager/setup.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# setup.py +# Copyright (C) 2013 LEAP +# +# 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 . + + +from setuptools import ( + setup, + find_packages +) + + +install_requirements = [ + 'leap.common', + 'simplejson', + 'requests', + 'python-gnupg', +] + + +tests_requirements = [ + 'mock', + 'leap.soledad', +] + + +setup( + name='leap.keymanager', + version='0.2.1', + url='https://leap.se/', + license='GPLv3+', + description='LEAP\'s Key Manager', + author='The LEAP Encryption Access Project', + author_email='info@leap.se', + long_description=( + "The Key Manager handles all types of keys to allow for " + "point-to-point encryption between parties communicating through " + "LEAP infrastructure." + ), + namespace_packages=["leap"], + packages=find_packages('src', exclude=['leap.keymanager.tests']), + package_dir={'': 'src'}, + test_suite='leap.keymanager.tests', + install_requires=install_requirements, + tests_require=tests_requirements, +) diff --git a/keymanager/src/leap/__init__.py b/keymanager/src/leap/__init__.py new file mode 100644 index 0000000..f48ad10 --- /dev/null +++ b/keymanager/src/leap/__init__.py @@ -0,0 +1,6 @@ +# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py new file mode 100644 index 0000000..e1f318c --- /dev/null +++ b/keymanager/src/leap/keymanager/__init__.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2013 LEAP +# +# 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 . + + +""" +Key Manager is a Nicknym agent for LEAP client. +""" + +import requests + +from leap.common.check import leap_assert +from leap.keymanager.errors import ( + KeyNotFound, + NoPasswordGiven, +) +from leap.keymanager.keys import ( + build_key_from_dict, + KEYMANAGER_KEY_TAG, + TAGS_PRIVATE_INDEX, +) +from leap.keymanager.openpgp import ( + OpenPGPKey, + OpenPGPScheme, +) + + +# +# The Key Manager +# + +class KeyManager(object): + + # + # server's key storage constants + # + + OPENPGP_KEY = 'openpgp' + PUBKEY_KEY = "user[public_key]" + + def __init__(self, address, nickserver_uri, soledad, session_id=None, + ca_cert_path=None, api_uri=None, api_version=None, uid=None): + """ + Initialize a Key Manager for user's C{address} with provider's + nickserver reachable in C{url}. + + :param address: The address of the user of this Key Manager. + :type address: str + :param url: The URL of the nickserver. + :type url: str + :param soledad: A Soledad instance for local storage of keys. + :type soledad: leap.soledad.Soledad + :param session_id: The session ID for interacting with the webapp API. + :type session_id: str + :param ca_cert_path: The path to the CA certificate. + :type ca_cert_path: str + :param api_uri: The URI of the webapp API. + :type api_uri: str + :param api_version: The version of the webapp API. + :type api_version: str + :param uid: The users' UID. + :type uid: str + """ + self._address = address + self._nickserver_uri = nickserver_uri + self._soledad = soledad + self._session_id = session_id + self.ca_cert_path = ca_cert_path + self.api_uri = api_uri + self.api_version = api_version + self.uid = uid + # a dict to map key types to their handlers + self._wrapper_map = { + OpenPGPKey: OpenPGPScheme(soledad), + # other types of key will be added to this mapper. + } + # the following are used to perform https requests + self._fetcher = requests + self._session = self._fetcher.session() + + # + # utilities + # + + def _key_class_from_type(self, ktype): + """ + Return key class from string representation of key type. + """ + return filter( + lambda klass: str(klass) == ktype, + self._wrapper_map).pop() + + def _get(self, uri, data=None): + """ + Send a GET request to C{uri} containing C{data}. + + :param uri: The URI of the request. + :type uri: str + :param data: The body of the request. + :type data: dict, str or file + + :return: The response to the request. + :rtype: requests.Response + """ + leap_assert( + self._ca_cert_path is not None, + 'We need the CA certificate path!') + res = self._fetcher.get(uri, data=data, verify=self._ca_cert_path) + # assert that the response is valid + res.raise_for_status() + leap_assert( + res.headers['content-type'].startswith('application/json'), + 'Content-type is not JSON.') + return res + + def _put(self, uri, data=None): + """ + Send a PUT request to C{uri} containing C{data}. + + The request will be sent using the configured CA certificate path to + verify the server certificate and the configured session id for + authentication. + + :param uri: The URI of the request. + :type uri: str + :param data: The body of the request. + :type data: dict, str or file + + :return: The response to the request. + :rtype: requests.Response + """ + leap_assert( + self._ca_cert_path is not None, + 'We need the CA certificate path!') + leap_assert( + self._session_id is not None, + 'We need a session_id to interact with webapp!') + res = self._fetcher.put( + uri, data=data, verify=self._ca_cert_path, + cookies={'_session_id': self._session_id}) + # assert that the response is valid + res.raise_for_status() + return res + + def _fetch_keys_from_server(self, address): + """ + Fetch keys bound to C{address} from nickserver and insert them in + local database. + + :param address: The address bound to the keys. + :type address: str + + @raise KeyNotFound: If the key was not found on nickserver. + """ + # request keys from the nickserver + server_keys = self._get( + self._nickserver_uri, {'address': address}).json() + # insert keys in local database + if self.OPENPGP_KEY in server_keys: + self._wrapper_map[OpenPGPKey].put_ascii_key( + server_keys['openpgp']) + + # + # key management + # + + def send_key(self, ktype): + """ + Send user's key of type C{ktype} to provider. + + Public key bound to user's is sent to provider, which will sign it and + replace any prior keys for the same address in its database. + + If C{send_private} is True, then the private key is encrypted with + C{password} and sent to server in the same request, together with a + hash string of user's address and password. The encrypted private key + will be saved in the server in a way it is publicly retrievable + through the hash string. + + :param ktype: The type of the key. + :type ktype: KeyType + + @raise KeyNotFound: If the key was not found in local database. + """ + leap_assert( + ktype is OpenPGPKey, + 'For now we only know how to send OpenPGP public keys.') + # prepare the public key bound to address + pubkey = self.get_key( + self._address, ktype, private=False, fetch_remote=False) + data = { + self.PUBKEY_KEY: pubkey.key_data + } + uri = "%s/%s/users/%s.json" % ( + self._api_uri, + self._api_version, + self._uid) + self._put(uri, data) + + def get_key(self, address, ktype, private=False, fetch_remote=True): + """ + Return a key of type C{ktype} bound to C{address}. + + First, search for the key in local storage. If it is not available, + then try to fetch from nickserver. + + :param address: The address bound to the key. + :type address: str + :param ktype: The type of the key. + :type ktype: KeyType + :param private: Look for a private key instead of a public one? + :type private: bool + + :return: A key of type C{ktype} bound to C{address}. + :rtype: EncryptionKey + @raise KeyNotFound: If the key was not found both locally and in + keyserver. + """ + leap_assert( + ktype in self._wrapper_map, + 'Unkown key type: %s.' % str(ktype)) + try: + # return key if it exists in local database + return self._wrapper_map[ktype].get_key(address, private=private) + except KeyNotFound: + # we will only try to fetch a key from nickserver if fetch_remote + # is True and the key is not private. + if fetch_remote is False or private is True: + raise + self._fetch_keys_from_server(address) + return self._wrapper_map[ktype].get_key(address, private=False) + + def get_all_keys_in_local_db(self, private=False): + """ + Return all keys stored in local database. + + :return: A list with all keys in local db. + :rtype: list + """ + return map( + lambda doc: build_key_from_dict( + self._key_class_from_type(doc.content['type']), + doc.content['address'], + doc.content), + self._soledad.get_from_index( + TAGS_PRIVATE_INDEX, + KEYMANAGER_KEY_TAG, + '1' if private else '0')) + + def refresh_keys(self): + """ + Fetch keys from nickserver and update them locally. + """ + addresses = set(map( + lambda doc: doc.address, + self.get_all_keys_in_local_db(private=False))) + for address in addresses: + # do not attempt to refresh our own key + if address == self._address: + continue + self._fetch_keys_from_server(address) + + def gen_key(self, ktype): + """ + Generate a key of type C{ktype} bound to the user's address. + + :param ktype: The type of the key. + :type ktype: KeyType + + :return: The generated key. + :rtype: EncryptionKey + """ + return self._wrapper_map[ktype].gen_key(self._address) + + # + # Setters/getters + # + + def _get_session_id(self): + return self._session_id + + def _set_session_id(self, session_id): + self._session_id = session_id + + session_id = property( + _get_session_id, _set_session_id, doc='The session id.') + + def _get_ca_cert_path(self): + return self._ca_cert_path + + def _set_ca_cert_path(self, ca_cert_path): + self._ca_cert_path = ca_cert_path + + ca_cert_path = property( + _get_ca_cert_path, _set_ca_cert_path, + doc='The path to the CA certificate.') + + def _get_api_uri(self): + return self._api_uri + + def _set_api_uri(self, api_uri): + self._api_uri = api_uri + + api_uri = property( + _get_api_uri, _set_api_uri, doc='The webapp API URI.') + + def _get_api_version(self): + return self._api_version + + def _set_api_version(self, api_version): + self._api_version = api_version + + api_version = property( + _get_api_version, _set_api_version, doc='The webapp API version.') + + def _get_uid(self): + return self._uid + + def _set_uid(self, uid): + self._uid = uid + + uid = property( + _get_uid, _set_uid, doc='The uid of the user.') diff --git a/keymanager/src/leap/keymanager/errors.py b/keymanager/src/leap/keymanager/errors.py new file mode 100644 index 0000000..89949d2 --- /dev/null +++ b/keymanager/src/leap/keymanager/errors.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# errors.py +# Copyright (C) 2013 LEAP +# +# 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 . + + +""" +Errors and exceptions used by the Key Manager. +""" + + +class KeyNotFound(Exception): + """ + Raised when key was no found on keyserver. + """ + pass + + +class KeyAlreadyExists(Exception): + """ + Raised when attempted to create a key that already exists. + """ + pass + + +class KeyAttributesDiffer(Exception): + """ + Raised when trying to delete a key but the stored key differs from the key + passed to the delete_key() method. + """ + pass + + +class NoPasswordGiven(Exception): + """ + Raised when trying to perform some action that needs a password without + providing one. + """ + pass + + +class InvalidSignature(Exception): + """ + Raised when signature could not be verified. + """ + pass + + +class EncryptionFailed(Exception): + """ + Raised upon failures of encryption. + """ + pass + + +class DecryptionFailed(Exception): + """ + Raised upon failures of decryption. + """ + pass + + +class EncryptionDecryptionFailed(Exception): + """ + Raised upon failures of encryption/decryption. + """ + pass + + +class SignFailed(Exception): + """ + Raised when failed to sign. + """ + pass diff --git a/keymanager/src/leap/keymanager/gpg.py b/keymanager/src/leap/keymanager/gpg.py new file mode 100644 index 0000000..15c1d9f --- /dev/null +++ b/keymanager/src/leap/keymanager/gpg.py @@ -0,0 +1,397 @@ +# -*- coding: utf-8 -*- +# gpgwrapper.py +# Copyright (C) 2013 LEAP +# +# 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 . + + +""" +A GPG wrapper used to handle OpenPGP keys. + +This is a temporary class that will be superseded by the a revised version of +python-gnupg. +""" + + +import os +import gnupg +import re +from gnupg import ( + logger, + _is_sequence, + _make_binary_stream, +) + + +class ListPackets(): + """ + Handle status messages for --list-packets. + """ + + def __init__(self, gpg): + """ + Initialize the packet listing handling class. + + :param gpg: GPG object instance. + :type gpg: gnupg.GPG + """ + self.gpg = gpg + self.nodata = None + self.key = None + self.need_passphrase = None + self.need_passphrase_sym = None + self.userid_hint = None + + def handle_status(self, key, value): + """ + Handle one line of the --list-packets status message. + + :param key: The status message key. + :type key: str + :param value: The status message value. + :type value: str + """ + # TODO: write tests for handle_status + if key == 'NODATA': + self.nodata = True + if key == 'ENC_TO': + # This will only capture keys in our keyring. In the future we + # may want to include multiple unknown keys in this list. + self.key, _, _ = value.split() + if key == 'NEED_PASSPHRASE': + self.need_passphrase = True + if key == 'NEED_PASSPHRASE_SYM': + self.need_passphrase_sym = True + if key == 'USERID_HINT': + self.userid_hint = value.strip().split() + + +class GPGWrapper(gnupg.GPG): + """ + This is a temporary class for handling GPG requests, and should be + replaced by a more general class used throughout the project. + """ + + GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg" + GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS + + def __init__(self, gpgbinary=GNUPG_BINARY, gnupghome=GNUPG_HOME, + verbose=False, use_agent=False, keyring=None, options=None): + """ + Initialize a GnuPG process wrapper. + + :param gpgbinary: Name for GnuPG binary executable. + :type gpgbinary: C{str} + :param gpghome: Full pathname to directory containing the public and + private keyrings. + :type gpghome: C{str} + :param keyring: Name of alternative keyring file to use. If specified, + the default keyring is not used. + :param verbose: Should some verbose info be output? + :type verbose: bool + :param use_agent: Should pass `--use-agent` to GPG binary? + :type use_agent: bool + :param keyring: Path for the keyring to use. + :type keyring: str + @options: A list of additional options to pass to the GPG binary. + :type options: list + + @raise: RuntimeError with explanation message if there is a problem + invoking gpg. + """ + gnupg.GPG.__init__(self, gnupghome=gnupghome, gpgbinary=gpgbinary, + verbose=verbose, use_agent=use_agent, + keyring=keyring, options=options) + self.result_map['list-packets'] = ListPackets + + def find_key_by_email(self, email, secret=False): + """ + Find user's key based on their email. + + :param email: Email address of key being searched for. + :type email: str + :param secret: Should we search for a secret key? + :type secret: bool + + :return: The fingerprint of the found key. + :rtype: str + """ + for key in self.list_keys(secret=secret): + for uid in key['uids']: + if re.search(email, uid): + return key + raise LookupError("GnuPG public key for email %s not found!" % email) + + def find_key_by_subkey(self, subkey, secret=False): + """ + Find user's key based on a subkey fingerprint. + + :param email: Subkey fingerprint of the key being searched for. + :type email: str + :param secret: Should we search for a secret key? + :type secret: bool + + :return: The fingerprint of the found key. + :rtype: str + """ + for key in self.list_keys(secret=secret): + for sub in key['subkeys']: + if sub[0] == subkey: + return key + raise LookupError( + "GnuPG public key for subkey %s not found!" % subkey) + + def find_key_by_keyid(self, keyid, secret=False): + """ + Find user's key based on the key ID. + + :param email: The key ID of the key being searched for. + :type email: str + :param secret: Should we search for a secret key? + :type secret: bool + + :return: The fingerprint of the found key. + :rtype: str + """ + for key in self.list_keys(secret=secret): + if keyid == key['keyid']: + return key + raise LookupError( + "GnuPG public key for keyid %s not found!" % keyid) + + def find_key_by_fingerprint(self, fingerprint, secret=False): + """ + Find user's key based on the key fingerprint. + + :param email: The fingerprint of the key being searched for. + :type email: str + :param secret: Should we search for a secret key? + :type secret: bool + + :return: The fingerprint of the found key. + :rtype: str + """ + for key in self.list_keys(secret=secret): + if fingerprint == key['fingerprint']: + return key + raise LookupError( + "GnuPG public key for fingerprint %s not found!" % fingerprint) + + def encrypt(self, data, recipient, sign=None, always_trust=True, + passphrase=None, symmetric=False): + """ + Encrypt data using GPG. + + :param data: The data to be encrypted. + :type data: str + :param recipient: The address of the public key to be used. + :type recipient: str + :param sign: Should the encrypted content be signed? + :type sign: bool + :param always_trust: Skip key validation and assume that used keys + are always fully trusted? + :type always_trust: bool + :param passphrase: The passphrase to be used if symmetric encryption + is desired. + :type passphrase: str + :param symmetric: Should we encrypt to a password? + :type symmetric: bool + + :return: An object with encrypted result in the `data` field. + :rtype: gnupg.Crypt + """ + # TODO: devise a way so we don't need to "always trust". + return gnupg.GPG.encrypt(self, data, recipient, sign=sign, + always_trust=always_trust, + passphrase=passphrase, + symmetric=symmetric, + cipher_algo='AES256') + + def decrypt(self, data, always_trust=True, passphrase=None): + """ + Decrypt data using GPG. + + :param data: The data to be decrypted. + :type data: str + :param always_trust: Skip key validation and assume that used keys + are always fully trusted? + :type always_trust: bool + :param passphrase: The passphrase to be used if symmetric encryption + is desired. + :type passphrase: str + + :return: An object with decrypted result in the `data` field. + :rtype: gnupg.Crypt + """ + # TODO: devise a way so we don't need to "always trust". + return gnupg.GPG.decrypt(self, data, always_trust=always_trust, + passphrase=passphrase) + + def send_keys(self, keyserver, *keyids): + """ + Send keys to a keyserver + + :param keyserver: The keyserver to send the keys to. + :type keyserver: str + :param keyids: The key ids to send. + :type keyids: list + + :return: A list of keys sent to server. + :rtype: gnupg.ListKeys + """ + # TODO: write tests for this. + # TODO: write a SendKeys class to handle status for this. + result = self.result_map['list'](self) + gnupg.logger.debug('send_keys: %r', keyids) + data = gnupg._make_binary_stream("", self.encoding) + args = ['--keyserver', keyserver, '--send-keys'] + args.extend(keyids) + self._handle_io(args, data, result, binary=True) + gnupg.logger.debug('send_keys result: %r', result.__dict__) + data.close() + return result + + def encrypt_file(self, file, recipients, sign=None, + always_trust=False, passphrase=None, + armor=True, output=None, symmetric=False, + cipher_algo=None): + """ + Encrypt the message read from the file-like object 'file'. + + :param file: The file to be encrypted. + :type data: file + :param recipient: The address of the public key to be used. + :type recipient: str + :param sign: Should the encrypted content be signed? + :type sign: bool + :param always_trust: Skip key validation and assume that used keys + are always fully trusted? + :type always_trust: bool + :param passphrase: The passphrase to be used if symmetric encryption + is desired. + :type passphrase: str + :param armor: Create ASCII armored output? + :type armor: bool + :param output: Path of file to write results in. + :type output: str + :param symmetric: Should we encrypt to a password? + :type symmetric: bool + :param cipher_algo: Algorithm to use. + :type cipher_algo: str + + :return: An object with encrypted result in the `data` field. + :rtype: gnupg.Crypt + """ + args = ['--encrypt'] + if symmetric: + args = ['--symmetric'] + if cipher_algo: + args.append('--cipher-algo %s' % cipher_algo) + else: + args = ['--encrypt'] + if not _is_sequence(recipients): + recipients = (recipients,) + for recipient in recipients: + args.append('--recipient "%s"' % recipient) + if armor: # create ascii-armored output - set to False for binary + args.append('--armor') + if output: # write the output to a file with the specified name + if os.path.exists(output): + os.remove(output) # to avoid overwrite confirmation message + args.append('--output "%s"' % output) + if sign: + args.append('--sign --default-key "%s"' % sign) + if always_trust: + args.append("--always-trust") + result = self.result_map['crypt'](self) + self._handle_io(args, file, result, passphrase=passphrase, binary=True) + logger.debug('encrypt result: %r', result.data) + return result + + def list_packets(self, data): + """ + List the sequence of packets. + + :param data: The data to extract packets from. + :type data: str + + :return: An object with packet info. + :rtype ListPackets + """ + args = ["--list-packets"] + result = self.result_map['list-packets'](self) + self._handle_io( + args, + _make_binary_stream(data, self.encoding), + result, + ) + return result + + def encrypted_to(self, data): + """ + Return the key to which data is encrypted to. + + :param data: The data to be examined. + :type data: str + + :return: The fingerprint of the key to which data is encrypted to. + :rtype: str + """ + # TODO: make this support multiple keys. + result = self.list_packets(data) + if not result.key: + raise LookupError( + "Content is not encrypted to a GnuPG key!") + try: + return self.find_key_by_keyid(result.key) + except: + return self.find_key_by_subkey(result.key) + + def is_encrypted_sym(self, data): + """ + Say whether some chunk of data is encrypted to a symmetric key. + + :param data: The data to be examined. + :type data: str + + :return: Whether data is encrypted to a symmetric key. + :rtype: bool + """ + result = self.list_packets(data) + return bool(result.need_passphrase_sym) + + def is_encrypted_asym(self, data): + """ + Say whether some chunk of data is encrypted to a private key. + + :param data: The data to be examined. + :type data: str + + :return: Whether data is encrypted to a private key. + :rtype: bool + """ + result = self.list_packets(data) + return bool(result.key) + + def is_encrypted(self, data): + """ + Say whether some chunk of data is encrypted to a key. + + :param data: The data to be examined. + :type data: str + + :return: Whether data is encrypted to a key. + :rtype: bool + """ + return self.is_encrypted_asym(data) or self.is_encrypted_sym(data) diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py new file mode 100644 index 0000000..44bd587 --- /dev/null +++ b/keymanager/src/leap/keymanager/keys.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- +# keys.py +# Copyright (C) 2013 LEAP +# +# 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 . + + +""" +Abstact key type and encryption scheme representations. +""" + + +try: + import simplejson as json +except ImportError: + import json # noqa +import re + + +from abc import ABCMeta, abstractmethod +from leap.common.check import leap_assert + + +# +# Dictionary keys used for storing cryptographic keys. +# + +KEY_ADDRESS_KEY = 'address' +KEY_TYPE_KEY = 'type' +KEY_ID_KEY = 'key_id' +KEY_FINGERPRINT_KEY = 'fingerprint' +KEY_DATA_KEY = 'key_data' +KEY_PRIVATE_KEY = 'private' +KEY_LENGTH_KEY = 'length' +KEY_EXPIRY_DATE_KEY = 'expiry_date' +KEY_FIRST_SEEN_AT_KEY = 'first_seen_at' +KEY_LAST_AUDITED_AT_KEY = 'last_audited_at' +KEY_VALIDATION_KEY = 'validation' +KEY_TAGS_KEY = 'tags' + + +# +# Key storage constants +# + +KEYMANAGER_KEY_TAG = 'keymanager-key' + + +# +# key indexing constants. +# + +TAGS_PRIVATE_INDEX = 'by-tags-private' +TAGS_ADDRESS_PRIVATE_INDEX = 'by-tags-address-private' +INDEXES = { + TAGS_PRIVATE_INDEX: [ + KEY_TAGS_KEY, + 'bool(%s)' % KEY_PRIVATE_KEY, + ], + TAGS_ADDRESS_PRIVATE_INDEX: [ + KEY_TAGS_KEY, + KEY_ADDRESS_KEY, + 'bool(%s)' % KEY_PRIVATE_KEY, + ] +} + + +# +# Key handling utilities +# + +def is_address(address): + """ + Return whether the given C{address} is in the form user@provider. + + :param address: The address to be tested. + :type address: str + :return: Whether C{address} is in the form user@provider. + :rtype: bool + """ + return bool(re.match('[\w.-]+@[\w.-]+', address)) + + +def build_key_from_dict(kClass, address, kdict): + """ + Build an C{kClass} key bound to C{address} based on info in C{kdict}. + + :param address: The address bound to the key. + :type address: str + :param kdict: Dictionary with key data. + :type kdict: dict + :return: An instance of the key. + :rtype: C{kClass} + """ + leap_assert( + address == kdict[KEY_ADDRESS_KEY], + 'Wrong address in key data.') + return kClass( + address, + key_id=kdict[KEY_ID_KEY], + fingerprint=kdict[KEY_FINGERPRINT_KEY], + key_data=kdict[KEY_DATA_KEY], + private=kdict[KEY_PRIVATE_KEY], + length=kdict[KEY_LENGTH_KEY], + expiry_date=kdict[KEY_EXPIRY_DATE_KEY], + first_seen_at=kdict[KEY_FIRST_SEEN_AT_KEY], + last_audited_at=kdict[KEY_LAST_AUDITED_AT_KEY], + validation=kdict[KEY_VALIDATION_KEY], # TODO: verify for validation. + ) + + +# +# Abstraction for encryption keys +# + +class EncryptionKey(object): + """ + Abstract class for encryption keys. + + A key is "validated" if the nicknym agent has bound the user address to a + public key. Nicknym supports three different levels of key validation: + + * Level 3 - path trusted: A path of cryptographic signatures can be traced + from a trusted key to the key under evaluation. By default, only the + provider key from the user's provider is a "trusted key". + * level 2 - provider signed: The key has been signed by a provider key for + the same domain, but the provider key is not validated using a trust + path (i.e. it is only registered) + * level 1 - registered: The key has been encountered and saved, it has no + signatures (that are meaningful to the nicknym agent). + """ + + __metaclass__ = ABCMeta + + def __init__(self, address, key_id=None, fingerprint=None, + key_data=None, private=None, length=None, expiry_date=None, + validation=None, first_seen_at=None, last_audited_at=None): + self.address = address + self.key_id = key_id + self.fingerprint = fingerprint + self.key_data = key_data + self.private = private + self.length = length + self.expiry_date = expiry_date + self.validation = validation + self.first_seen_at = first_seen_at + self.last_audited_at = last_audited_at + + def get_json(self): + """ + Return a JSON string describing this key. + + :return: The JSON string describing this key. + :rtype: str + """ + return json.dumps({ + KEY_ADDRESS_KEY: self.address, + KEY_TYPE_KEY: str(self.__class__), + KEY_ID_KEY: self.key_id, + KEY_FINGERPRINT_KEY: self.fingerprint, + KEY_DATA_KEY: self.key_data, + KEY_PRIVATE_KEY: self.private, + KEY_LENGTH_KEY: self.length, + KEY_EXPIRY_DATE_KEY: self.expiry_date, + KEY_VALIDATION_KEY: self.validation, + KEY_FIRST_SEEN_AT_KEY: self.first_seen_at, + KEY_LAST_AUDITED_AT_KEY: self.last_audited_at, + KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG], + }) + + def __repr__(self): + """ + Representation of this class + """ + return u"<%s 0x%s (%s - %s)>" % ( + self.__class__.__name__, + self.key_id, + self.address, + "priv" if self.private else "publ") + + +# +# Encryption schemes +# + +class EncryptionScheme(object): + """ + Abstract class for Encryption Schemes. + + A wrapper for a certain encryption schemes should know how to get and put + keys in local storage using Soledad, how to generate new keys and how to + find out about possibly encrypted content. + """ + + __metaclass__ = ABCMeta + + def __init__(self, soledad): + """ + Initialize this Encryption Scheme. + + :param soledad: A Soledad instance for local storage of keys. + :type soledad: leap.soledad.Soledad + """ + self._soledad = soledad + self._init_indexes() + + def _init_indexes(self): + """ + Initialize the database indexes. + """ + # Ask the database for currently existing indexes. + db_indexes = dict(self._soledad.list_indexes()) + # Loop through the indexes we expect to find. + for name, expression in INDEXES.items(): + if name not in db_indexes: + # The index does not yet exist. + self._soledad.create_index(name, *expression) + continue + if expression == db_indexes[name]: + # The index exists and is up to date. + continue + # The index exists but the definition is not what expected, so we + # delete it and add the proper index expression. + self._soledad.delete_index(name) + self._soledad.create_index(name, *expression) + + @abstractmethod + def get_key(self, address, private=False): + """ + Get key from local storage. + + :param address: The address bound to the key. + :type address: str + :param private: Look for a private key instead of a public one? + :type private: bool + + :return: The key bound to C{address}. + :rtype: EncryptionKey + @raise KeyNotFound: If the key was not found on local storage. + """ + pass + + @abstractmethod + def put_key(self, key): + """ + Put a key in local storage. + + :param key: The key to be stored. + :type key: EncryptionKey + """ + pass + + @abstractmethod + def gen_key(self, address): + """ + Generate a new key. + + :param address: The address bound to the key. + :type address: str + + :return: The key bound to C{address}. + :rtype: EncryptionKey + """ + pass + + @abstractmethod + def delete_key(self, key): + """ + Remove C{key} from storage. + + :param key: The key to be removed. + :type key: EncryptionKey + """ + pass diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py new file mode 100644 index 0000000..d19bb2b --- /dev/null +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -0,0 +1,636 @@ +# -*- coding: utf-8 -*- +# openpgp.py +# Copyright (C) 2013 LEAP +# +# 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 . + +""" +Infrastructure for using OpenPGP keys in Key Manager. +""" +import logging +import os +import re +import shutil +import tempfile + +from leap.common.check import leap_assert, leap_assert_type +from leap.keymanager import errors +from leap.keymanager.keys import ( + EncryptionKey, + EncryptionScheme, + is_address, + build_key_from_dict, + KEYMANAGER_KEY_TAG, + TAGS_ADDRESS_PRIVATE_INDEX, +) +from leap.keymanager.gpg import GPGWrapper + +logger = logging.getLogger(__name__) + + +# +# gpg wrapper and decorator +# + +def temporary_gpgwrapper(keys=None): + """ + Returns a unitary gpg wrapper that implements context manager + protocol. + + :param key_data: ASCII armored key data. + :type key_data: str + + :return: a GPGWrapper instance + :rtype: GPGWrapper + """ + # TODO do here checks on key_data + return TempGPGWrapper(keys=keys) + + +def with_temporary_gpg(fun): + """ + Decorator to add a temporary gpg wrapper as context + to gpg related functions. + + Decorated functions are expected to return a function whose only + argument is a gpgwrapper instance. + """ + def wrapped(*args, **kwargs): + """ + We extract the arguments passed to the wrapped function, + run the function and do validations. + We expect that the positional arguments are `data`, + and an optional `key`. + All the rest of arguments should be passed as named arguments + to allow for a correct unpacking. + """ + if len(args) == 2: + keys = args[1] if isinstance(args[1], OpenPGPKey) else None + else: + keys = None + + # sign/verify keys passed as arguments + sign = kwargs.get('sign', None) + if sign: + keys = [keys, sign] + + verify = kwargs.get('verify', None) + if verify: + keys = [keys, verify] + + # is the wrapped function sign or verify? + fun_name = fun.__name__ + is_sign_function = True if fun_name == "sign" else False + is_verify_function = True if fun_name == "verify" else False + + result = None + + with temporary_gpgwrapper(keys) as gpg: + result = fun(*args, **kwargs)(gpg) + + # TODO: cleanup a little bit the + # validation. maybe delegate to other + # auxiliary functions for clarity. + + ok = getattr(result, 'ok', None) + + stderr = getattr(result, 'stderr', None) + if stderr: + logger.debug("%s" % (stderr,)) + + if ok is False: + raise errors.EncryptionDecryptionFailed( + 'Failed to encrypt/decrypt in %s: %s' % ( + fun.__name__, + stderr)) + + if verify is not None: + # A verify key has been passed + if result.valid is False or \ + verify.fingerprint != result.pubkey_fingerprint: + raise errors.InvalidSignature( + 'Failed to verify signature with key %s: %s' % + (verify.key_id, stderr)) + + if is_sign_function: + # Specific validation for sign function + privkey = gpg.list_keys(secret=True).pop() + rfprint = result.fingerprint + kfprint = privkey['fingerprint'] + if result.fingerprint is None: + raise errors.SignFailed( + 'Failed to sign with key %s: %s' % + (privkey['keyid'], stderr)) + leap_assert( + result.fingerprint == kfprint, + 'Signature and private key fingerprints mismatch: ' + '%s != %s' % + (rfprint, kfprint)) + + if is_verify_function: + # Specific validation for verify function + pubkey = gpg.list_keys().pop() + valid = result.valid + rfprint = result.fingerprint + kfprint = pubkey['fingerprint'] + if valid is False or rfprint != kfprint: + raise errors.InvalidSignature( + 'Failed to verify signature ' + 'with key %s.' % pubkey['keyid']) + result = result.valid + + # ok, enough checks. let's return data if available + if hasattr(result, 'data'): + result = result.data + return result + return wrapped + + +class TempGPGWrapper(object): + """ + A context manager returning a temporary GPG wrapper keyring, which + contains exactly zero or one pubkeys, and zero or one privkeys. + + Temporary unitary keyrings allow the to use GPG's facilities for exactly + one key. This function creates an empty temporary keyring and imports + C{keys} if it is not None. + """ + def __init__(self, keys=None): + """ + :param keys: OpenPGP key, or list of. + :type keys: OpenPGPKey or list of OpenPGPKeys + """ + self._gpg = None + if not keys: + keys = list() + if not isinstance(keys, list): + keys = [keys] + self._keys = keys + for key in filter(None, keys): + leap_assert_type(key, OpenPGPKey) + + def __enter__(self): + """ + Calls the unitary gpgwrapper initializer + + :return: A GPG wrapper with a unitary keyring. + :rtype: gnupg.GPG + """ + self._build_keyring() + return self._gpg + + def __exit__(self, exc_type, exc_value, traceback): + """ + Ensures the gpgwrapper is properly destroyed. + """ + # TODO handle exceptions and log here + self._destroy_keyring() + + def _build_keyring(self): + """ + Create an empty GPG keyring and import C{keys} into it. + + :param keys: List of keys to add to the keyring. + :type keys: list of OpenPGPKey + + :return: A GPG wrapper with a unitary keyring. + :rtype: gnupg.GPG + """ + privkeys = [key for key in self._keys if key and key.private is True] + publkeys = [key for key in self._keys if key and key.private is False] + # here we filter out public keys that have a correspondent + # private key in the list because the private key_data by + # itself is enough to also have the public key in the keyring, + # and we want to count the keys afterwards. + + privaddrs = map(lambda privkey: privkey.address, privkeys) + publkeys = filter( + lambda pubkey: pubkey.address not in privaddrs, publkeys) + + listkeys = lambda: self._gpg.list_keys() + listsecretkeys = lambda: self._gpg.list_keys(secret=True) + + self._gpg = GPGWrapper(gnupghome=tempfile.mkdtemp()) + leap_assert(len(listkeys()) is 0, 'Keyring not empty.') + + # import keys into the keyring: + # concatenating ascii-armored keys, which is correctly + # understood by the GPGWrapper. + + self._gpg.import_keys("".join( + [x.key_data for x in publkeys + privkeys])) + + # assert the number of keys in the keyring + leap_assert( + len(listkeys()) == len(publkeys) + len(privkeys), + 'Wrong number of public keys in keyring: %d, should be %d)' % + (len(listkeys()), len(publkeys) + len(privkeys))) + leap_assert( + len(listsecretkeys()) == len(privkeys), + 'Wrong number of private keys in keyring: %d, should be %d)' % + (len(listsecretkeys()), len(privkeys))) + + def _destroy_keyring(self): + """ + Securely erase a unitary keyring. + """ + # TODO: implement some kind of wiping of data or a more + # secure way that + # does not write to disk. + + try: + for secret in [True, False]: + for key in self._gpg.list_keys(secret=secret): + self._gpg.delete_keys( + key['fingerprint'], + secret=secret) + leap_assert(len(self._gpg.list_keys()) is 0, 'Keyring not empty!') + + except: + raise + + finally: + leap_assert(self._gpg.gnupghome != os.path.expanduser('~/.gnupg'), + "watch out! Tried to remove default gnupg home!") + shutil.rmtree(self._gpg.gnupghome) + + +# +# API functions +# + +@with_temporary_gpg +def encrypt_asym(data, key, passphrase=None, sign=None): + """ + Encrypt C{data} using public @{key} and sign with C{sign} key. + + :param data: The data to be encrypted. + :type data: str + :param pubkey: The key used to encrypt. + :type pubkey: OpenPGPKey + :param sign: The key used for signing. + :type sign: OpenPGPKey + + :return: The encrypted data. + :rtype: str + """ + leap_assert_type(key, OpenPGPKey) + leap_assert(key.private is False, 'Key is not public.') + if sign is not None: + leap_assert_type(sign, OpenPGPKey) + leap_assert(sign.private is True) + + # Here we cannot assert for correctness of sig because the sig is in + # the ciphertext. + # result.ok - (bool) indicates if the operation succeeded + # result.data - (bool) contains the result of the operation + + return lambda gpg: gpg.encrypt( + data, key.fingerprint, + sign=sign.key_id if sign else None, + passphrase=passphrase, symmetric=False) + + +@with_temporary_gpg +def decrypt_asym(data, key, passphrase=None, verify=None): + """ + Decrypt C{data} using private @{key} and verify with C{verify} key. + + :param data: The data to be decrypted. + :type data: str + :param privkey: The key used to decrypt. + :type privkey: OpenPGPKey + :param verify: The key used to verify a signature. + :type verify: OpenPGPKey + + :return: The decrypted data. + :rtype: str + + @raise InvalidSignature: Raised if unable to verify the signature with + C{verify} key. + """ + leap_assert(key.private is True, 'Key is not private.') + if verify is not None: + leap_assert_type(verify, OpenPGPKey) + leap_assert(verify.private is False) + + return lambda gpg: gpg.decrypt( + data, passphrase=passphrase) + + +@with_temporary_gpg +def is_encrypted(data): + """ + Return whether C{data} was encrypted using OpenPGP. + + :param data: The data we want to know about. + :type data: str + + :return: Whether C{data} was encrypted using this wrapper. + :rtype: bool + """ + return lambda gpg: gpg.is_encrypted(data) + + +@with_temporary_gpg +def is_encrypted_asym(data): + """ + Return whether C{data} was asymmetrically encrypted using OpenPGP. + + :param data: The data we want to know about. + :type data: str + + :return: Whether C{data} was encrypted using this wrapper. + :rtype: bool + """ + return lambda gpg: gpg.is_encrypted_asym(data) + + +@with_temporary_gpg +def sign(data, privkey): + """ + Sign C{data} with C{privkey}. + + :param data: The data to be signed. + :type data: str + + :param privkey: The private key to be used to sign. + :type privkey: OpenPGPKey + + :return: The ascii-armored signed data. + :rtype: str + """ + leap_assert_type(privkey, OpenPGPKey) + leap_assert(privkey.private is True) + + # result.fingerprint - contains the fingerprint of the key used to + # sign. + return lambda gpg: gpg.sign(data, keyid=privkey.key_id) + + +@with_temporary_gpg +def verify(data, key): + """ + Verify signed C{data} with C{pubkey}. + + :param data: The data to be verified. + :type data: str + + :param pubkey: The public key to be used on verification. + :type pubkey: OpenPGPKey + + :return: The ascii-armored signed data. + :rtype: str + """ + leap_assert_type(key, OpenPGPKey) + leap_assert(key.private is False) + + return lambda gpg: gpg.verify(data) + + +# +# Helper functions +# + + +def _build_key_from_gpg(address, key, key_data): + """ + Build an OpenPGPKey for C{address} based on C{key} from + local gpg storage. + + ASCII armored GPG key data has to be queried independently in this + wrapper, so we receive it in C{key_data}. + + :param address: The address bound to the key. + :type address: str + :param key: Key obtained from GPG storage. + :type key: dict + :param key_data: Key data obtained from GPG storage. + :type key_data: str + :return: An instance of the key. + :rtype: OpenPGPKey + """ + return OpenPGPKey( + address, + key_id=key['keyid'], + fingerprint=key['fingerprint'], + key_data=key_data, + private=True if key['type'] == 'sec' else False, + length=key['length'], + expiry_date=key['expires'], + validation=None, # TODO: verify for validation. + ) + + +# +# The OpenPGP wrapper +# + +class OpenPGPKey(EncryptionKey): + """ + Base class for OpenPGP keys. + """ + + +class OpenPGPScheme(EncryptionScheme): + """ + A wrapper for OpenPGP keys. + """ + + def __init__(self, soledad): + """ + Initialize the OpenPGP wrapper. + + :param soledad: A Soledad instance for key storage. + :type soledad: leap.soledad.Soledad + """ + EncryptionScheme.__init__(self, soledad) + + def gen_key(self, address): + """ + Generate an OpenPGP keypair bound to C{address}. + + :param address: The address bound to the key. + :type address: str + :return: The key bound to C{address}. + :rtype: OpenPGPKey + @raise KeyAlreadyExists: If key already exists in local database. + """ + # make sure the key does not already exist + leap_assert(is_address(address), 'Not an user address: %s' % address) + try: + self.get_key(address) + raise errors.KeyAlreadyExists(address) + except errors.KeyNotFound: + pass + + def _gen_key(gpg): + params = gpg.gen_key_input( + key_type='RSA', + key_length=4096, + name_real=address, + name_email=address, + name_comment='Generated by LEAP Key Manager.') + gpg.gen_key(params) + pubkeys = gpg.list_keys() + # assert for new key characteristics + leap_assert( + len(pubkeys) is 1, # a unitary keyring! + 'Keyring has wrong number of keys: %d.' % len(pubkeys)) + key = gpg.list_keys(secret=True).pop() + leap_assert( + len(key['uids']) is 1, # with just one uid! + 'Wrong number of uids for key: %d.' % len(key['uids'])) + leap_assert( + re.match('.*<%s>$' % address, key['uids'][0]) is not None, + 'Key not correctly bound to address.') + # insert both public and private keys in storage + for secret in [True, False]: + key = gpg.list_keys(secret=secret).pop() + openpgp_key = _build_key_from_gpg( + address, key, + gpg.export_keys(key['fingerprint'], secret=secret)) + self.put_key(openpgp_key) + + with temporary_gpgwrapper() as gpg: + # TODO: inspect result, or use decorator + _gen_key(gpg) + + return self.get_key(address, private=True) + + def get_key(self, address, private=False): + """ + Get key bound to C{address} from local storage. + + :param address: The address bound to the key. + :type address: str + :param private: Look for a private key instead of a public one? + :type private: bool + + :return: The key bound to C{address}. + :rtype: OpenPGPKey + @raise KeyNotFound: If the key was not found on local storage. + """ + leap_assert(is_address(address), 'Not an user address: %s' % address) + doc = self._get_key_doc(address, private) + if doc is None: + raise errors.KeyNotFound(address) + return build_key_from_dict(OpenPGPKey, address, doc.content) + + def put_ascii_key(self, key_data): + """ + Put key contained in ascii-armored C{key_data} in local storage. + + :param key_data: The key data to be stored. + :type key_data: str + """ + leap_assert_type(key_data, str) + # TODO: add more checks for correct key data. + leap_assert(key_data is not None, 'Data does not represent a key.') + + def _put_ascii_key(gpg): + gpg.import_keys(key_data) + privkey = None + pubkey = None + + try: + privkey = gpg.list_keys(secret=True).pop() + except IndexError: + pass + pubkey = gpg.list_keys(secret=False).pop() # unitary keyring + # extract adress from first uid on key + match = re.match('.*<([\w.-]+@[\w.-]+)>.*', pubkey['uids'].pop()) + leap_assert(match is not None, 'No user address in key data.') + address = match.group(1) + if privkey is not None: + match = re.match( + '.*<([\w.-]+@[\w.-]+)>.*', privkey['uids'].pop()) + leap_assert(match is not None, 'No user address in key data.') + privaddress = match.group(1) + leap_assert( + address == privaddress, + 'Addresses in pub and priv key differ.') + leap_assert( + pubkey['fingerprint'] == privkey['fingerprint'], + 'Fingerprints for pub and priv key differ.') + # insert private key in storage + openpgp_privkey = _build_key_from_gpg( + address, privkey, + gpg.export_keys(privkey['fingerprint'], secret=True)) + self.put_key(openpgp_privkey) + # insert public key in storage + openpgp_pubkey = _build_key_from_gpg( + address, pubkey, + gpg.export_keys(pubkey['fingerprint'], secret=False)) + self.put_key(openpgp_pubkey) + + with temporary_gpgwrapper() as gpg: + # TODO: inspect result, or use decorator + _put_ascii_key(gpg) + + def put_key(self, key): + """ + Put C{key} in local storage. + + :param key: The key to be stored. + :type key: OpenPGPKey + """ + doc = self._get_key_doc(key.address, private=key.private) + if doc is None: + self._soledad.create_doc_from_json(key.get_json()) + else: + doc.set_json(key.get_json()) + self._soledad.put_doc(doc) + + def _get_key_doc(self, address, private=False): + """ + Get the document with a key (public, by default) bound to C{address}. + + If C{private} is True, looks for a private key instead of a public. + + :param address: The address bound to the key. + :type address: str + :param private: Whether to look for a private key. + :type private: bool + :return: The document with the key or None if it does not exist. + :rtype: leap.soledad.document.SoledadDocument + """ + doclist = self._soledad.get_from_index( + TAGS_ADDRESS_PRIVATE_INDEX, + KEYMANAGER_KEY_TAG, + address, + '1' if private else '0') + if len(doclist) is 0: + return None + leap_assert( + len(doclist) is 1, + 'Found more than one %s key for address!' % + 'private' if private else 'public') + return doclist.pop() + + def delete_key(self, key): + """ + Remove C{key} from storage. + + :param key: The key to be removed. + :type key: EncryptionKey + """ + leap_assert(key.__class__ is OpenPGPKey, 'Wrong key type.') + stored_key = self.get_key(key.address, private=key.private) + if stored_key is None: + raise errors.KeyNotFound(key) + if stored_key.__dict__ != key.__dict__: + raise errors.KeyAttributesDiffer(key) + doc = self._get_key_doc(key.address, key.private) + self._soledad.delete_doc(doc) diff --git a/keymanager/src/leap/keymanager/tests/__init__.py b/keymanager/src/leap/keymanager/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py new file mode 100644 index 0000000..09a6183 --- /dev/null +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -0,0 +1,676 @@ +## -*- coding: utf-8 -*- +# test_keymanager.py +# Copyright (C) 2013 LEAP +# +# 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 . + + +""" +Tests for the Key Manager. +""" + + +from mock import Mock +from leap.common.testing.basetest import BaseLeapTest +from leap.soledad import Soledad +from leap.keymanager import ( + KeyManager, + openpgp, + KeyNotFound, + NoPasswordGiven, + errors, +) +from leap.keymanager.openpgp import OpenPGPKey +from leap.keymanager.keys import ( + is_address, + build_key_from_dict, +) + + +ADDRESS = 'leap@leap.se' +ADDRESS_2 = 'anotheruser@leap.se' + + +class KeyManagerUtilTestCase(BaseLeapTest): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_is_address(self): + self.assertTrue( + is_address('user@leap.se'), + 'Incorrect address detection.') + self.assertFalse( + is_address('userleap.se'), + 'Incorrect address detection.') + self.assertFalse( + is_address('user@'), + 'Incorrect address detection.') + self.assertFalse( + is_address('@leap.se'), + 'Incorrect address detection.') + + def test_build_key_from_dict(self): + kdict = { + 'address': ADDRESS, + 'key_id': 'key_id', + 'fingerprint': 'fingerprint', + 'key_data': 'key_data', + 'private': 'private', + 'length': 'length', + 'expiry_date': 'expiry_date', + 'first_seen_at': 'first_seen_at', + 'last_audited_at': 'last_audited_at', + 'validation': 'validation', + } + key = build_key_from_dict(OpenPGPKey, ADDRESS, kdict) + self.assertEqual( + kdict['address'], key.address, + 'Wrong data in key.') + self.assertEqual( + kdict['key_id'], key.key_id, + 'Wrong data in key.') + self.assertEqual( + kdict['fingerprint'], key.fingerprint, + 'Wrong data in key.') + self.assertEqual( + kdict['key_data'], key.key_data, + 'Wrong data in key.') + self.assertEqual( + kdict['private'], key.private, + 'Wrong data in key.') + self.assertEqual( + kdict['length'], key.length, + 'Wrong data in key.') + self.assertEqual( + kdict['expiry_date'], key.expiry_date, + 'Wrong data in key.') + self.assertEqual( + kdict['first_seen_at'], key.first_seen_at, + 'Wrong data in key.') + self.assertEqual( + kdict['last_audited_at'], key.last_audited_at, + 'Wrong data in key.') + self.assertEqual( + kdict['validation'], key.validation, + 'Wrong data in key.') + + +class KeyManagerWithSoledadTestCase(BaseLeapTest): + + def setUp(self): + # mock key fetching and storing so Soledad doesn't fail when trying to + # reach the server. + Soledad._get_secrets_from_shared_db = Mock(return_value=None) + Soledad._put_secrets_in_shared_db = Mock(return_value=None) + + self._soledad = Soledad( + "leap@leap.se", + "123456", + secrets_path=self.tempdir + "/secret.gpg", + local_db_path=self.tempdir + "/soledad.u1db", + server_url='', + cert_file=None, + auth_token=None, + ) + + def tearDown(self): + km = self._key_manager() + for key in km.get_all_keys_in_local_db(): + km._wrapper_map[key.__class__].delete_key(key) + for key in km.get_all_keys_in_local_db(private=True): + km._wrapper_map[key.__class__].delete_key(key) + + def _key_manager(self, user=ADDRESS, url=''): + return KeyManager(user, url, self._soledad) + + +class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): + + def _test_openpgp_gen_key(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + self.assertRaises(KeyNotFound, pgp.get_key, 'user@leap.se') + key = pgp.gen_key('user@leap.se') + self.assertIsInstance(key, openpgp.OpenPGPKey) + self.assertEqual( + 'user@leap.se', key.address, 'Wrong address bound to key.') + self.assertEqual( + '4096', key.length, 'Wrong key length.') + + def test_openpgp_put_delete_key(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + pgp.put_ascii_key(PUBLIC_KEY) + key = pgp.get_key(ADDRESS, private=False) + pgp.delete_key(key) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + + def test_openpgp_put_ascii_key(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + pgp.put_ascii_key(PUBLIC_KEY) + key = pgp.get_key(ADDRESS, private=False) + self.assertIsInstance(key, openpgp.OpenPGPKey) + self.assertEqual( + ADDRESS, key.address, 'Wrong address bound to key.') + self.assertEqual( + '4096', key.length, 'Wrong key length.') + pgp.delete_key(key) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + + def test_get_public_key(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + pgp.put_ascii_key(PUBLIC_KEY) + self.assertRaises( + KeyNotFound, pgp.get_key, ADDRESS, private=True) + key = pgp.get_key(ADDRESS, private=False) + self.assertEqual(ADDRESS, key.address) + self.assertFalse(key.private) + self.assertEqual(KEY_FINGERPRINT, key.fingerprint) + pgp.delete_key(key) + self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) + + def test_openpgp_encrypt_decrypt_asym(self): + # encrypt + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PUBLIC_KEY) + pubkey = pgp.get_key(ADDRESS, private=False) + cyphertext = openpgp.encrypt_asym('data', pubkey) + # assert + self.assertTrue(cyphertext is not None) + self.assertTrue(cyphertext != '') + self.assertTrue(cyphertext != 'data') + self.assertTrue(openpgp.is_encrypted_asym(cyphertext)) + self.assertTrue(openpgp.is_encrypted(cyphertext)) + # decrypt + self.assertRaises( + KeyNotFound, pgp.get_key, ADDRESS, private=True) + pgp.put_ascii_key(PRIVATE_KEY) + privkey = pgp.get_key(ADDRESS, private=True) + plaintext = openpgp.decrypt_asym(cyphertext, privkey) + pgp.delete_key(pubkey) + pgp.delete_key(privkey) + self.assertRaises( + KeyNotFound, pgp.get_key, ADDRESS, private=False) + self.assertRaises( + KeyNotFound, pgp.get_key, ADDRESS, private=True) + + def test_verify_with_private_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + signed = openpgp.sign(data, privkey) + self.assertRaises( + AssertionError, + openpgp.verify, signed, privkey) + + def test_sign_with_public_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PUBLIC_KEY) + data = 'data' + pubkey = pgp.get_key(ADDRESS, private=False) + self.assertRaises( + AssertionError, + openpgp.sign, data, pubkey) + + def test_verify_with_wrong_key_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + signed = openpgp.sign(data, privkey) + pgp.put_ascii_key(PUBLIC_KEY_2) + wrongkey = pgp.get_key(ADDRESS_2) + self.assertRaises( + errors.InvalidSignature, + openpgp.verify, signed, wrongkey) + + def test_encrypt_asym_sign_with_public_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + pubkey = pgp.get_key(ADDRESS, private=False) + self.assertRaises( + AssertionError, + openpgp.encrypt_asym, data, privkey, sign=pubkey) + + def test_decrypt_asym_verify_with_private_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + pubkey = pgp.get_key(ADDRESS, private=False) + encrypted_and_signed = openpgp.encrypt_asym( + data, pubkey, sign=privkey) + self.assertRaises( + AssertionError, + openpgp.decrypt_asym, + encrypted_and_signed, privkey, verify=privkey) + + def test_decrypt_asym_verify_with_wrong_key_raises(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + pubkey = pgp.get_key(ADDRESS, private=False) + encrypted_and_signed = openpgp.encrypt_asym(data, pubkey, sign=privkey) + pgp.put_ascii_key(PUBLIC_KEY_2) + wrongkey = pgp.get_key(ADDRESS_2) + self.assertRaises( + errors.InvalidSignature, + openpgp.verify, encrypted_and_signed, wrongkey) + + def test_sign_verify(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + signed = openpgp.sign(data, privkey) + pubkey = pgp.get_key(ADDRESS, private=False) + self.assertTrue(openpgp.verify(signed, pubkey)) + + def test_encrypt_asym_sign_decrypt_verify(self): + pgp = openpgp.OpenPGPScheme(self._soledad) + pgp.put_ascii_key(PRIVATE_KEY) + pubkey = pgp.get_key(ADDRESS, private=False) + privkey = pgp.get_key(ADDRESS, private=True) + pgp.put_ascii_key(PRIVATE_KEY_2) + pubkey2 = pgp.get_key(ADDRESS_2, private=False) + privkey2 = pgp.get_key(ADDRESS_2, private=True) + data = 'data' + encrypted_and_signed = openpgp.encrypt_asym( + data, pubkey2, sign=privkey) + res = openpgp.decrypt_asym( + encrypted_and_signed, privkey2, verify=pubkey) + self.assertTrue(data, res) + + +class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): + + def test_get_all_keys_in_db(self): + km = self._key_manager() + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + # get public keys + keys = km.get_all_keys_in_local_db(False) + self.assertEqual(len(keys), 1, 'Wrong number of keys') + self.assertEqual(ADDRESS, keys[0].address) + self.assertFalse(keys[0].private) + # get private keys + keys = km.get_all_keys_in_local_db(True) + self.assertEqual(len(keys), 1, 'Wrong number of keys') + self.assertEqual(ADDRESS, keys[0].address) + self.assertTrue(keys[0].private) + + def test_get_public_key(self): + km = self._key_manager() + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + # get the key + key = km.get_key(ADDRESS, OpenPGPKey, private=False, + fetch_remote=False) + self.assertTrue(key is not None) + self.assertEqual(key.address, ADDRESS) + self.assertEqual( + key.fingerprint.lower(), KEY_FINGERPRINT.lower()) + self.assertFalse(key.private) + + def test_get_private_key(self): + km = self._key_manager() + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + # get the key + key = km.get_key(ADDRESS, OpenPGPKey, private=True, + fetch_remote=False) + self.assertTrue(key is not None) + self.assertEqual(key.address, ADDRESS) + self.assertEqual( + key.fingerprint.lower(), KEY_FINGERPRINT.lower()) + self.assertTrue(key.private) + + def test_send_key_raises_key_not_found(self): + km = self._key_manager() + self.assertRaises( + KeyNotFound, + km.send_key, OpenPGPKey) + + def test_send_key(self): + """ + Test that request is well formed when sending keys to server. + """ + km = self._key_manager() + km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY) + km._fetcher.put = Mock() + # the following data will be used on the send + km.ca_cert_path = 'capath' + km.session_id = 'sessionid' + km.uid = 'myuid' + km.api_uri = 'apiuri' + km.api_version = 'apiver' + km.send_key(OpenPGPKey) + # setup expected args + data = { + km.PUBKEY_KEY: km.get_key(km._address, OpenPGPKey).key_data, + } + url = '%s/%s/users/%s.json' % ('apiuri', 'apiver', 'myuid') + km._fetcher.put.assert_called_once_with( + url, data=data, verify='capath', + cookies={'_session_id': 'sessionid'}, + ) + + def test__fetch_keys_from_server(self): + """ + Test that the request is well formed when fetching keys from server. + """ + km = self._key_manager(url='http://nickserver.domain') + + class Response(object): + status_code = 200 + headers = {'content-type': 'application/json'} + + def json(self): + return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2} + + def raise_for_status(self): + pass + + # mock the fetcher so it returns the key for ADDRESS_2 + km._fetcher.get = Mock( + return_value=Response()) + km.ca_cert_path = 'cacertpath' + # do the fetch + km._fetch_keys_from_server(ADDRESS_2) + # and verify the call + km._fetcher.get.assert_called_once_with( + 'http://nickserver.domain', + data={'address': ADDRESS_2}, + verify='cacertpath', + ) + + def test_refresh_keys_does_not_refresh_own_key(self): + """ + Test that refreshing keys will not attempt to refresh our own key. + """ + km = self._key_manager() + # we add 2 keys but we expect it to only refresh the second one. + km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY) + km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY_2) + # mock the key fetching + km._fetch_keys_from_server = Mock(return_value=[]) + km.ca_cert_path = '' # some bogus path so the km does not complain. + # do the refreshing + km.refresh_keys() + km._fetch_keys_from_server.assert_called_once_with( + ADDRESS_2 + ) + + def test_get_key_fetches_from_server(self): + """ + Test that getting a key successfuly fetches from server. + """ + km = self._key_manager(url='http://nickserver.domain') + + class Response(object): + status_code = 200 + headers = {'content-type': 'application/json'} + + def json(self): + return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2} + + def raise_for_status(self): + pass + + # mock the fetcher so it returns the key for ADDRESS_2 + km._fetcher.get = Mock(return_value=Response()) + km.ca_cert_path = 'cacertpath' + # try to key get without fetching from server + self.assertRaises( + KeyNotFound, km.get_key, ADDRESS_2, OpenPGPKey, + fetch_remote=False + ) + # try to get key fetching from server. + key = km.get_key(ADDRESS_2, OpenPGPKey) + self.assertIsInstance(key, OpenPGPKey) + self.assertEqual(ADDRESS_2, key.address) + + +# Key material for testing + +# key 24D18DDF: public key "Leap Test Key " +KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" +PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=MuOY +-----END PGP PUBLIC KEY BLOCK----- +""" +PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=JTFu +-----END PGP PRIVATE KEY BLOCK----- +""" + +# key 7FEE575A: public key "anotheruser " +PUBLIC_KEY_2 = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mI0EUYwJXgEEAMbTKHuPJ5/Gk34l9Z06f+0WCXTDXdte1UBoDtZ1erAbudgC4MOR +gquKqoj3Hhw0/ILqJ88GcOJmKK/bEoIAuKaqlzDF7UAYpOsPZZYmtRfPC2pTCnXq +Z1vdeqLwTbUspqXflkCkFtfhGKMq5rH8GV5a3tXZkRWZhdNwhVXZagC3ABEBAAG0 +IWFub3RoZXJ1c2VyIDxhbm90aGVydXNlckBsZWFwLnNlPoi4BBMBAgAiBQJRjAle +AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRB/nfpof+5XWotuA/4tLN4E +gUr7IfLy2HkHAxzw7A4rqfMN92DIM9mZrDGaWRrOn3aVF7VU1UG7MDkHfPvp/cFw +ezoCw4s4IoHVc/pVlOkcHSyt4/Rfh248tYEJmFCJXGHpkK83VIKYJAithNccJ6Q4 +JE/o06Mtf4uh/cA1HUL4a4ceqUhtpLJULLeKo7iNBFGMCV4BBADsyQI7GR0wSAxz +VayLjuPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQt +Z/hwcLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63 +yuRe94WenT1RJd6xU1aaUff4rKizuQARAQABiJ8EGAECAAkFAlGMCV4CGwwACgkQ +f536aH/uV1rPZQQAqCzRysOlu8ez7PuiBD4SebgRqWlxa1TF1ujzfLmuPivROZ2X +Kw5aQstxgGSjoB7tac49s0huh4X8XK+BtJBfU84JS8Jc2satlfwoyZ35LH6sDZck +I+RS/3we6zpMfHs3vvp9xgca6ZupQxivGtxlJs294TpJorx+mFFqbV17AzQ= +=Thdu +-----END PGP PUBLIC KEY BLOCK----- +""" + +PRIVATE_KEY_2 = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +lQHYBFGMCV4BBADG0yh7jyefxpN+JfWdOn/tFgl0w13bXtVAaA7WdXqwG7nYAuDD +kYKriqqI9x4cNPyC6ifPBnDiZiiv2xKCALimqpcwxe1AGKTrD2WWJrUXzwtqUwp1 +6mdb3Xqi8E21LKal35ZApBbX4RijKuax/BleWt7V2ZEVmYXTcIVV2WoAtwARAQAB +AAP7BLuSAx7tOohnimEs74ks8l/L6dOcsFQZj2bqs4AoY3jFe7bV0tHr4llypb/8 +H3/DYvpf6DWnCjyUS1tTnXSW8JXtx01BUKaAufSmMNg9blKV6GGHlT/Whe9uVyks +7XHk/+9mebVMNJ/kNlqq2k+uWqJohzC8WWLRK+d1tBeqDsECANZmzltPaqUsGV5X +C3zszE3tUBgptV/mKnBtopKi+VH+t7K6fudGcG+bAcZDUoH/QVde52mIIjjIdLje +uajJuHUCAO1mqh+vPoGv4eBLV7iBo3XrunyGXiys4a39eomhxTy3YktQanjjx+ty +GltAGCs5PbWGO6/IRjjvd46wh53kzvsCAO0J97gsWhzLuFnkxFAJSPk7RRlyl7lI +1XS/x0Og6j9XHCyY1OYkfBm0to3UlCfkgirzCYlTYObCofzdKFIPDmSqHbQhYW5v +dGhlcnVzZXIgPGFub3RoZXJ1c2VyQGxlYXAuc2U+iLgEEwECACIFAlGMCV4CGwMG +CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEH+d+mh/7ldai24D/i0s3gSBSvsh +8vLYeQcDHPDsDiup8w33YMgz2ZmsMZpZGs6fdpUXtVTVQbswOQd8++n9wXB7OgLD +izgigdVz+lWU6RwdLK3j9F+Hbjy1gQmYUIlcYemQrzdUgpgkCK2E1xwnpDgkT+jT +oy1/i6H9wDUdQvhrhx6pSG2kslQst4qjnQHYBFGMCV4BBADsyQI7GR0wSAxzVayL +juPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQtZ/hw +cLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63yuRe +94WenT1RJd6xU1aaUff4rKizuQARAQABAAP9EyElqJ3dq3EErXwwT4mMnbd1SrVC +rUJrNWQZL59mm5oigS00uIyR0SvusOr+UzTtd8ysRuwHy5d/LAZsbjQStaOMBILx +77TJveOel0a1QK0YSMF2ywZMCKvquvjli4hAtWYz/EwfuzQN3t23jc5ny+GqmqD2 +3FUxLJosFUfLNmECAO9KhVmJi+L9dswIs+2Dkjd1eiRQzNOEVffvYkGYZyKxNiXF +UA5kvyZcB4iAN9sWCybE4WHZ9jd4myGB0MPDGxkCAP1RsXJbbuD6zS7BXe5gwunO +2q4q7ptdSl/sJYQuTe1KNP5d/uGsvlcFfsYjpsopasPjFBIncc/2QThMKlhoEaEB +/0mVAxpT6SrEvUbJ18z7kna24SgMPr3OnPMxPGfvNLJY/Xv/A17YfoqjmByCvsKE +JCDjopXtmbcrZyoEZbEht9mko4ifBBgBAgAJBQJRjAleAhsMAAoJEH+d+mh/7lda +z2UEAKgs0crDpbvHs+z7ogQ+Enm4EalpcWtUxdbo83y5rj4r0TmdlysOWkLLcYBk +o6Ae7WnOPbNIboeF/FyvgbSQX1POCUvCXNrGrZX8KMmd+Sx+rA2XJCPkUv98Hus6 +THx7N776fcYHGumbqUMYrxrcZSbNveE6SaK8fphRam1dewM0 +=a5gs +-----END PGP PRIVATE KEY BLOCK----- +""" +import unittest +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From b0b610c1f17433de686e7c4cb37835b3d3ea6a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 12 Jul 2013 15:04:44 -0300 Subject: Add CHANGELOG --- keymanager/CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 keymanager/CHANGELOG diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG new file mode 100644 index 0000000..174318d --- /dev/null +++ b/keymanager/CHANGELOG @@ -0,0 +1,2 @@ +0.2.0 Jul 12: + o Move keymanager to its own package -- cgit v1.2.3 From f2fefe6a5e58d81d5c2d85371c4a575603258a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 12 Jul 2013 15:05:16 -0300 Subject: Correct version to 0.2.0 --- keymanager/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymanager/setup.py b/keymanager/setup.py index 83aeddb..84780ed 100644 --- a/keymanager/setup.py +++ b/keymanager/setup.py @@ -38,7 +38,7 @@ tests_requirements = [ setup( name='leap.keymanager', - version='0.2.1', + version='0.2.0', url='https://leap.se/', license='GPLv3+', description='LEAP\'s Key Manager', -- cgit v1.2.3 From 599d17a4306d15217314edde3c43a62ce792eb32 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 2 Aug 2013 11:15:32 +0200 Subject: add gitignore --- keymanager/.gitignore | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 keymanager/.gitignore diff --git a/keymanager/.gitignore b/keymanager/.gitignore new file mode 100644 index 0000000..b0f9a13 --- /dev/null +++ b/keymanager/.gitignore @@ -0,0 +1,34 @@ +*.swp +*.swo +*.pyc +*.log +*.*~ +.* +*_rc.py +ui_*.py +!.coveragerc +!.tx +bin/ +build/ +core +debian/python-leap-client/ +dist/ +docs/_build +docs/covhtml +include/ +lib/ +local/ +share/ +src/leap/util/reqs.txt +src/leap.egg-info/ +src/leap_client.egg-info +src/leap/_branding.py +src/leap/certs/*.pem +src/*.egg-info +src/pysqlcipher +pkg/osx/dist +pkg/osx/build +MANIFEST +_trial_temp* +config/* +CHANGELOG~ -- cgit v1.2.3 From bd817552f6410f2ecd54dbbc7a1116ff5dc1659e Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 2 Aug 2013 11:16:04 +0200 Subject: Add some logs to be able to follow the key generation. --- keymanager/src/leap/keymanager/openpgp.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index d19bb2b..9d404cb 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -473,7 +473,7 @@ class OpenPGPScheme(EncryptionScheme): self.get_key(address) raise errors.KeyAlreadyExists(address) except errors.KeyNotFound: - pass + logger.debug('Key for %s not found' % (address,)) def _gen_key(gpg): params = gpg.gen_key_input( @@ -482,9 +482,18 @@ class OpenPGPScheme(EncryptionScheme): name_real=address, name_email=address, name_comment='Generated by LEAP Key Manager.') + logger.info("About to generate keys... This might take SOME time.") gpg.gen_key(params) + logger.info("Keys for %s have been successfully " + "generated." % (address,)) pubkeys = gpg.list_keys() + # assert for new key characteristics + + # XXX This exception is not properly catched by the soledad + # bootstrapping, so if we do not finish generating the keys + # we end with a blocked thread -- kali + leap_assert( len(pubkeys) is 1, # a unitary keyring! 'Keyring has wrong number of keys: %d.' % len(pubkeys)) -- cgit v1.2.3 From 2623cf55c275bda37ad7249e4cf33d448ea7f246 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 7 Aug 2013 16:29:54 +0200 Subject: Support bundled GPG and change API. - Move openpgp encrypt/decrypt/sign/verify API to inside OpenPGP class. - Add encrypt/decrypt/sign/verify API to KeyManager. - Add possibility of passing custom gpg binary to KeyManager and OpenPGPScheme. - Remove "_asym" suffix from method names. - Bump version to 0.2.1. New API is *not* backwards compatible. --- ...ture_3397-keymanager-should-support-bundled-gpg | 2 + keymanager/src/leap/keymanager/__init__.py | 97 ++++- keymanager/src/leap/keymanager/keys.py | 69 +++ keymanager/src/leap/keymanager/openpgp.py | 475 +++++++++------------ .../src/leap/keymanager/tests/test_keymanager.py | 124 ++++-- 5 files changed, 458 insertions(+), 309 deletions(-) create mode 100644 keymanager/changes/feature_3397-keymanager-should-support-bundled-gpg diff --git a/keymanager/changes/feature_3397-keymanager-should-support-bundled-gpg b/keymanager/changes/feature_3397-keymanager-should-support-bundled-gpg new file mode 100644 index 0000000..6774dd8 --- /dev/null +++ b/keymanager/changes/feature_3397-keymanager-should-support-bundled-gpg @@ -0,0 +1,2 @@ + o Add support for bundled gpg. Closes #3397. + o Refactor API to include encrypt/decrypt/sign/verify in KeyManager. diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index e1f318c..d24e08e 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -22,12 +22,13 @@ Key Manager is a Nicknym agent for LEAP client. import requests -from leap.common.check import leap_assert +from leap.common.check import leap_assert, leap_assert_type from leap.keymanager.errors import ( KeyNotFound, NoPasswordGiven, ) from leap.keymanager.keys import ( + EncryptionKey, build_key_from_dict, KEYMANAGER_KEY_TAG, TAGS_PRIVATE_INDEX, @@ -52,7 +53,8 @@ class KeyManager(object): PUBKEY_KEY = "user[public_key]" def __init__(self, address, nickserver_uri, soledad, session_id=None, - ca_cert_path=None, api_uri=None, api_version=None, uid=None): + ca_cert_path=None, api_uri=None, api_version=None, uid=None, + gpgbinary=None): """ Initialize a Key Manager for user's C{address} with provider's nickserver reachable in C{url}. @@ -73,6 +75,8 @@ class KeyManager(object): :type api_version: str :param uid: The users' UID. :type uid: str + :param gpgbinary: Name for GnuPG binary executable. + :type gpgbinary: C{str} """ self._address = address self._nickserver_uri = nickserver_uri @@ -84,7 +88,7 @@ class KeyManager(object): self.uid = uid # a dict to map key types to their handlers self._wrapper_map = { - OpenPGPKey: OpenPGPScheme(soledad), + OpenPGPKey: OpenPGPScheme(soledad, gpgbinary=gpgbinary), # other types of key will be added to this mapper. } # the following are used to perform https requests @@ -334,3 +338,90 @@ class KeyManager(object): uid = property( _get_uid, _set_uid, doc='The uid of the user.') + + # + # encrypt/decrypt and sign/verify API + # + + def encrypt(self, data, pubkey, passphrase=None, sign=None): + """ + Encrypt C{data} using public @{key} and sign with C{sign} key. + + :param data: The data to be encrypted. + :type data: str + :param pubkey: The key used to encrypt. + :type pubkey: EncryptionKey + :param sign: The key used for signing. + :type sign: EncryptionKey + + :return: The encrypted data. + :rtype: str + """ + leap_assert_type(pubkey, EncryptionKey) + leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.') + leap_assert(pubkey.private is False, 'Key is not public.') + return self._wrapper_map[pubkey.__class__].encrypt( + data, pubkey, passphrase, sign) + + def decrypt(self, data, privkey, passphrase=None, verify=None): + """ + Decrypt C{data} using private @{privkey} and verify with C{verify} key. + + :param data: The data to be decrypted. + :type data: str + :param privkey: The key used to decrypt. + :type privkey: OpenPGPKey + :param verify: The key used to verify a signature. + :type verify: OpenPGPKey + + :return: The decrypted data. + :rtype: str + + @raise InvalidSignature: Raised if unable to verify the signature with + C{verify} key. + """ + leap_assert_type(privkey, EncryptionKey) + leap_assert( + privkey.__class__ in self._wrapper_map, + 'Unknown key type.') + leap_assert(privkey.private is True, 'Key is not private.') + return self._wrapper_map[privkey.__class__].decrypt( + data, privkey, passphrase, verify) + + def sign(self, data, privkey): + """ + Sign C{data} with C{privkey}. + + :param data: The data to be signed. + :type data: str + + :param privkey: The private key to be used to sign. + :type privkey: EncryptionKey + + :return: The signed data. + :rtype: str + """ + leap_assert_type(privkey, EncryptionKey) + leap_assert( + privkey.__class__ in self._wrapper_map, + 'Unknown key type.') + leap_assert(privkey.private is True, 'Key is not private.') + return self._wrapper_map[privkey.__class__].sign(data, privkey) + + def verify(self, data, pubkey): + """ + Verify signed C{data} with C{pubkey}. + + :param data: The data to be verified. + :type data: str + + :param pubkey: The public key to be used on verification. + :type pubkey: EncryptionKey + + :return: The signed data. + :rtype: str + """ + leap_assert_type(pubkey, EncryptionKey) + leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.') + leap_assert(pubkey.private is False, 'Key is not public.') + return self._wrapper_map[pubkey.__class__].verify(data, pubkey) diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index 44bd587..1c33745 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -283,3 +283,72 @@ class EncryptionScheme(object): :type key: EncryptionKey """ pass + + @abstractmethod + def encrypt(self, data, pubkey, passphrase=None, sign=None): + """ + Encrypt C{data} using public @{pubkey} and sign with C{sign} key. + + :param data: The data to be encrypted. + :type data: str + :param pubkey: The key used to encrypt. + :type pubkey: EncryptionKey + :param sign: The key used for signing. + :type sign: EncryptionKey + + :return: The encrypted data. + :rtype: str + """ + pass + + @abstractmethod + def decrypt(self, data, privkey, passphrase=None, verify=None): + """ + Decrypt C{data} using private @{privkey} and verify with C{verify} key. + + :param data: The data to be decrypted. + :type data: str + :param privkey: The key used to decrypt. + :type privkey: OpenPGPKey + :param verify: The key used to verify a signature. + :type verify: OpenPGPKey + + :return: The decrypted data. + :rtype: str + + @raise InvalidSignature: Raised if unable to verify the signature with + C{verify} key. + """ + pass + + @abstractmethod + def sign(self, data, privkey): + """ + Sign C{data} with C{privkey}. + + :param data: The data to be signed. + :type data: str + + :param privkey: The private key to be used to sign. + :type privkey: EncryptionKey + + :return: The signed data. + :rtype: str + """ + pass + + @abstractmethod + def verify(self, data, pubkey): + """ + Verify signed C{data} with C{pubkey}. + + :param data: The data to be verified. + :type data: str + + :param pubkey: The public key to be used on verification. + :type pubkey: EncryptionKey + + :return: The signed data. + :rtype: str + """ + pass diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 9d404cb..1e78c70 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -15,15 +15,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + """ Infrastructure for using OpenPGP keys in Key Manager. """ + + import logging import os import re import shutil import tempfile + from leap.common.check import leap_assert, leap_assert_type from leap.keymanager import errors from leap.keymanager.keys import ( @@ -36,125 +40,8 @@ from leap.keymanager.keys import ( ) from leap.keymanager.gpg import GPGWrapper -logger = logging.getLogger(__name__) - - -# -# gpg wrapper and decorator -# - -def temporary_gpgwrapper(keys=None): - """ - Returns a unitary gpg wrapper that implements context manager - protocol. - - :param key_data: ASCII armored key data. - :type key_data: str - - :return: a GPGWrapper instance - :rtype: GPGWrapper - """ - # TODO do here checks on key_data - return TempGPGWrapper(keys=keys) - - -def with_temporary_gpg(fun): - """ - Decorator to add a temporary gpg wrapper as context - to gpg related functions. - - Decorated functions are expected to return a function whose only - argument is a gpgwrapper instance. - """ - def wrapped(*args, **kwargs): - """ - We extract the arguments passed to the wrapped function, - run the function and do validations. - We expect that the positional arguments are `data`, - and an optional `key`. - All the rest of arguments should be passed as named arguments - to allow for a correct unpacking. - """ - if len(args) == 2: - keys = args[1] if isinstance(args[1], OpenPGPKey) else None - else: - keys = None - - # sign/verify keys passed as arguments - sign = kwargs.get('sign', None) - if sign: - keys = [keys, sign] - - verify = kwargs.get('verify', None) - if verify: - keys = [keys, verify] - - # is the wrapped function sign or verify? - fun_name = fun.__name__ - is_sign_function = True if fun_name == "sign" else False - is_verify_function = True if fun_name == "verify" else False - - result = None - - with temporary_gpgwrapper(keys) as gpg: - result = fun(*args, **kwargs)(gpg) - - # TODO: cleanup a little bit the - # validation. maybe delegate to other - # auxiliary functions for clarity. - - ok = getattr(result, 'ok', None) - - stderr = getattr(result, 'stderr', None) - if stderr: - logger.debug("%s" % (stderr,)) - - if ok is False: - raise errors.EncryptionDecryptionFailed( - 'Failed to encrypt/decrypt in %s: %s' % ( - fun.__name__, - stderr)) - - if verify is not None: - # A verify key has been passed - if result.valid is False or \ - verify.fingerprint != result.pubkey_fingerprint: - raise errors.InvalidSignature( - 'Failed to verify signature with key %s: %s' % - (verify.key_id, stderr)) - - if is_sign_function: - # Specific validation for sign function - privkey = gpg.list_keys(secret=True).pop() - rfprint = result.fingerprint - kfprint = privkey['fingerprint'] - if result.fingerprint is None: - raise errors.SignFailed( - 'Failed to sign with key %s: %s' % - (privkey['keyid'], stderr)) - leap_assert( - result.fingerprint == kfprint, - 'Signature and private key fingerprints mismatch: ' - '%s != %s' % - (rfprint, kfprint)) - - if is_verify_function: - # Specific validation for verify function - pubkey = gpg.list_keys().pop() - valid = result.valid - rfprint = result.fingerprint - kfprint = pubkey['fingerprint'] - if valid is False or rfprint != kfprint: - raise errors.InvalidSignature( - 'Failed to verify signature ' - 'with key %s.' % pubkey['keyid']) - result = result.valid - # ok, enough checks. let's return data if available - if hasattr(result, 'data'): - result = result.data - return result - return wrapped +logger = logging.getLogger(__name__) class TempGPGWrapper(object): @@ -166,12 +53,15 @@ class TempGPGWrapper(object): one key. This function creates an empty temporary keyring and imports C{keys} if it is not None. """ - def __init__(self, keys=None): + def __init__(self, keys=None, gpgbinary=None): """ :param keys: OpenPGP key, or list of. :type keys: OpenPGPKey or list of OpenPGPKeys + :param gpgbinary: Name for GnuPG binary executable. + :type gpgbinary: C{str} """ self._gpg = None + self._gpgbinary = gpgbinary if not keys: keys = list() if not isinstance(keys, list): @@ -221,7 +111,9 @@ class TempGPGWrapper(object): listkeys = lambda: self._gpg.list_keys() listsecretkeys = lambda: self._gpg.list_keys(secret=True) - self._gpg = GPGWrapper(gnupghome=tempfile.mkdtemp()) + self._gpg = GPGWrapper( + gnupghome=tempfile.mkdtemp(), + gpgbinary=self._gpgbinary) leap_assert(len(listkeys()) is 0, 'Keyring not empty.') # import keys into the keyring: @@ -266,144 +158,6 @@ class TempGPGWrapper(object): shutil.rmtree(self._gpg.gnupghome) -# -# API functions -# - -@with_temporary_gpg -def encrypt_asym(data, key, passphrase=None, sign=None): - """ - Encrypt C{data} using public @{key} and sign with C{sign} key. - - :param data: The data to be encrypted. - :type data: str - :param pubkey: The key used to encrypt. - :type pubkey: OpenPGPKey - :param sign: The key used for signing. - :type sign: OpenPGPKey - - :return: The encrypted data. - :rtype: str - """ - leap_assert_type(key, OpenPGPKey) - leap_assert(key.private is False, 'Key is not public.') - if sign is not None: - leap_assert_type(sign, OpenPGPKey) - leap_assert(sign.private is True) - - # Here we cannot assert for correctness of sig because the sig is in - # the ciphertext. - # result.ok - (bool) indicates if the operation succeeded - # result.data - (bool) contains the result of the operation - - return lambda gpg: gpg.encrypt( - data, key.fingerprint, - sign=sign.key_id if sign else None, - passphrase=passphrase, symmetric=False) - - -@with_temporary_gpg -def decrypt_asym(data, key, passphrase=None, verify=None): - """ - Decrypt C{data} using private @{key} and verify with C{verify} key. - - :param data: The data to be decrypted. - :type data: str - :param privkey: The key used to decrypt. - :type privkey: OpenPGPKey - :param verify: The key used to verify a signature. - :type verify: OpenPGPKey - - :return: The decrypted data. - :rtype: str - - @raise InvalidSignature: Raised if unable to verify the signature with - C{verify} key. - """ - leap_assert(key.private is True, 'Key is not private.') - if verify is not None: - leap_assert_type(verify, OpenPGPKey) - leap_assert(verify.private is False) - - return lambda gpg: gpg.decrypt( - data, passphrase=passphrase) - - -@with_temporary_gpg -def is_encrypted(data): - """ - Return whether C{data} was encrypted using OpenPGP. - - :param data: The data we want to know about. - :type data: str - - :return: Whether C{data} was encrypted using this wrapper. - :rtype: bool - """ - return lambda gpg: gpg.is_encrypted(data) - - -@with_temporary_gpg -def is_encrypted_asym(data): - """ - Return whether C{data} was asymmetrically encrypted using OpenPGP. - - :param data: The data we want to know about. - :type data: str - - :return: Whether C{data} was encrypted using this wrapper. - :rtype: bool - """ - return lambda gpg: gpg.is_encrypted_asym(data) - - -@with_temporary_gpg -def sign(data, privkey): - """ - Sign C{data} with C{privkey}. - - :param data: The data to be signed. - :type data: str - - :param privkey: The private key to be used to sign. - :type privkey: OpenPGPKey - - :return: The ascii-armored signed data. - :rtype: str - """ - leap_assert_type(privkey, OpenPGPKey) - leap_assert(privkey.private is True) - - # result.fingerprint - contains the fingerprint of the key used to - # sign. - return lambda gpg: gpg.sign(data, keyid=privkey.key_id) - - -@with_temporary_gpg -def verify(data, key): - """ - Verify signed C{data} with C{pubkey}. - - :param data: The data to be verified. - :type data: str - - :param pubkey: The public key to be used on verification. - :type pubkey: OpenPGPKey - - :return: The ascii-armored signed data. - :rtype: str - """ - leap_assert_type(key, OpenPGPKey) - leap_assert(key.private is False) - - return lambda gpg: gpg.verify(data) - - -# -# Helper functions -# - - def _build_key_from_gpg(address, key, key_data): """ Build an OpenPGPKey for C{address} based on C{key} from @@ -445,17 +199,25 @@ class OpenPGPKey(EncryptionKey): class OpenPGPScheme(EncryptionScheme): """ - A wrapper for OpenPGP keys. + A wrapper for OpenPGP keys management and use (encryption, decyption, + signing and verification). """ - def __init__(self, soledad): + def __init__(self, soledad, gpgbinary=None): """ Initialize the OpenPGP wrapper. :param soledad: A Soledad instance for key storage. :type soledad: leap.soledad.Soledad + :param gpgbinary: Name for GnuPG binary executable. + :type gpgbinary: C{str} """ EncryptionScheme.__init__(self, soledad) + self._gpgbinary = gpgbinary + + # + # Keys management + # def gen_key(self, address): """ @@ -475,7 +237,8 @@ class OpenPGPScheme(EncryptionScheme): except errors.KeyNotFound: logger.debug('Key for %s not found' % (address,)) - def _gen_key(gpg): + with self._temporary_gpgwrapper() as gpg: + # TODO: inspect result, or use decorator params = gpg.gen_key_input( key_type='RSA', key_length=4096, @@ -512,10 +275,6 @@ class OpenPGPScheme(EncryptionScheme): gpg.export_keys(key['fingerprint'], secret=secret)) self.put_key(openpgp_key) - with temporary_gpgwrapper() as gpg: - # TODO: inspect result, or use decorator - _gen_key(gpg) - return self.get_key(address, private=True) def get_key(self, address, private=False): @@ -548,7 +307,8 @@ class OpenPGPScheme(EncryptionScheme): # TODO: add more checks for correct key data. leap_assert(key_data is not None, 'Data does not represent a key.') - def _put_ascii_key(gpg): + with self._temporary_gpgwrapper() as gpg: + # TODO: inspect result, or use decorator gpg.import_keys(key_data) privkey = None pubkey = None @@ -584,10 +344,6 @@ class OpenPGPScheme(EncryptionScheme): gpg.export_keys(pubkey['fingerprint'], secret=False)) self.put_key(openpgp_pubkey) - with temporary_gpgwrapper() as gpg: - # TODO: inspect result, or use decorator - _put_ascii_key(gpg) - def put_key(self, key): """ Put C{key} in local storage. @@ -643,3 +399,182 @@ class OpenPGPScheme(EncryptionScheme): raise errors.KeyAttributesDiffer(key) doc = self._get_key_doc(key.address, key.private) self._soledad.delete_doc(doc) + + # + # Data encryption, decryption, signing and verifying + # + + def _temporary_gpgwrapper(self, keys=None): + """ + Returns a unitary gpg wrapper that implements context manager + protocol. + + :param key_data: ASCII armored key data. + :type key_data: str + :param gpgbinary: Name for GnuPG binary executable. + :type gpgbinary: C{str} + + :return: a GPGWrapper instance + :rtype: GPGWrapper + """ + # TODO do here checks on key_data + return TempGPGWrapper( + keys=keys, gpgbinary=self._gpgbinary) + + @staticmethod + def _assert_gpg_result_ok(result): + """ + Check if GPG result is 'ok' and log stderr outputs. + :param result: The GPG results + :type result: + """ + stderr = getattr(result, 'stderr', None) + if stderr: + logger.debug("%s" % (stderr,)) + if getattr(result, 'ok', None) is not True: + raise errors.EncryptionDecryptionFailed( + 'Failed to encrypt/decrypt in %s: %s' % ( + this.encrypt, + stderr)) + + def encrypt(self, data, pubkey, passphrase=None, sign=None): + """ + Encrypt C{data} using public @{pubkey} and sign with C{sign} key. + + :param data: The data to be encrypted. + :type data: str + :param pubkey: The key used to encrypt. + :type pubkey: OpenPGPKey + :param sign: The key used for signing. + :type sign: OpenPGPKey + + :return: The encrypted data. + :rtype: str + """ + leap_assert_type(pubkey, OpenPGPKey) + leap_assert(pubkey.private is False, 'Key is not public.') + keys = [pubkey] + if sign is not None: + leap_assert_type(sign, OpenPGPKey) + leap_assert(sign.private is True) + keys.append(sign) + with self._temporary_gpgwrapper(keys) as gpg: + result = gpg.encrypt( + data, pubkey.fingerprint, + sign=sign.key_id if sign else None, + passphrase=passphrase, symmetric=False) + # Here we cannot assert for correctness of sig because the sig is + # in the ciphertext. + # result.ok - (bool) indicates if the operation succeeded + # result.data - (bool) contains the result of the operation + self._assert_gpg_result_ok(result) + return result.data + + def decrypt(self, data, privkey, passphrase=None, verify=None): + """ + Decrypt C{data} using private @{privkey} and verify with C{verify} key. + + :param data: The data to be decrypted. + :type data: str + :param privkey: The key used to decrypt. + :type privkey: OpenPGPKey + :param verify: The key used to verify a signature. + :type verify: OpenPGPKey + + :return: The decrypted data. + :rtype: str + + @raise InvalidSignature: Raised if unable to verify the signature with + C{verify} key. + """ + leap_assert(privkey.private is True, 'Key is not private.') + keys = [privkey] + if verify is not None: + leap_assert_type(verify, OpenPGPKey) + leap_assert(verify.private is False) + keys.append(verify) + with self._temporary_gpgwrapper(keys) as gpg: + result = gpg.decrypt(data, passphrase=passphrase) + self._assert_gpg_result_ok(result) + # verify signature + if (verify is not None): + if result.valid is False or \ + verify.fingerprint != result.pubkey_fingerprint: + raise errors.InvalidSignature( + 'Failed to verify signature with key %s: %s' % + (verify.key_id, stderr)) + return result.data + + def is_encrypted(self, data): + """ + Return whether C{data} was asymmetrically encrypted using OpenPGP. + + :param data: The data we want to know about. + :type data: str + + :return: Whether C{data} was encrypted using this wrapper. + :rtype: bool + """ + with self._temporary_gpgwrapper() as gpg: + return gpg.is_encrypted_asym(data) + + def sign(self, data, privkey): + """ + Sign C{data} with C{privkey}. + + :param data: The data to be signed. + :type data: str + + :param privkey: The private key to be used to sign. + :type privkey: OpenPGPKey + + :return: The ascii-armored signed data. + :rtype: str + """ + leap_assert_type(privkey, OpenPGPKey) + leap_assert(privkey.private is True) + + # result.fingerprint - contains the fingerprint of the key used to + # sign. + with self._temporary_gpgwrapper(privkey) as gpg: + result = gpg.sign(data, keyid=privkey.key_id) + rfprint = privkey.fingerprint + privkey = gpg.list_keys(secret=True).pop() + kfprint = privkey['fingerprint'] + if result.fingerprint is None: + raise errors.SignFailed( + 'Failed to sign with key %s: %s' % + (privkey['keyid'], stderr)) + leap_assert( + result.fingerprint == kfprint, + 'Signature and private key fingerprints mismatch: ' + '%s != %s' % (rfprint, kfprint)) + return result.data + + def verify(self, data, pubkey): + """ + Verify signed C{data} with C{pubkey}. + + :param data: The data to be verified. + :type data: str + + :param pubkey: The public key to be used on verification. + :type pubkey: OpenPGPKey + + :return: The ascii-armored signed data. + :rtype: str + """ + leap_assert_type(pubkey, OpenPGPKey) + leap_assert(pubkey.private is False) + with self._temporary_gpgwrapper(pubkey) as gpg: + result = gpg.verify(data) + gpgpubkey = gpg.list_keys().pop() + valid = result.valid + rfprint = result.fingerprint + kfprint = gpgpubkey['fingerprint'] + # raise in case sig is invalid + if valid is False or rfprint != kfprint: + raise errors.InvalidSignature( + 'Failed to verify signature ' + 'with key %s.' % gpgpubkey['keyid']) + return True diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 09a6183..a474e32 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -40,6 +40,7 @@ from leap.keymanager.keys import ( ADDRESS = 'leap@leap.se' ADDRESS_2 = 'anotheruser@leap.se' +GPG_BINARY_PATH = '/usr/bin/gpg' class KeyManagerUtilTestCase(BaseLeapTest): @@ -136,7 +137,8 @@ class KeyManagerWithSoledadTestCase(BaseLeapTest): km._wrapper_map[key.__class__].delete_key(key) def _key_manager(self, user=ADDRESS, url=''): - return KeyManager(user, url, self._soledad) + return KeyManager(user, url, self._soledad, + gpgbinary=GPG_BINARY_PATH) class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): @@ -152,7 +154,8 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): '4096', key.length, 'Wrong key length.') def test_openpgp_put_delete_key(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) pgp.put_ascii_key(PUBLIC_KEY) key = pgp.get_key(ADDRESS, private=False) @@ -160,7 +163,8 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) def test_openpgp_put_ascii_key(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) pgp.put_ascii_key(PUBLIC_KEY) key = pgp.get_key(ADDRESS, private=False) @@ -173,7 +177,8 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) def test_get_public_key(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) pgp.put_ascii_key(PUBLIC_KEY) self.assertRaises( @@ -185,24 +190,25 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp.delete_key(key) self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - def test_openpgp_encrypt_decrypt_asym(self): + def test_openpgp_encrypt_decrypt(self): # encrypt - pgp = openpgp.OpenPGPScheme(self._soledad) + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PUBLIC_KEY) pubkey = pgp.get_key(ADDRESS, private=False) - cyphertext = openpgp.encrypt_asym('data', pubkey) + cyphertext = pgp.encrypt('data', pubkey) # assert self.assertTrue(cyphertext is not None) self.assertTrue(cyphertext != '') self.assertTrue(cyphertext != 'data') - self.assertTrue(openpgp.is_encrypted_asym(cyphertext)) - self.assertTrue(openpgp.is_encrypted(cyphertext)) + self.assertTrue(pgp.is_encrypted(cyphertext)) + self.assertTrue(pgp.is_encrypted(cyphertext)) # decrypt self.assertRaises( KeyNotFound, pgp.get_key, ADDRESS, private=True) pgp.put_ascii_key(PRIVATE_KEY) privkey = pgp.get_key(ADDRESS, private=True) - plaintext = openpgp.decrypt_asym(cyphertext, privkey) + plaintext = pgp.decrypt(cyphertext, privkey) pgp.delete_key(pubkey) pgp.delete_key(privkey) self.assertRaises( @@ -211,83 +217,91 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): KeyNotFound, pgp.get_key, ADDRESS, private=True) def test_verify_with_private_raises(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PRIVATE_KEY) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) - signed = openpgp.sign(data, privkey) + signed = pgp.sign(data, privkey) self.assertRaises( AssertionError, - openpgp.verify, signed, privkey) + pgp.verify, signed, privkey) def test_sign_with_public_raises(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PUBLIC_KEY) data = 'data' pubkey = pgp.get_key(ADDRESS, private=False) self.assertRaises( AssertionError, - openpgp.sign, data, pubkey) + pgp.sign, data, pubkey) def test_verify_with_wrong_key_raises(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PRIVATE_KEY) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) - signed = openpgp.sign(data, privkey) + signed = pgp.sign(data, privkey) pgp.put_ascii_key(PUBLIC_KEY_2) wrongkey = pgp.get_key(ADDRESS_2) self.assertRaises( errors.InvalidSignature, - openpgp.verify, signed, wrongkey) + pgp.verify, signed, wrongkey) - def test_encrypt_asym_sign_with_public_raises(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + def test_encrypt_sign_with_public_raises(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PRIVATE_KEY) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) pubkey = pgp.get_key(ADDRESS, private=False) self.assertRaises( AssertionError, - openpgp.encrypt_asym, data, privkey, sign=pubkey) + pgp.encrypt, data, privkey, sign=pubkey) - def test_decrypt_asym_verify_with_private_raises(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + def test_decrypt_verify_with_private_raises(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PRIVATE_KEY) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) pubkey = pgp.get_key(ADDRESS, private=False) - encrypted_and_signed = openpgp.encrypt_asym( + encrypted_and_signed = pgp.encrypt( data, pubkey, sign=privkey) self.assertRaises( AssertionError, - openpgp.decrypt_asym, + pgp.decrypt, encrypted_and_signed, privkey, verify=privkey) - def test_decrypt_asym_verify_with_wrong_key_raises(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + def test_decrypt_verify_with_wrong_key_raises(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PRIVATE_KEY) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) pubkey = pgp.get_key(ADDRESS, private=False) - encrypted_and_signed = openpgp.encrypt_asym(data, pubkey, sign=privkey) + encrypted_and_signed = pgp.encrypt(data, pubkey, sign=privkey) pgp.put_ascii_key(PUBLIC_KEY_2) wrongkey = pgp.get_key(ADDRESS_2) self.assertRaises( errors.InvalidSignature, - openpgp.verify, encrypted_and_signed, wrongkey) + pgp.verify, encrypted_and_signed, wrongkey) def test_sign_verify(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PRIVATE_KEY) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) - signed = openpgp.sign(data, privkey) + signed = pgp.sign(data, privkey) pubkey = pgp.get_key(ADDRESS, private=False) - self.assertTrue(openpgp.verify(signed, pubkey)) + self.assertTrue(pgp.verify(signed, pubkey)) - def test_encrypt_asym_sign_decrypt_verify(self): - pgp = openpgp.OpenPGPScheme(self._soledad) + def test_encrypt_sign_decrypt_verify(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PRIVATE_KEY) pubkey = pgp.get_key(ADDRESS, private=False) privkey = pgp.get_key(ADDRESS, private=True) @@ -295,9 +309,9 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pubkey2 = pgp.get_key(ADDRESS_2, private=False) privkey2 = pgp.get_key(ADDRESS_2, private=True) data = 'data' - encrypted_and_signed = openpgp.encrypt_asym( + encrypted_and_signed = pgp.encrypt( data, pubkey2, sign=privkey) - res = openpgp.decrypt_asym( + res = pgp.decrypt( encrypted_and_signed, privkey2, verify=pubkey) self.assertTrue(data, res) @@ -448,6 +462,44 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): self.assertEqual(ADDRESS_2, key.address) +class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): + + RAW_DATA = 'data' + + def test_keymanager_openpgp_encrypt_decrypt(self): + km = self._key_manager() + # put raw private key + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + # get public key + pubkey = km.get_key( + ADDRESS, OpenPGPKey, private=False, fetch_remote=False) + # encrypt + encdata = km.encrypt(self.RAW_DATA, pubkey) + self.assertNotEqual(self.RAW_DATA, encdata) + # get private key + privkey = km.get_key( + ADDRESS, OpenPGPKey, private=True, fetch_remote=False) + # decrypt + rawdata = km.decrypt(encdata, privkey) + self.assertEqual(self.RAW_DATA, rawdata) + + def test_keymanager_openpgp_sign_verify(self): + km = self._key_manager() + # put raw private keys + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + # get private key for signing + privkey = km.get_key( + ADDRESS, OpenPGPKey, private=True, fetch_remote=False) + # encrypt + signdata = km.sign(self.RAW_DATA, privkey) + self.assertNotEqual(self.RAW_DATA, signdata) + # get public key for verifying + pubkey = km.get_key( + ADDRESS, OpenPGPKey, private=False, fetch_remote=False) + # decrypt + self.assertTrue(km.verify(signdata, pubkey)) + + # Key material for testing # key 24D18DDF: public key "Leap Test Key " -- cgit v1.2.3 From 66e7044bef038b5bf07fa4d6bddd50edc80e6c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 8 Aug 2013 11:36:00 -0300 Subject: Remove options from the constructor call for gnupg.GPG --- keymanager/changes/bug_supported_options | 2 ++ keymanager/src/leap/keymanager/gpg.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 keymanager/changes/bug_supported_options diff --git a/keymanager/changes/bug_supported_options b/keymanager/changes/bug_supported_options new file mode 100644 index 0000000..e7a70d3 --- /dev/null +++ b/keymanager/changes/bug_supported_options @@ -0,0 +1,2 @@ + o Options parameter in gnupg.GPG isn't supported by all versions, so + removing it for the time being. \ No newline at end of file diff --git a/keymanager/src/leap/keymanager/gpg.py b/keymanager/src/leap/keymanager/gpg.py index 15c1d9f..b81b218 100644 --- a/keymanager/src/leap/keymanager/gpg.py +++ b/keymanager/src/leap/keymanager/gpg.py @@ -110,9 +110,10 @@ class GPGWrapper(gnupg.GPG): @raise: RuntimeError with explanation message if there is a problem invoking gpg. """ + # XXX: options isn't always supported, so removing for the time being gnupg.GPG.__init__(self, gnupghome=gnupghome, gpgbinary=gpgbinary, verbose=verbose, use_agent=use_agent, - keyring=keyring, options=options) + keyring=keyring)#, options=options) self.result_map['list-packets'] = ListPackets def find_key_by_email(self, email, secret=False): -- cgit v1.2.3 From 4bdec73025a8d923782ee99f1096bbd1bbd93c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 8 Aug 2013 13:44:50 -0300 Subject: Do not fail if nickserver request fails --- keymanager/changes/bug_dont_fail_if_nickserver_does | 1 + keymanager/src/leap/keymanager/__init__.py | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 keymanager/changes/bug_dont_fail_if_nickserver_does diff --git a/keymanager/changes/bug_dont_fail_if_nickserver_does b/keymanager/changes/bug_dont_fail_if_nickserver_does new file mode 100644 index 0000000..6e44d93 --- /dev/null +++ b/keymanager/changes/bug_dont_fail_if_nickserver_does @@ -0,0 +1 @@ + o If a nickserver request fails in any way, notify and continue. \ No newline at end of file diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index d24e08e..e6122ff 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -20,6 +20,7 @@ Key Manager is a Nicknym agent for LEAP client. """ +import logging import requests from leap.common.check import leap_assert, leap_assert_type @@ -38,6 +39,8 @@ from leap.keymanager.openpgp import ( OpenPGPScheme, ) +logger = logging.getLogger(__name__) + # # The Key Manager @@ -170,12 +173,18 @@ class KeyManager(object): @raise KeyNotFound: If the key was not found on nickserver. """ # request keys from the nickserver - server_keys = self._get( - self._nickserver_uri, {'address': address}).json() - # insert keys in local database - if self.OPENPGP_KEY in server_keys: - self._wrapper_map[OpenPGPKey].put_ascii_key( - server_keys['openpgp']) + res = None + try: + res = self._get(self._nickserver_uri, {'address': address}) + server_keys = res.json() + # insert keys in local database + if self.OPENPGP_KEY in server_keys: + self._wrapper_map[OpenPGPKey].put_ascii_key( + server_keys['openpgp']) + except Exception as e: + logger.warning("Error retrieving the keys: %r" % (e,)) + if res: + logger.warning("%s" % (res.content,)) # # key management -- cgit v1.2.3 From 3e9aa2257a292bd7ce26896dd824c57d640058e4 Mon Sep 17 00:00:00 2001 From: drebs Date: Fri, 9 Aug 2013 00:34:00 +0200 Subject: Fix error in encryption failure logging method. --- keymanager/src/leap/keymanager/openpgp.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 1e78c70..aa04ed0 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -433,9 +433,7 @@ class OpenPGPScheme(EncryptionScheme): logger.debug("%s" % (stderr,)) if getattr(result, 'ok', None) is not True: raise errors.EncryptionDecryptionFailed( - 'Failed to encrypt/decrypt in %s: %s' % ( - this.encrypt, - stderr)) + 'Failed to encrypt/decrypt: %s' % stderr) def encrypt(self, data, pubkey, passphrase=None, sign=None): """ -- cgit v1.2.3 From 24d3fb8cd92387f9c19f9b7ad24b50e775754672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 9 Aug 2013 14:26:02 -0300 Subject: Fold in changes --- keymanager/CHANGELOG | 7 +++++++ keymanager/changes/bug_dont_fail_if_nickserver_does | 1 - keymanager/changes/bug_supported_options | 2 -- .../changes/feature_3397-keymanager-should-support-bundled-gpg | 2 -- 4 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 keymanager/changes/bug_dont_fail_if_nickserver_does delete mode 100644 keymanager/changes/bug_supported_options delete mode 100644 keymanager/changes/feature_3397-keymanager-should-support-bundled-gpg diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index 174318d..b7dc4a2 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,2 +1,9 @@ +0.3.0 Aug 9: + o If a nickserver request fails in any way, notify and continue. + o Options parameter in gnupg.GPG isn't supported by all versions, so + removing it for the time being. + o Add support for bundled gpg. Closes #3397. + o Refactor API to include encrypt/decrypt/sign/verify in KeyManager. + 0.2.0 Jul 12: o Move keymanager to its own package diff --git a/keymanager/changes/bug_dont_fail_if_nickserver_does b/keymanager/changes/bug_dont_fail_if_nickserver_does deleted file mode 100644 index 6e44d93..0000000 --- a/keymanager/changes/bug_dont_fail_if_nickserver_does +++ /dev/null @@ -1 +0,0 @@ - o If a nickserver request fails in any way, notify and continue. \ No newline at end of file diff --git a/keymanager/changes/bug_supported_options b/keymanager/changes/bug_supported_options deleted file mode 100644 index e7a70d3..0000000 --- a/keymanager/changes/bug_supported_options +++ /dev/null @@ -1,2 +0,0 @@ - o Options parameter in gnupg.GPG isn't supported by all versions, so - removing it for the time being. \ No newline at end of file diff --git a/keymanager/changes/feature_3397-keymanager-should-support-bundled-gpg b/keymanager/changes/feature_3397-keymanager-should-support-bundled-gpg deleted file mode 100644 index 6774dd8..0000000 --- a/keymanager/changes/feature_3397-keymanager-should-support-bundled-gpg +++ /dev/null @@ -1,2 +0,0 @@ - o Add support for bundled gpg. Closes #3397. - o Refactor API to include encrypt/decrypt/sign/verify in KeyManager. -- cgit v1.2.3 From cc76711ff17e7f45eca4a28a601b792fd15dc64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 9 Aug 2013 14:26:52 -0300 Subject: Bump version to 0.3.0 --- keymanager/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymanager/setup.py b/keymanager/setup.py index 84780ed..88fe35f 100644 --- a/keymanager/setup.py +++ b/keymanager/setup.py @@ -38,7 +38,7 @@ tests_requirements = [ setup( name='leap.keymanager', - version='0.2.0', + version='0.3.0', url='https://leap.se/', license='GPLv3+', description='LEAP\'s Key Manager', -- cgit v1.2.3 From 79dae47bf729a04c7b633234ca634949a6172720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 16 Aug 2013 15:29:43 -0300 Subject: Signal events from keymanager --- keymanager/changes/bug_signal_events | 2 ++ keymanager/src/leap/keymanager/__init__.py | 31 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 keymanager/changes/bug_signal_events diff --git a/keymanager/changes/bug_signal_events b/keymanager/changes/bug_signal_events new file mode 100644 index 0000000..6149c56 --- /dev/null +++ b/keymanager/changes/bug_signal_events @@ -0,0 +1,2 @@ + o Signal different key related events, like key generation, key + upload. \ No newline at end of file diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index e6122ff..20e44fd 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -24,10 +24,11 @@ import logging import requests from leap.common.check import leap_assert, leap_assert_type -from leap.keymanager.errors import ( - KeyNotFound, - NoPasswordGiven, -) +from leap.common.events import signal +from leap.common.events import events_pb2 as proto + +from leap.keymanager.errors import KeyNotFound + from leap.keymanager.keys import ( EncryptionKey, build_key_from_dict, @@ -222,6 +223,7 @@ class KeyManager(object): self._api_version, self._uid) self._put(uri, data) + signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address) def get_key(self, address, ktype, private=False, fetch_remote=True): """ @@ -246,15 +248,26 @@ class KeyManager(object): ktype in self._wrapper_map, 'Unkown key type: %s.' % str(ktype)) try: + signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address) # return key if it exists in local database - return self._wrapper_map[ktype].get_key(address, private=private) + key = self._wrapper_map[ktype].get_key(address, private=private) + signal(proto.KEYMANAGER_KEY_FOUND, address) + + return key except KeyNotFound: + signal(proto.KEYMANAGER_KEY_NOT_FOUND, address) + # we will only try to fetch a key from nickserver if fetch_remote # is True and the key is not private. if fetch_remote is False or private is True: raise + + signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address) self._fetch_keys_from_server(address) - return self._wrapper_map[ktype].get_key(address, private=False) + key = self._wrapper_map[ktype].get_key(address, private=False) + signal(proto.KEYMANAGER_KEY_FOUND, address) + + return key def get_all_keys_in_local_db(self, private=False): """ @@ -296,7 +309,11 @@ class KeyManager(object): :return: The generated key. :rtype: EncryptionKey """ - return self._wrapper_map[ktype].gen_key(self._address) + signal(proto.KEYMANAGER_STARTED_KEY_GENERATION, self._address) + key = self._wrapper_map[ktype].gen_key(self._address) + signal(proto.KEYMANAGER_FINISHED_KEY_GENERATION, self._address) + + return key # # Setters/getters -- cgit v1.2.3 From 82191866aa668e7ad294087c0365decf54588698 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 19 Aug 2013 08:57:42 -0300 Subject: Update to new soledad package scheme. --- .../changes/feature_3487-split-soledad-into-common-client-and-server | 2 ++ keymanager/setup.py | 2 +- keymanager/src/leap/keymanager/tests/test_keymanager.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 keymanager/changes/feature_3487-split-soledad-into-common-client-and-server diff --git a/keymanager/changes/feature_3487-split-soledad-into-common-client-and-server b/keymanager/changes/feature_3487-split-soledad-into-common-client-and-server new file mode 100644 index 0000000..4698323 --- /dev/null +++ b/keymanager/changes/feature_3487-split-soledad-into-common-client-and-server @@ -0,0 +1,2 @@ + o Update to new soledad package scheme (common, client and server). Closes + #3487. diff --git a/keymanager/setup.py b/keymanager/setup.py index 88fe35f..c42ad2b 100644 --- a/keymanager/setup.py +++ b/keymanager/setup.py @@ -32,7 +32,7 @@ install_requirements = [ tests_requirements = [ 'mock', - 'leap.soledad', + 'leap.soledad.client', ] diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index a474e32..5f232fe 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -23,7 +23,7 @@ Tests for the Key Manager. from mock import Mock from leap.common.testing.basetest import BaseLeapTest -from leap.soledad import Soledad +from leap.soledad.client import Soledad from leap.keymanager import ( KeyManager, openpgp, -- cgit v1.2.3 From 8783a07eea46e7d35c11ab1f6805fc5c715464c9 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 23 Aug 2013 12:17:20 +0200 Subject: Some packaging improvements * versioneer all the things * add parse_requirements --- keymanager/.gitattributes | 1 + keymanager/.gitignore | 1 + keymanager/changes/feature_improve-packaging | 1 + keymanager/pkg/__init__.py | 0 keymanager/pkg/requirements-testing.pip | 2 + keymanager/pkg/requirements.pip | 4 + keymanager/pkg/utils.py | 84 ++++ keymanager/setup.py | 49 +- keymanager/src/leap/keymanager/__init__.py | 4 + keymanager/src/leap/keymanager/_version.py | 203 ++++++++ keymanager/versioneer.py | 669 +++++++++++++++++++++++++++ 11 files changed, 999 insertions(+), 19 deletions(-) create mode 100644 keymanager/.gitattributes create mode 100644 keymanager/changes/feature_improve-packaging create mode 100644 keymanager/pkg/__init__.py create mode 100644 keymanager/pkg/requirements-testing.pip create mode 100644 keymanager/pkg/requirements.pip create mode 100644 keymanager/pkg/utils.py create mode 100644 keymanager/src/leap/keymanager/_version.py create mode 100644 keymanager/versioneer.py diff --git a/keymanager/.gitattributes b/keymanager/.gitattributes new file mode 100644 index 0000000..9cde827 --- /dev/null +++ b/keymanager/.gitattributes @@ -0,0 +1 @@ +src/leap/keymanager/_version.py export-subst diff --git a/keymanager/.gitignore b/keymanager/.gitignore index b0f9a13..18bfcd6 100644 --- a/keymanager/.gitignore +++ b/keymanager/.gitignore @@ -8,6 +8,7 @@ ui_*.py !.coveragerc !.tx +!.gitattributes bin/ build/ core diff --git a/keymanager/changes/feature_improve-packaging b/keymanager/changes/feature_improve-packaging new file mode 100644 index 0000000..5da993c --- /dev/null +++ b/keymanager/changes/feature_improve-packaging @@ -0,0 +1 @@ + o Packaging improvements: add versioneer and parse_requirements diff --git a/keymanager/pkg/__init__.py b/keymanager/pkg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keymanager/pkg/requirements-testing.pip b/keymanager/pkg/requirements-testing.pip new file mode 100644 index 0000000..f41fdba --- /dev/null +++ b/keymanager/pkg/requirements-testing.pip @@ -0,0 +1,2 @@ +mock +leap.soledad.client diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip new file mode 100644 index 0000000..9e16e95 --- /dev/null +++ b/keymanager/pkg/requirements.pip @@ -0,0 +1,4 @@ +leap.common>=0.3.0 +simplejson +requests +python-gnupg diff --git a/keymanager/pkg/utils.py b/keymanager/pkg/utils.py new file mode 100644 index 0000000..deace14 --- /dev/null +++ b/keymanager/pkg/utils.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# utils.py +# Copyright (C) 2013 LEAP +# +# 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 . + +""" +Utils to help in the setup process +""" + +import os +import re +import sys + + +def get_reqs_from_files(reqfiles): + """ + Returns the contents of the top requirement file listed as a + string list with the lines + + @param reqfiles: requirement files to parse + @type reqfiles: list of str + """ + for reqfile in reqfiles: + if os.path.isfile(reqfile): + return open(reqfile, 'r').read().split('\n') + + +def parse_requirements(reqfiles=['requirements.txt', + 'requirements.pip', + 'pkg/requirements.pip']): + """ + Parses the requirement files provided. + + Checks the value of LEAP_VENV_SKIP_PYSIDE to see if it should + return PySide as a dep or not. Don't set, or set to 0 if you want + to install it through pip. + + @param reqfiles: requirement files to parse + @type reqfiles: list of str + """ + + requirements = [] + skip_pyside = os.getenv("LEAP_VENV_SKIP_PYSIDE", "0") != "0" + for line in get_reqs_from_files(reqfiles): + # -e git://foo.bar/baz/master#egg=foobar + if re.match(r'\s*-e\s+', line): + pass + # do not try to do anything with externals on vcs + #requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', + #line)) + # http://foo.bar/baz/foobar/zipball/master#egg=foobar + elif re.match(r'\s*https?:', line): + requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', + line)) + # -f lines are for index locations, and don't get used here + elif re.match(r'\s*-f\s+', line): + pass + + # argparse is part of the standard library starting with 2.7 + # adding it to the requirements list screws distro installs + elif line == 'argparse' and sys.version_info >= (2, 7): + pass + elif line == 'PySide' and skip_pyside: + pass + # do not include comments + elif line.lstrip().startswith('#'): + pass + else: + if line != '': + requirements.append(line) + + return requirements diff --git a/keymanager/setup.py b/keymanager/setup.py index c42ad2b..1e48931 100644 --- a/keymanager/setup.py +++ b/keymanager/setup.py @@ -14,31 +14,40 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +""" +setup file for leap.keymanager +""" +from setuptools import setup +from setuptools import find_packages +import versioneer +versioneer.versionfile_source = 'src/leap/keymanager/_version.py' +versioneer.versionfile_build = 'leap/keymanager/_version.py' +versioneer.tag_prefix = '' # tags are like 1.2.0 +versioneer.parentdir_prefix = 'leap.keymanager-' -from setuptools import ( - setup, - find_packages -) - - -install_requirements = [ - 'leap.common', - 'simplejson', - 'requests', - 'python-gnupg', -] - +from pkg import utils -tests_requirements = [ - 'mock', - 'leap.soledad.client', +trove_classifiers = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Topic :: Communications :: Email', + 'Topic :: Internet', + 'Topic :: Security :: Cryptography', + 'Topic :: Software Development :: Libraries', ] +# XXX add ref to docs setup( name='leap.keymanager', - version='0.3.0', + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), url='https://leap.se/', license='GPLv3+', description='LEAP\'s Key Manager', @@ -49,10 +58,12 @@ setup( "point-to-point encryption between parties communicating through " "LEAP infrastructure." ), + classifiers=trove_classifiers, namespace_packages=["leap"], packages=find_packages('src', exclude=['leap.keymanager.tests']), package_dir={'': 'src'}, test_suite='leap.keymanager.tests', - install_requires=install_requirements, - tests_require=tests_requirements, + install_requires=utils.parse_requirements(), + tests_require=utils.parse_requirements( + reqfiles=['pkg/requirements-testing.pip']), ) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 20e44fd..05cbcca 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -451,3 +451,7 @@ class KeyManager(object): leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.') leap_assert(pubkey.private is False, 'Key is not public.') return self._wrapper_map[pubkey.__class__].verify(data, pubkey) + +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions diff --git a/keymanager/src/leap/keymanager/_version.py b/keymanager/src/leap/keymanager/_version.py new file mode 100644 index 0000000..3a514e1 --- /dev/null +++ b/keymanager/src/leap/keymanager/_version.py @@ -0,0 +1,203 @@ + +IN_LONG_VERSION_PY = True +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.7+ (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "$Format:%d$" +git_full = "$Format:%H$" + + +import subprocess +import sys + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + f = open(versionfile_source,"r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs-tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "" +parentdir_prefix = "leap.keymanager-" +versionfile_source = "src/leap/keymanager/_version.py" + +def get_versions(default={"version": "unknown", "full": ""}, verbose=False): + variables = { "refnames": git_refnames, "full": git_full } + ver = versions_from_expanded_variables(variables, tag_prefix, verbose) + if not ver: + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if not ver: + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose) + if not ver: + ver = default + return ver + diff --git a/keymanager/versioneer.py b/keymanager/versioneer.py new file mode 100644 index 0000000..34e4807 --- /dev/null +++ b/keymanager/versioneer.py @@ -0,0 +1,669 @@ +#! /usr/bin/python + +"""versioneer.py + +(like a rocketeer, but for versions) + +* https://github.com/warner/python-versioneer +* Brian Warner +* License: Public Domain +* Version: 0.7+ + +This file helps distutils-based projects manage their version number by just +creating version-control tags. + +For developers who work from a VCS-generated tree (e.g. 'git clone' etc), +each 'setup.py version', 'setup.py build', 'setup.py sdist' will compute a +version number by asking your version-control tool about the current +checkout. The version number will be written into a generated _version.py +file of your choosing, where it can be included by your __init__.py + +For users who work from a VCS-generated tarball (e.g. 'git archive'), it will +compute a version number by looking at the name of the directory created when +te tarball is unpacked. This conventionally includes both the name of the +project and a version number. + +For users who work from a tarball built by 'setup.py sdist', it will get a +version number from a previously-generated _version.py file. + +As a result, loading code directly from the source tree will not result in a +real version. If you want real versions from VCS trees (where you frequently +update from the upstream repository, or do new development), you will need to +do a 'setup.py version' after each update, and load code from the build/ +directory. + +You need to provide this code with a few configuration values: + + versionfile_source: + A project-relative pathname into which the generated version strings + should be written. This is usually a _version.py next to your project's + main __init__.py file. If your project uses src/myproject/__init__.py, + this should be 'src/myproject/_version.py'. This file should be checked + in to your VCS as usual: the copy created below by 'setup.py + update_files' will include code that parses expanded VCS keywords in + generated tarballs. The 'build' and 'sdist' commands will replace it with + a copy that has just the calculated version string. + + versionfile_build: + Like versionfile_source, but relative to the build directory instead of + the source directory. These will differ when your setup.py uses + 'package_dir='. If you have package_dir={'myproject': 'src/myproject'}, + then you will probably have versionfile_build='myproject/_version.py' and + versionfile_source='src/myproject/_version.py'. + + tag_prefix: a string, like 'PROJECTNAME-', which appears at the start of all + VCS tags. If your tags look like 'myproject-1.2.0', then you + should use tag_prefix='myproject-'. If you use unprefixed tags + like '1.2.0', this should be an empty string. + + parentdir_prefix: a string, frequently the same as tag_prefix, which + appears at the start of all unpacked tarball filenames. If + your tarball unpacks into 'myproject-1.2.0', this should + be 'myproject-'. + +To use it: + + 1: include this file in the top level of your project + 2: make the following changes to the top of your setup.py: + import versioneer + versioneer.versionfile_source = 'src/myproject/_version.py' + versioneer.versionfile_build = 'myproject/_version.py' + versioneer.tag_prefix = '' # tags are like 1.2.0 + versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0' + 3: add the following arguments to the setup() call in your setup.py: + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), + 4: run 'setup.py update_files', which will create _version.py, and will + modify your __init__.py to define __version__ (by calling a function + from _version.py) + 5: modify your MANIFEST.in to include versioneer.py + 6: add both versioneer.py and the generated _version.py to your VCS +""" + +import os, sys, re +from distutils.core import Command +from distutils.command.sdist import sdist as _sdist +from distutils.command.build import build as _build + +versionfile_source = None +versionfile_build = None +tag_prefix = None +parentdir_prefix = None + +VCS = "git" +IN_LONG_VERSION_PY = False + + +LONG_VERSION_PY = ''' +IN_LONG_VERSION_PY = True +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.7+ (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" +git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" + + +import subprocess +import sys + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %%s" %% args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %%s (error)" %% args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + f = open(versionfile_source,"r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %%d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%%s', no digits" %% ",".join(refs-tags)) + if verbose: + print("likely tags: %%s" %% ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %%s" %% r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %%s" %% root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%%s' doesn't start with prefix '%%s'" %% (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%%s', but '%%s' doesn't start with prefix '%%s'" %% + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "%(TAG_PREFIX)s" +parentdir_prefix = "%(PARENTDIR_PREFIX)s" +versionfile_source = "%(VERSIONFILE_SOURCE)s" + +def get_versions(default={"version": "unknown", "full": ""}, verbose=False): + variables = { "refnames": git_refnames, "full": git_full } + ver = versions_from_expanded_variables(variables, tag_prefix, verbose) + if not ver: + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if not ver: + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose) + if not ver: + ver = default + return ver + +''' + + +import subprocess +import sys + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + f = open(versionfile_source,"r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs-tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +import sys + +def do_vcs_install(versionfile_source, ipy): + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + run_command([GIT, "add", "versioneer.py"]) + run_command([GIT, "add", versionfile_source]) + run_command([GIT, "add", ipy]) + present = False + try: + f = open(".gitattributes", "r") + for line in f.readlines(): + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + f.close() + except EnvironmentError: + pass + if not present: + f = open(".gitattributes", "a+") + f.write("%s export-subst\n" % versionfile_source) + f.close() + run_command([GIT, "add", ".gitattributes"]) + + +SHORT_VERSION_PY = """ +# This file was generated by 'versioneer.py' (0.7+) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +version_version = '%(version)s' +version_full = '%(full)s' +def get_versions(default={}, verbose=False): + return {'version': version_version, 'full': version_full} + +""" + +DEFAULT = {"version": "unknown", "full": "unknown"} + +def versions_from_file(filename): + versions = {} + try: + f = open(filename) + except EnvironmentError: + return versions + for line in f.readlines(): + mo = re.match("version_version = '([^']+)'", line) + if mo: + versions["version"] = mo.group(1) + mo = re.match("version_full = '([^']+)'", line) + if mo: + versions["full"] = mo.group(1) + f.close() + return versions + +def write_to_version_file(filename, versions): + f = open(filename, "w") + f.write(SHORT_VERSION_PY % versions) + f.close() + print("set %s to '%s'" % (filename, versions["version"])) + + +def get_best_versions(versionfile, tag_prefix, parentdir_prefix, + default=DEFAULT, verbose=False): + # returns dict with two keys: 'version' and 'full' + # + # extract version from first of _version.py, 'git describe', parentdir. + # This is meant to work for developers using a source checkout, for users + # of a tarball created by 'setup.py sdist', and for users of a + # tarball/zipball created by 'git archive' or github's download-from-tag + # feature. + + variables = get_expanded_variables(versionfile_source) + if variables: + ver = versions_from_expanded_variables(variables, tag_prefix) + if ver: + if verbose: print("got version from expanded variable %s" % ver) + return ver + + ver = versions_from_file(versionfile) + if ver: + if verbose: print("got version from file %s %s" % (versionfile, ver)) + return ver + + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if ver: + if verbose: print("got version from git %s" % ver) + return ver + + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, verbose) + if ver: + if verbose: print("got version from parentdir %s" % ver) + return ver + + if verbose: print("got version from default %s" % ver) + return default + +def get_versions(default=DEFAULT, verbose=False): + assert versionfile_source is not None, "please set versioneer.versionfile_source" + assert tag_prefix is not None, "please set versioneer.tag_prefix" + assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix" + return get_best_versions(versionfile_source, tag_prefix, parentdir_prefix, + default=default, verbose=verbose) +def get_version(verbose=False): + return get_versions(verbose=verbose)["version"] + +class cmd_version(Command): + description = "report generated version string" + user_options = [] + boolean_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + ver = get_version(verbose=True) + print("Version is currently: %s" % ver) + + +class cmd_build(_build): + def run(self): + versions = get_versions(verbose=True) + _build.run(self) + # now locate _version.py in the new build/ directory and replace it + # with an updated value + target_versionfile = os.path.join(self.build_lib, versionfile_build) + print("UPDATING %s" % target_versionfile) + os.unlink(target_versionfile) + f = open(target_versionfile, "w") + f.write(SHORT_VERSION_PY % versions) + f.close() + +class cmd_sdist(_sdist): + def run(self): + versions = get_versions(verbose=True) + self._versioneer_generated_versions = versions + # unless we update this, the command will keep using the old version + self.distribution.metadata.version = versions["version"] + return _sdist.run(self) + + def make_release_tree(self, base_dir, files): + _sdist.make_release_tree(self, base_dir, files) + # now locate _version.py in the new base_dir directory (remembering + # that it may be a hardlink) and replace it with an updated value + target_versionfile = os.path.join(base_dir, versionfile_source) + print("UPDATING %s" % target_versionfile) + os.unlink(target_versionfile) + f = open(target_versionfile, "w") + f.write(SHORT_VERSION_PY % self._versioneer_generated_versions) + f.close() + +INIT_PY_SNIPPET = """ +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions +""" + +class cmd_update_files(Command): + description = "modify __init__.py and create _version.py" + user_options = [] + boolean_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + ipy = os.path.join(os.path.dirname(versionfile_source), "__init__.py") + print(" creating %s" % versionfile_source) + f = open(versionfile_source, "w") + f.write(LONG_VERSION_PY % {"DOLLAR": "$", + "TAG_PREFIX": tag_prefix, + "PARENTDIR_PREFIX": parentdir_prefix, + "VERSIONFILE_SOURCE": versionfile_source, + }) + f.close() + try: + old = open(ipy, "r").read() + except EnvironmentError: + old = "" + if INIT_PY_SNIPPET not in old: + print(" appending to %s" % ipy) + f = open(ipy, "a") + f.write(INIT_PY_SNIPPET) + f.close() + else: + print(" %s unmodified" % ipy) + do_vcs_install(versionfile_source, ipy) + +def get_cmdclass(): + return {'version': cmd_version, + 'update_files': cmd_update_files, + 'build': cmd_build, + 'sdist': cmd_sdist, + } -- cgit v1.2.3 From b849933c7c4b9d8af4ad5e5aac30ea0f344f3f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 23 Aug 2013 11:36:28 -0300 Subject: Fold in changes --- keymanager/CHANGELOG | 7 +++++++ keymanager/changes/bug_signal_events | 2 -- .../feature_3487-split-soledad-into-common-client-and-server | 2 -- keymanager/changes/feature_improve-packaging | 1 - 4 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 keymanager/changes/bug_signal_events delete mode 100644 keymanager/changes/feature_3487-split-soledad-into-common-client-and-server delete mode 100644 keymanager/changes/feature_improve-packaging diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index b7dc4a2..d2e45aa 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,10 @@ +0.3.1 Aug 23: + o Signal different key related events, like key generation, key + upload. + o Update to new soledad package scheme (common, client and + server). Closes #3487. + o Packaging improvements: add versioneer and parse_requirements. + 0.3.0 Aug 9: o If a nickserver request fails in any way, notify and continue. o Options parameter in gnupg.GPG isn't supported by all versions, so diff --git a/keymanager/changes/bug_signal_events b/keymanager/changes/bug_signal_events deleted file mode 100644 index 6149c56..0000000 --- a/keymanager/changes/bug_signal_events +++ /dev/null @@ -1,2 +0,0 @@ - o Signal different key related events, like key generation, key - upload. \ No newline at end of file diff --git a/keymanager/changes/feature_3487-split-soledad-into-common-client-and-server b/keymanager/changes/feature_3487-split-soledad-into-common-client-and-server deleted file mode 100644 index 4698323..0000000 --- a/keymanager/changes/feature_3487-split-soledad-into-common-client-and-server +++ /dev/null @@ -1,2 +0,0 @@ - o Update to new soledad package scheme (common, client and server). Closes - #3487. diff --git a/keymanager/changes/feature_improve-packaging b/keymanager/changes/feature_improve-packaging deleted file mode 100644 index 5da993c..0000000 --- a/keymanager/changes/feature_improve-packaging +++ /dev/null @@ -1 +0,0 @@ - o Packaging improvements: add versioneer and parse_requirements -- cgit v1.2.3 From c388b844dfff5c714b833abefa07585cabcda957 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 26 Aug 2013 15:35:17 +0200 Subject: add crate icon and link --- keymanager/README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keymanager/README.rst b/keymanager/README.rst index f542214..8aedfb4 100644 --- a/keymanager/README.rst +++ b/keymanager/README.rst @@ -1,6 +1,8 @@ LEAP's Key Manager ================== +.. image:: https://pypip.in/v/leap.keymanager/badge.png + :target: https://crate.io/packages/leap.keymanager -The Key Manager is a Nicknym agent for LEAP client: +The Key Manager is a Nicknym agent for the LEAP project: https://leap.se/pt/docs/design/nicknym -- cgit v1.2.3 From 824997f2423a47f6fefff92ac676cb64e0f3e420 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 27 Aug 2013 19:55:57 +0200 Subject: add manifest --- keymanager/MANIFEST.in | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 keymanager/MANIFEST.in diff --git a/keymanager/MANIFEST.in b/keymanager/MANIFEST.in new file mode 100644 index 0000000..7f6148e --- /dev/null +++ b/keymanager/MANIFEST.in @@ -0,0 +1,4 @@ +include pkg/* +include versioneer.py +include LICENSE +include CHANGELOG -- cgit v1.2.3 From ce2f89eb8dfbeeb173cb6c76f23241d0a213e21a Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 27 Aug 2013 19:56:16 +0200 Subject: remove unneeded portions from license --- keymanager/LICENSE | 79 ------------------------------------------------------ 1 file changed, 79 deletions(-) diff --git a/keymanager/LICENSE b/keymanager/LICENSE index 5f7cfba..bc08fe2 100644 --- a/keymanager/LICENSE +++ b/keymanager/LICENSE @@ -617,82 +617,3 @@ 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 -. - --------------------------------------------------------------------------------- - -Leap-client also uses third party icons: - ---- -data/images/Arrow-Up-32.png -data/images/Arrow-Down-32.png - -Author: Liam McKay -License: GNU General Public License - http://en.wikipedia.org/wiki/GNU_General_Public_License -WebSite: http://wefunction.com/ -IconPackage: WooFunction icon pack - http://www.iconspedia.com/pack/woofunction-icons-4136/ ---- -data/images/Globe.png - -Author: Everaldo Coelho -License: LGPL - http://www.gnu.org/licenses/lgpl.html -WebSite: http://www.everaldo.com/ ---- -data/images/oxygen-icons/ - -License: LGPL - http://www.gnu.org/licenses/lgpl.html -Website: http://www.oxygen-icons.org/ -- cgit v1.2.3 From 03ecafdc5834e64cc9f1f1493ddfaa82210ace47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Thu, 29 Aug 2013 17:41:43 -0300 Subject: Do not raise on not 2XX error codes Also accept unicode ascii keys --- keymanager/changes/bug_minor_issues | 4 ++++ keymanager/src/leap/keymanager/__init__.py | 15 ++++++++++----- keymanager/src/leap/keymanager/openpgp.py | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 keymanager/changes/bug_minor_issues diff --git a/keymanager/changes/bug_minor_issues b/keymanager/changes/bug_minor_issues new file mode 100644 index 0000000..91a69ed --- /dev/null +++ b/keymanager/changes/bug_minor_issues @@ -0,0 +1,4 @@ + o Do not raise exception when a GET request doesn't return 2XX + code. Nickserver uses error codes for more verbosity in the + result. + o Accept unicode ascii keys along with str. \ No newline at end of file diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 05cbcca..2f39ad9 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -127,11 +127,16 @@ class KeyManager(object): self._ca_cert_path is not None, 'We need the CA certificate path!') res = self._fetcher.get(uri, data=data, verify=self._ca_cert_path) - # assert that the response is valid - res.raise_for_status() - leap_assert( - res.headers['content-type'].startswith('application/json'), - 'Content-type is not JSON.') + # Nickserver now returns 404 for key not found and 500 for + # other cases (like key too small), so we are skipping this + # check for the time being + # res.raise_for_status() + + # Responses are now text/plain, although it's json anyway, but + # this will fail when it shouldn't + # leap_assert( + # res.headers['content-type'].startswith('application/json'), + # 'Content-type is not JSON.') return res def _put(self, uri, data=None): diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index aa04ed0..7946db8 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -301,9 +301,9 @@ class OpenPGPScheme(EncryptionScheme): Put key contained in ascii-armored C{key_data} in local storage. :param key_data: The key data to be stored. - :type key_data: str + :type key_data: str or unicode """ - leap_assert_type(key_data, str) + leap_assert_type(key_data, (str, unicode)) # TODO: add more checks for correct key data. leap_assert(key_data is not None, 'Data does not represent a key.') -- cgit v1.2.3 From c32a1efec962b309dbe9e2904a991016ad6e575e Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 2 Sep 2013 01:04:51 +0200 Subject: assert nonnull soledad --- keymanager/src/leap/keymanager/keys.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index 1c33745..b8e88d4 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -219,6 +219,8 @@ class EncryptionScheme(object): """ Initialize the database indexes. """ + leap_assert(self._soledad is not None, + "Cannot init indexes with null soledad") # Ask the database for currently existing indexes. db_indexes = dict(self._soledad.list_indexes()) # Loop through the indexes we expect to find. -- cgit v1.2.3 From 04323f8e7e49d44c653914afd514bdc3455d23d2 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 6 Sep 2013 15:27:18 -0300 Subject: Fold in changes. --- keymanager/CHANGELOG | 6 ++++++ keymanager/changes/bug_minor_issues | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 keymanager/changes/bug_minor_issues diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index d2e45aa..9cd53b3 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,9 @@ +0.3.2 Sep 6: + o Do not raise exception when a GET request doesn't return 2XX + code. Nickserver uses error codes for more verbosity in the + result. + o Accept unicode ascii keys along with str. + 0.3.1 Aug 23: o Signal different key related events, like key generation, key upload. diff --git a/keymanager/changes/bug_minor_issues b/keymanager/changes/bug_minor_issues deleted file mode 100644 index 91a69ed..0000000 --- a/keymanager/changes/bug_minor_issues +++ /dev/null @@ -1,4 +0,0 @@ - o Do not raise exception when a GET request doesn't return 2XX - code. Nickserver uses error codes for more verbosity in the - result. - o Accept unicode ascii keys along with str. \ No newline at end of file -- cgit v1.2.3 From fea2f9b9c3f2427184e957d860aca7d96e116bbf Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 30 Sep 2013 15:55:01 -0300 Subject: Update to use gnupg 1.2.2 module. --- .../changes/feature_2342-use-new-gnupg-module | 1 + keymanager/pkg/requirements.pip | 2 +- keymanager/src/leap/keymanager/gpg.py | 398 --------------------- keymanager/src/leap/keymanager/openpgp.py | 65 ++-- .../src/leap/keymanager/tests/test_keymanager.py | 1 - 5 files changed, 35 insertions(+), 432 deletions(-) create mode 100644 keymanager/changes/feature_2342-use-new-gnupg-module delete mode 100644 keymanager/src/leap/keymanager/gpg.py diff --git a/keymanager/changes/feature_2342-use-new-gnupg-module b/keymanager/changes/feature_2342-use-new-gnupg-module new file mode 100644 index 0000000..b83e8ec --- /dev/null +++ b/keymanager/changes/feature_2342-use-new-gnupg-module @@ -0,0 +1 @@ + o Update code to use gnupg 1.2.2 python module. Closes #2342. diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index 9e16e95..5ebd803 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -1,4 +1,4 @@ leap.common>=0.3.0 simplejson requests -python-gnupg +gnupg diff --git a/keymanager/src/leap/keymanager/gpg.py b/keymanager/src/leap/keymanager/gpg.py deleted file mode 100644 index b81b218..0000000 --- a/keymanager/src/leap/keymanager/gpg.py +++ /dev/null @@ -1,398 +0,0 @@ -# -*- coding: utf-8 -*- -# gpgwrapper.py -# Copyright (C) 2013 LEAP -# -# 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 . - - -""" -A GPG wrapper used to handle OpenPGP keys. - -This is a temporary class that will be superseded by the a revised version of -python-gnupg. -""" - - -import os -import gnupg -import re -from gnupg import ( - logger, - _is_sequence, - _make_binary_stream, -) - - -class ListPackets(): - """ - Handle status messages for --list-packets. - """ - - def __init__(self, gpg): - """ - Initialize the packet listing handling class. - - :param gpg: GPG object instance. - :type gpg: gnupg.GPG - """ - self.gpg = gpg - self.nodata = None - self.key = None - self.need_passphrase = None - self.need_passphrase_sym = None - self.userid_hint = None - - def handle_status(self, key, value): - """ - Handle one line of the --list-packets status message. - - :param key: The status message key. - :type key: str - :param value: The status message value. - :type value: str - """ - # TODO: write tests for handle_status - if key == 'NODATA': - self.nodata = True - if key == 'ENC_TO': - # This will only capture keys in our keyring. In the future we - # may want to include multiple unknown keys in this list. - self.key, _, _ = value.split() - if key == 'NEED_PASSPHRASE': - self.need_passphrase = True - if key == 'NEED_PASSPHRASE_SYM': - self.need_passphrase_sym = True - if key == 'USERID_HINT': - self.userid_hint = value.strip().split() - - -class GPGWrapper(gnupg.GPG): - """ - This is a temporary class for handling GPG requests, and should be - replaced by a more general class used throughout the project. - """ - - GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg" - GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS - - def __init__(self, gpgbinary=GNUPG_BINARY, gnupghome=GNUPG_HOME, - verbose=False, use_agent=False, keyring=None, options=None): - """ - Initialize a GnuPG process wrapper. - - :param gpgbinary: Name for GnuPG binary executable. - :type gpgbinary: C{str} - :param gpghome: Full pathname to directory containing the public and - private keyrings. - :type gpghome: C{str} - :param keyring: Name of alternative keyring file to use. If specified, - the default keyring is not used. - :param verbose: Should some verbose info be output? - :type verbose: bool - :param use_agent: Should pass `--use-agent` to GPG binary? - :type use_agent: bool - :param keyring: Path for the keyring to use. - :type keyring: str - @options: A list of additional options to pass to the GPG binary. - :type options: list - - @raise: RuntimeError with explanation message if there is a problem - invoking gpg. - """ - # XXX: options isn't always supported, so removing for the time being - gnupg.GPG.__init__(self, gnupghome=gnupghome, gpgbinary=gpgbinary, - verbose=verbose, use_agent=use_agent, - keyring=keyring)#, options=options) - self.result_map['list-packets'] = ListPackets - - def find_key_by_email(self, email, secret=False): - """ - Find user's key based on their email. - - :param email: Email address of key being searched for. - :type email: str - :param secret: Should we search for a secret key? - :type secret: bool - - :return: The fingerprint of the found key. - :rtype: str - """ - for key in self.list_keys(secret=secret): - for uid in key['uids']: - if re.search(email, uid): - return key - raise LookupError("GnuPG public key for email %s not found!" % email) - - def find_key_by_subkey(self, subkey, secret=False): - """ - Find user's key based on a subkey fingerprint. - - :param email: Subkey fingerprint of the key being searched for. - :type email: str - :param secret: Should we search for a secret key? - :type secret: bool - - :return: The fingerprint of the found key. - :rtype: str - """ - for key in self.list_keys(secret=secret): - for sub in key['subkeys']: - if sub[0] == subkey: - return key - raise LookupError( - "GnuPG public key for subkey %s not found!" % subkey) - - def find_key_by_keyid(self, keyid, secret=False): - """ - Find user's key based on the key ID. - - :param email: The key ID of the key being searched for. - :type email: str - :param secret: Should we search for a secret key? - :type secret: bool - - :return: The fingerprint of the found key. - :rtype: str - """ - for key in self.list_keys(secret=secret): - if keyid == key['keyid']: - return key - raise LookupError( - "GnuPG public key for keyid %s not found!" % keyid) - - def find_key_by_fingerprint(self, fingerprint, secret=False): - """ - Find user's key based on the key fingerprint. - - :param email: The fingerprint of the key being searched for. - :type email: str - :param secret: Should we search for a secret key? - :type secret: bool - - :return: The fingerprint of the found key. - :rtype: str - """ - for key in self.list_keys(secret=secret): - if fingerprint == key['fingerprint']: - return key - raise LookupError( - "GnuPG public key for fingerprint %s not found!" % fingerprint) - - def encrypt(self, data, recipient, sign=None, always_trust=True, - passphrase=None, symmetric=False): - """ - Encrypt data using GPG. - - :param data: The data to be encrypted. - :type data: str - :param recipient: The address of the public key to be used. - :type recipient: str - :param sign: Should the encrypted content be signed? - :type sign: bool - :param always_trust: Skip key validation and assume that used keys - are always fully trusted? - :type always_trust: bool - :param passphrase: The passphrase to be used if symmetric encryption - is desired. - :type passphrase: str - :param symmetric: Should we encrypt to a password? - :type symmetric: bool - - :return: An object with encrypted result in the `data` field. - :rtype: gnupg.Crypt - """ - # TODO: devise a way so we don't need to "always trust". - return gnupg.GPG.encrypt(self, data, recipient, sign=sign, - always_trust=always_trust, - passphrase=passphrase, - symmetric=symmetric, - cipher_algo='AES256') - - def decrypt(self, data, always_trust=True, passphrase=None): - """ - Decrypt data using GPG. - - :param data: The data to be decrypted. - :type data: str - :param always_trust: Skip key validation and assume that used keys - are always fully trusted? - :type always_trust: bool - :param passphrase: The passphrase to be used if symmetric encryption - is desired. - :type passphrase: str - - :return: An object with decrypted result in the `data` field. - :rtype: gnupg.Crypt - """ - # TODO: devise a way so we don't need to "always trust". - return gnupg.GPG.decrypt(self, data, always_trust=always_trust, - passphrase=passphrase) - - def send_keys(self, keyserver, *keyids): - """ - Send keys to a keyserver - - :param keyserver: The keyserver to send the keys to. - :type keyserver: str - :param keyids: The key ids to send. - :type keyids: list - - :return: A list of keys sent to server. - :rtype: gnupg.ListKeys - """ - # TODO: write tests for this. - # TODO: write a SendKeys class to handle status for this. - result = self.result_map['list'](self) - gnupg.logger.debug('send_keys: %r', keyids) - data = gnupg._make_binary_stream("", self.encoding) - args = ['--keyserver', keyserver, '--send-keys'] - args.extend(keyids) - self._handle_io(args, data, result, binary=True) - gnupg.logger.debug('send_keys result: %r', result.__dict__) - data.close() - return result - - def encrypt_file(self, file, recipients, sign=None, - always_trust=False, passphrase=None, - armor=True, output=None, symmetric=False, - cipher_algo=None): - """ - Encrypt the message read from the file-like object 'file'. - - :param file: The file to be encrypted. - :type data: file - :param recipient: The address of the public key to be used. - :type recipient: str - :param sign: Should the encrypted content be signed? - :type sign: bool - :param always_trust: Skip key validation and assume that used keys - are always fully trusted? - :type always_trust: bool - :param passphrase: The passphrase to be used if symmetric encryption - is desired. - :type passphrase: str - :param armor: Create ASCII armored output? - :type armor: bool - :param output: Path of file to write results in. - :type output: str - :param symmetric: Should we encrypt to a password? - :type symmetric: bool - :param cipher_algo: Algorithm to use. - :type cipher_algo: str - - :return: An object with encrypted result in the `data` field. - :rtype: gnupg.Crypt - """ - args = ['--encrypt'] - if symmetric: - args = ['--symmetric'] - if cipher_algo: - args.append('--cipher-algo %s' % cipher_algo) - else: - args = ['--encrypt'] - if not _is_sequence(recipients): - recipients = (recipients,) - for recipient in recipients: - args.append('--recipient "%s"' % recipient) - if armor: # create ascii-armored output - set to False for binary - args.append('--armor') - if output: # write the output to a file with the specified name - if os.path.exists(output): - os.remove(output) # to avoid overwrite confirmation message - args.append('--output "%s"' % output) - if sign: - args.append('--sign --default-key "%s"' % sign) - if always_trust: - args.append("--always-trust") - result = self.result_map['crypt'](self) - self._handle_io(args, file, result, passphrase=passphrase, binary=True) - logger.debug('encrypt result: %r', result.data) - return result - - def list_packets(self, data): - """ - List the sequence of packets. - - :param data: The data to extract packets from. - :type data: str - - :return: An object with packet info. - :rtype ListPackets - """ - args = ["--list-packets"] - result = self.result_map['list-packets'](self) - self._handle_io( - args, - _make_binary_stream(data, self.encoding), - result, - ) - return result - - def encrypted_to(self, data): - """ - Return the key to which data is encrypted to. - - :param data: The data to be examined. - :type data: str - - :return: The fingerprint of the key to which data is encrypted to. - :rtype: str - """ - # TODO: make this support multiple keys. - result = self.list_packets(data) - if not result.key: - raise LookupError( - "Content is not encrypted to a GnuPG key!") - try: - return self.find_key_by_keyid(result.key) - except: - return self.find_key_by_subkey(result.key) - - def is_encrypted_sym(self, data): - """ - Say whether some chunk of data is encrypted to a symmetric key. - - :param data: The data to be examined. - :type data: str - - :return: Whether data is encrypted to a symmetric key. - :rtype: bool - """ - result = self.list_packets(data) - return bool(result.need_passphrase_sym) - - def is_encrypted_asym(self, data): - """ - Say whether some chunk of data is encrypted to a private key. - - :param data: The data to be examined. - :type data: str - - :return: Whether data is encrypted to a private key. - :rtype: bool - """ - result = self.list_packets(data) - return bool(result.key) - - def is_encrypted(self, data): - """ - Say whether some chunk of data is encrypted to a key. - - :param data: The data to be examined. - :type data: str - - :return: Whether data is encrypted to a key. - :rtype: bool - """ - return self.is_encrypted_asym(data) or self.is_encrypted_sym(data) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 7946db8..9d8d89a 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -27,6 +27,8 @@ import re import shutil import tempfile +from gnupg import GPG +from gnupg.gnupg import GPGUtilities from leap.common.check import leap_assert, leap_assert_type from leap.keymanager import errors @@ -38,7 +40,6 @@ from leap.keymanager.keys import ( KEYMANAGER_KEY_TAG, TAGS_ADDRESS_PRIVATE_INDEX, ) -from leap.keymanager.gpg import GPGWrapper logger = logging.getLogger(__name__) @@ -46,15 +47,15 @@ logger = logging.getLogger(__name__) class TempGPGWrapper(object): """ - A context manager returning a temporary GPG wrapper keyring, which - contains exactly zero or one pubkeys, and zero or one privkeys. - - Temporary unitary keyrings allow the to use GPG's facilities for exactly - one key. This function creates an empty temporary keyring and imports - C{keys} if it is not None. + A context manager that wraps a temporary GPG keyring which only contains + the keys given at object creation. """ + def __init__(self, keys=None, gpgbinary=None): """ + Create an empty temporary keyring and import any given C{keys} into + it. + :param keys: OpenPGP key, or list of. :type keys: OpenPGPKey or list of OpenPGPKeys :param gpgbinary: Name for GnuPG binary executable. @@ -67,14 +68,15 @@ class TempGPGWrapper(object): if not isinstance(keys, list): keys = [keys] self._keys = keys - for key in filter(None, keys): + for key in keys: leap_assert_type(key, OpenPGPKey) def __enter__(self): """ - Calls the unitary gpgwrapper initializer + Build and return a GPG keyring containing the keys given on + object creation. - :return: A GPG wrapper with a unitary keyring. + :return: A GPG instance containing the keys given on object creation. :rtype: gnupg.GPG """ self._build_keyring() @@ -82,19 +84,16 @@ class TempGPGWrapper(object): def __exit__(self, exc_type, exc_value, traceback): """ - Ensures the gpgwrapper is properly destroyed. + Ensure the gpg is properly destroyed. """ # TODO handle exceptions and log here self._destroy_keyring() def _build_keyring(self): """ - Create an empty GPG keyring and import C{keys} into it. - - :param keys: List of keys to add to the keyring. - :type keys: list of OpenPGPKey + Create a GPG keyring containing the keys given on object creation. - :return: A GPG wrapper with a unitary keyring. + :return: A GPG instance containing the keys given on object creation. :rtype: gnupg.GPG """ privkeys = [key for key in self._keys if key and key.private is True] @@ -111,14 +110,13 @@ class TempGPGWrapper(object): listkeys = lambda: self._gpg.list_keys() listsecretkeys = lambda: self._gpg.list_keys(secret=True) - self._gpg = GPGWrapper( - gnupghome=tempfile.mkdtemp(), - gpgbinary=self._gpgbinary) + self._gpg = GPG(binary=self._gpgbinary, + homedir=tempfile.mkdtemp()) leap_assert(len(listkeys()) is 0, 'Keyring not empty.') # import keys into the keyring: # concatenating ascii-armored keys, which is correctly - # understood by the GPGWrapper. + # understood by GPG. self._gpg.import_keys("".join( [x.key_data for x in publkeys + privkeys])) @@ -135,7 +133,7 @@ class TempGPGWrapper(object): def _destroy_keyring(self): """ - Securely erase a unitary keyring. + Securely erase the keyring. """ # TODO: implement some kind of wiping of data or a more # secure way that @@ -153,9 +151,9 @@ class TempGPGWrapper(object): raise finally: - leap_assert(self._gpg.gnupghome != os.path.expanduser('~/.gnupg'), + leap_assert(self._gpg.homedir != os.path.expanduser('~/.gnupg'), "watch out! Tried to remove default gnupg home!") - shutil.rmtree(self._gpg.gnupghome) + shutil.rmtree(self._gpg.homedir) def _build_key_from_gpg(address, key, key_data): @@ -406,16 +404,16 @@ class OpenPGPScheme(EncryptionScheme): def _temporary_gpgwrapper(self, keys=None): """ - Returns a unitary gpg wrapper that implements context manager - protocol. + Return a gpg wrapper that implements the context manager protocol and + contains C{keys}. :param key_data: ASCII armored key data. :type key_data: str :param gpgbinary: Name for GnuPG binary executable. :type gpgbinary: C{str} - :return: a GPGWrapper instance - :rtype: GPGWrapper + :return: a TempGPGWrapper instance + :rtype: TempGPGWrapper """ # TODO do here checks on key_data return TempGPGWrapper( @@ -459,8 +457,9 @@ class OpenPGPScheme(EncryptionScheme): with self._temporary_gpgwrapper(keys) as gpg: result = gpg.encrypt( data, pubkey.fingerprint, - sign=sign.key_id if sign else None, - passphrase=passphrase, symmetric=False) + default_key=sign.key_id if sign else None, + passphrase=passphrase, symmetric=False, + cipher_algo='AES256') # Here we cannot assert for correctness of sig because the sig is # in the ciphertext. # result.ok - (bool) indicates if the operation succeeded @@ -492,7 +491,8 @@ class OpenPGPScheme(EncryptionScheme): leap_assert(verify.private is False) keys.append(verify) with self._temporary_gpgwrapper(keys) as gpg: - result = gpg.decrypt(data, passphrase=passphrase) + result = gpg.decrypt( + data, passphrase=passphrase, always_trust=True) self._assert_gpg_result_ok(result) # verify signature if (verify is not None): @@ -514,7 +514,8 @@ class OpenPGPScheme(EncryptionScheme): :rtype: bool """ with self._temporary_gpgwrapper() as gpg: - return gpg.is_encrypted_asym(data) + gpgutil = GPGUtilities(gpg) + return gpgutil.is_encrypted_asym(data) def sign(self, data, privkey): """ @@ -535,7 +536,7 @@ class OpenPGPScheme(EncryptionScheme): # result.fingerprint - contains the fingerprint of the key used to # sign. with self._temporary_gpgwrapper(privkey) as gpg: - result = gpg.sign(data, keyid=privkey.key_id) + result = gpg.sign(data, default_key=privkey.key_id) rfprint = privkey.fingerprint privkey = gpg.list_keys(secret=True).pop() kfprint = privkey['fingerprint'] diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 5f232fe..2512604 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -28,7 +28,6 @@ from leap.keymanager import ( KeyManager, openpgp, KeyNotFound, - NoPasswordGiven, errors, ) from leap.keymanager.openpgp import OpenPGPKey -- cgit v1.2.3 From 5e5af82141b6740067432bfc24750ffd2e0c98a3 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 2 Oct 2013 11:33:28 -0400 Subject: add sanity check for gnupg package and version --- keymanager/changes/bug_enforce_correct_version | 1 + keymanager/src/leap/keymanager/__init__.py | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 keymanager/changes/bug_enforce_correct_version diff --git a/keymanager/changes/bug_enforce_correct_version b/keymanager/changes/bug_enforce_correct_version new file mode 100644 index 0000000..eb03e41 --- /dev/null +++ b/keymanager/changes/bug_enforce_correct_version @@ -0,0 +1 @@ + o Add a sanity check for the correct version of gnupg. diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 2f39ad9..76be226 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -14,11 +14,28 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - - """ Key Manager is a Nicknym agent for LEAP client. """ +# let's do a little sanity check to see if we're using the wrong gnupg +import sys + +try: + from gnupg.gnupg import GPGUtilities + assert(GPGUtilities) # pyflakes happy + from gnupg import __version__ + from distutils.version import LooseVersion as V + assert(V(__version__) >= V('1.2.2')) + +except ImportError, AssertionError: + print "Ooops! It looks like there is a conflict in the installed version " + print "of gnupg." + print "Disclaimer: Ideally, we would need to work a patch and propose the " + print "merge to upstream. But until then do: " + print + print "% pip uninstall python-gnupg" + print "% pip install gnupg" + sys.exit(1) import logging import requests -- cgit v1.2.3 From 270af39ef825f010f40ab7f5317578a7d4a18fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 4 Oct 2013 11:22:53 -0300 Subject: Fold in changes --- keymanager/CHANGELOG | 4 ++++ keymanager/changes/bug_enforce_correct_version | 1 - keymanager/changes/feature_2342-use-new-gnupg-module | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 keymanager/changes/bug_enforce_correct_version delete mode 100644 keymanager/changes/feature_2342-use-new-gnupg-module diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index 9cd53b3..6ec2624 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,7 @@ +0.3.3 Oct 4: + o Add a sanity check for the correct version of gnupg. + o Update code to use gnupg 1.2.2 python module. Closes #2342. + 0.3.2 Sep 6: o Do not raise exception when a GET request doesn't return 2XX code. Nickserver uses error codes for more verbosity in the diff --git a/keymanager/changes/bug_enforce_correct_version b/keymanager/changes/bug_enforce_correct_version deleted file mode 100644 index eb03e41..0000000 --- a/keymanager/changes/bug_enforce_correct_version +++ /dev/null @@ -1 +0,0 @@ - o Add a sanity check for the correct version of gnupg. diff --git a/keymanager/changes/feature_2342-use-new-gnupg-module b/keymanager/changes/feature_2342-use-new-gnupg-module deleted file mode 100644 index b83e8ec..0000000 --- a/keymanager/changes/feature_2342-use-new-gnupg-module +++ /dev/null @@ -1 +0,0 @@ - o Update code to use gnupg 1.2.2 python module. Closes #2342. -- cgit v1.2.3 From aa8015567aa840da4874385716f11b1161a4ec5f Mon Sep 17 00:00:00 2001 From: drebs Date: Fri, 4 Oct 2013 16:30:24 -0300 Subject: Add option to choose cipher and digest algorithms. --- .../feature_4030-add-cipher-and-digest-algo | 2 ++ keymanager/src/leap/keymanager/__init__.py | 20 +++++++++++++++++--- keymanager/src/leap/keymanager/openpgp.py | 22 ++++++++++++++++++---- 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 keymanager/changes/feature_4030-add-cipher-and-digest-algo diff --git a/keymanager/changes/feature_4030-add-cipher-and-digest-algo b/keymanager/changes/feature_4030-add-cipher-and-digest-algo new file mode 100644 index 0000000..a05e589 --- /dev/null +++ b/keymanager/changes/feature_4030-add-cipher-and-digest-algo @@ -0,0 +1,2 @@ + o Add option to choose cipher and digest algorithms when signing and + encrypting. Closes #4030. diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 76be226..edd0ba1 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -391,7 +391,8 @@ class KeyManager(object): # encrypt/decrypt and sign/verify API # - def encrypt(self, data, pubkey, passphrase=None, sign=None): + def encrypt(self, data, pubkey, passphrase=None, sign=None, + cipher_algo='AES256'): """ Encrypt C{data} using public @{key} and sign with C{sign} key. @@ -401,6 +402,8 @@ class KeyManager(object): :type pubkey: EncryptionKey :param sign: The key used for signing. :type sign: EncryptionKey + :param cipher_algo: The cipher algorithm to use. + :type cipher_algo: str :return: The encrypted data. :rtype: str @@ -436,7 +439,8 @@ class KeyManager(object): return self._wrapper_map[privkey.__class__].decrypt( data, privkey, passphrase, verify) - def sign(self, data, privkey): + def sign(self, data, privkey, digest_algo='SHA512', clearsign=False, + detach=True, binary=False): """ Sign C{data} with C{privkey}. @@ -445,6 +449,14 @@ class KeyManager(object): :param privkey: The private key to be used to sign. :type privkey: EncryptionKey + :param digest_algo: The hash digest to use. + :type digest_algo: str + :param clearsign: If True, create a cleartext signature. + :type clearsign: bool + :param detach: If True, create a detached signature. + :type detach: bool + :param binary: If True, do not ascii armour the output. + :type binary: bool :return: The signed data. :rtype: str @@ -454,7 +466,9 @@ class KeyManager(object): privkey.__class__ in self._wrapper_map, 'Unknown key type.') leap_assert(privkey.private is True, 'Key is not private.') - return self._wrapper_map[privkey.__class__].sign(data, privkey) + return self._wrapper_map[privkey.__class__].sign( + data, privkey, digest_algo=digest_algo, clearsign=clearsign, + detach=detach, binary=binary) def verify(self, data, pubkey): """ diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 9d8d89a..6412331 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -433,7 +433,8 @@ class OpenPGPScheme(EncryptionScheme): raise errors.EncryptionDecryptionFailed( 'Failed to encrypt/decrypt: %s' % stderr) - def encrypt(self, data, pubkey, passphrase=None, sign=None): + def encrypt(self, data, pubkey, passphrase=None, sign=None, + cipher_algo='AES256'): """ Encrypt C{data} using public @{pubkey} and sign with C{sign} key. @@ -443,6 +444,8 @@ class OpenPGPScheme(EncryptionScheme): :type pubkey: OpenPGPKey :param sign: The key used for signing. :type sign: OpenPGPKey + :param cipher_algo: The cipher algorithm to use. + :type cipher_algo: str :return: The encrypted data. :rtype: str @@ -459,7 +462,7 @@ class OpenPGPScheme(EncryptionScheme): data, pubkey.fingerprint, default_key=sign.key_id if sign else None, passphrase=passphrase, symmetric=False, - cipher_algo='AES256') + cipher_algo=cipher_algo) # Here we cannot assert for correctness of sig because the sig is # in the ciphertext. # result.ok - (bool) indicates if the operation succeeded @@ -517,7 +520,8 @@ class OpenPGPScheme(EncryptionScheme): gpgutil = GPGUtilities(gpg) return gpgutil.is_encrypted_asym(data) - def sign(self, data, privkey): + def sign(self, data, privkey, digest_algo='SHA512', clearsign=False, + detach=True, binary=False): """ Sign C{data} with C{privkey}. @@ -526,6 +530,14 @@ class OpenPGPScheme(EncryptionScheme): :param privkey: The private key to be used to sign. :type privkey: OpenPGPKey + :param digest_algo: The hash digest to use. + :type digest_algo: str + :param clearsign: If True, create a cleartext signature. + :type clearsign: bool + :param detach: If True, create a detached signature. + :type detach: bool + :param binary: If True, do not ascii armour the output. + :type binary: bool :return: The ascii-armored signed data. :rtype: str @@ -536,7 +548,9 @@ class OpenPGPScheme(EncryptionScheme): # result.fingerprint - contains the fingerprint of the key used to # sign. with self._temporary_gpgwrapper(privkey) as gpg: - result = gpg.sign(data, default_key=privkey.key_id) + result = gpg.sign(data, default_key=privkey.key_id, + digest_algo=digest_algo, clearsign=clearsign, + detach=detach, binary=binary) rfprint = privkey.fingerprint privkey = gpg.list_keys(secret=True).pop() kfprint = privkey['fingerprint'] -- cgit v1.2.3 From 72c2264f7de479c2f3e91c6f17a27a7f35dc4312 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 9 Oct 2013 11:58:33 -0300 Subject: Set gnupg dep version to 1.2.3. --- keymanager/pkg/requirements.pip | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index 5ebd803..acb783c 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -1,4 +1,4 @@ leap.common>=0.3.0 simplejson requests -gnupg +gnupg>=1.2.3 -- cgit v1.2.3 From 542c4a5b7bf1c95bdfcb4059e9968412b0e53d67 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Oct 2013 12:23:35 -0300 Subject: bump also the gnupg sanity check --- keymanager/pkg/requirements.pip | 2 ++ keymanager/src/leap/keymanager/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index acb783c..1515204 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -1,4 +1,6 @@ leap.common>=0.3.0 simplejson requests +# if we bump the gnupg version, bump also the sanity check +# in keymanager.__init__ gnupg>=1.2.3 diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index edd0ba1..f2cffd5 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -25,7 +25,7 @@ try: assert(GPGUtilities) # pyflakes happy from gnupg import __version__ from distutils.version import LooseVersion as V - assert(V(__version__) >= V('1.2.2')) + assert(V(__version__) >= V('1.2.3')) except ImportError, AssertionError: print "Ooops! It looks like there is a conflict in the installed version " -- cgit v1.2.3 From abb91dd3af9aa28eb998546fbd4e3c6feaa3bdbd Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 9 Oct 2013 13:03:30 -0300 Subject: fix exception catching --- keymanager/src/leap/keymanager/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index f2cffd5..a550598 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -27,14 +27,17 @@ try: from distutils.version import LooseVersion as V assert(V(__version__) >= V('1.2.3')) -except ImportError, AssertionError: +except (ImportError, AssertionError): + print "*******" print "Ooops! It looks like there is a conflict in the installed version " print "of gnupg." + print print "Disclaimer: Ideally, we would need to work a patch and propose the " print "merge to upstream. But until then do: " print print "% pip uninstall python-gnupg" print "% pip install gnupg" + print "*******" sys.exit(1) import logging -- cgit v1.2.3 From d7b88c879a682551768c46e54f3e9856cd382226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 18 Oct 2013 09:05:37 -0300 Subject: Fold in changes --- keymanager/CHANGELOG | 4 ++++ keymanager/changes/feature_4030-add-cipher-and-digest-algo | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 keymanager/changes/feature_4030-add-cipher-and-digest-algo diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index 6ec2624..8371498 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,7 @@ +0.3.4 Oct 18: + o Add option to choose cipher and digest algorithms when signing and + encrypting. Closes #4030. + 0.3.3 Oct 4: o Add a sanity check for the correct version of gnupg. o Update code to use gnupg 1.2.2 python module. Closes #2342. diff --git a/keymanager/changes/feature_4030-add-cipher-and-digest-algo b/keymanager/changes/feature_4030-add-cipher-and-digest-algo deleted file mode 100644 index a05e589..0000000 --- a/keymanager/changes/feature_4030-add-cipher-and-digest-algo +++ /dev/null @@ -1,2 +0,0 @@ - o Add option to choose cipher and digest algorithms when signing and - encrypting. Closes #4030. -- cgit v1.2.3 From 9cd2ee1e1b96ad521967096424b0b9ff16ed82da Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 23 Oct 2013 10:39:28 -0300 Subject: Return unicode in order to solve encoding issues. --- keymanager/changes/bug-4000_suport-non-ascii | 1 + keymanager/src/leap/keymanager/openpgp.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 keymanager/changes/bug-4000_suport-non-ascii diff --git a/keymanager/changes/bug-4000_suport-non-ascii b/keymanager/changes/bug-4000_suport-non-ascii new file mode 100644 index 0000000..cb1b78f --- /dev/null +++ b/keymanager/changes/bug-4000_suport-non-ascii @@ -0,0 +1 @@ + o Return unicode decrypted text to avoid encoding issues. Related to #4000. diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 6412331..8ec8639 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -26,6 +26,7 @@ import os import re import shutil import tempfile +import locale from gnupg import GPG from gnupg.gnupg import GPGUtilities @@ -482,7 +483,7 @@ class OpenPGPScheme(EncryptionScheme): :type verify: OpenPGPKey :return: The decrypted data. - :rtype: str + :rtype: unicode @raise InvalidSignature: Raised if unable to verify the signature with C{verify} key. @@ -504,7 +505,12 @@ class OpenPGPScheme(EncryptionScheme): raise errors.InvalidSignature( 'Failed to verify signature with key %s: %s' % (verify.key_id, stderr)) - return result.data + + # XXX: this is the encoding used by gpg module + # https://github.com/isislovecruft/python-gnupg/\ + # blob/master/gnupg/_meta.py#L121 + encoding = locale.getpreferredencoding() + return result.data.decode(encoding, 'replace') def is_encrypted(self, data): """ -- cgit v1.2.3 From 8de47edac23f3d6a7304a78b701ad0fde0c0d214 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 30 Oct 2013 11:08:42 -0200 Subject: add freeze_debianver command --- keymanager/setup.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/keymanager/setup.py b/keymanager/setup.py index 1e48931..778909d 100644 --- a/keymanager/setup.py +++ b/keymanager/setup.py @@ -17,6 +17,7 @@ """ setup file for leap.keymanager """ +import re from setuptools import setup from setuptools import find_packages @@ -42,17 +43,82 @@ trove_classifiers = [ 'Topic :: Software Development :: Libraries', ] +DOWNLOAD_BASE = ('https://github.com/leapcode/keymanager/' + 'archive/%s.tar.gz') +_versions = versioneer.get_versions() +VERSION = _versions['version'] +VERSION_FULL = _versions['full'] +DOWNLOAD_URL = "" + +# get the short version for the download url +_version_short = re.findall('\d+\.\d+\.\d+', VERSION) +if len(_version_short) > 0: + VERSION_SHORT = _version_short[0] + DOWNLOAD_URL = DOWNLOAD_BASE % VERSION_SHORT + +cmdclass = versioneer.get_cmdclass() + + +from setuptools import Command + + +class freeze_debianver(Command): + """ + Freezes the version in a debian branch. + To be used after merging the development branch onto the debian one. + """ + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + proceed = str(raw_input( + "This will overwrite the file _version.py. Continue? [y/N] ")) + if proceed != "y": + print("He. You scared. Aborting.") + return + template = r""" +# This file was generated by the `freeze_debianver` command in setup.py +# Using 'versioneer.py' (0.7+) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +version_version = '{version}' +version_full = '{version_full}' +""" + templatefun = r""" + +def get_versions(default={}, verbose=False): + return {'version': version_version, 'full': version_full} +""" + subst_template = template.format( + version=VERSION_SHORT, + version_full=VERSION_FULL) + templatefun + with open(versioneer.versionfile_source, 'w') as f: + f.write(subst_template) + + +cmdclass["freeze_debianver"] = freeze_debianver + # XXX add ref to docs setup( name='leap.keymanager', - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), + version=VERSION, + cmdclass=cmdclass, url='https://leap.se/', + download_url=DOWNLOAD_URL, license='GPLv3+', description='LEAP\'s Key Manager', author='The LEAP Encryption Access Project', author_email='info@leap.se', + maintainer='Kali Kaneko', + maintainer_email='kali@leap.se', long_description=( "The Key Manager handles all types of keys to allow for " "point-to-point encryption between parties communicating through " -- cgit v1.2.3 From 4f066dfd45e03c9fab4c72205a217959f9ab8ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 1 Nov 2013 10:33:51 -0300 Subject: Fold in changes --- keymanager/CHANGELOG | 4 ++++ keymanager/changes/bug-4000_suport-non-ascii | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 keymanager/changes/bug-4000_suport-non-ascii diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index 8371498..dc89742 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,7 @@ +0.3.5 Nov 1: + o Return unicode decrypted text to avoid encoding issues. Related to + #4000. + 0.3.4 Oct 18: o Add option to choose cipher and digest algorithms when signing and encrypting. Closes #4030. diff --git a/keymanager/changes/bug-4000_suport-non-ascii b/keymanager/changes/bug-4000_suport-non-ascii deleted file mode 100644 index cb1b78f..0000000 --- a/keymanager/changes/bug-4000_suport-non-ascii +++ /dev/null @@ -1 +0,0 @@ - o Return unicode decrypted text to avoid encoding issues. Related to #4000. -- cgit v1.2.3 From 96ac727920ed50bb6c067a3b1da2b8ee7d603762 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 4 Nov 2013 15:09:40 -0200 Subject: Add verification of detached signatures. --- .../feature-4375_add-verification-of-detached-sigs | 1 + keymanager/src/leap/keymanager/__init__.py | 12 +++++++---- keymanager/src/leap/keymanager/keys.py | 9 +++++--- keymanager/src/leap/keymanager/openpgp.py | 25 ++++++++++++++++++---- .../src/leap/keymanager/tests/test_keymanager.py | 14 ++++++++++-- 5 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 keymanager/changes/feature-4375_add-verification-of-detached-sigs diff --git a/keymanager/changes/feature-4375_add-verification-of-detached-sigs b/keymanager/changes/feature-4375_add-verification-of-detached-sigs new file mode 100644 index 0000000..ee09cbe --- /dev/null +++ b/keymanager/changes/feature-4375_add-verification-of-detached-sigs @@ -0,0 +1 @@ + o Add verification of detached signatures. Closes #4375. diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index a550598..61213d3 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -473,15 +473,18 @@ class KeyManager(object): data, privkey, digest_algo=digest_algo, clearsign=clearsign, detach=detach, binary=binary) - def verify(self, data, pubkey): + def verify(self, data, pubkey, detached_sig=None): """ - Verify signed C{data} with C{pubkey}. + Verify signed C{data} with C{pubkey}, eventually using + C{detached_sig}. :param data: The data to be verified. :type data: str - :param pubkey: The public key to be used on verification. :type pubkey: EncryptionKey + :param detached_sig: A detached signature. If given, C{data} is + verified using this detached signature. + :type detached_sig: str :return: The signed data. :rtype: str @@ -489,7 +492,8 @@ class KeyManager(object): leap_assert_type(pubkey, EncryptionKey) leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.') leap_assert(pubkey.private is False, 'Key is not public.') - return self._wrapper_map[pubkey.__class__].verify(data, pubkey) + return self._wrapper_map[pubkey.__class__].verify( + data, pubkey, detached_sig=detached_sig) from ._version import get_versions __version__ = get_versions()['version'] diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index b8e88d4..ec1bfeb 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -340,15 +340,18 @@ class EncryptionScheme(object): pass @abstractmethod - def verify(self, data, pubkey): + def verify(self, data, pubkey, detached_sig=None): """ - Verify signed C{data} with C{pubkey}. + Verify signed C{data} with C{pubkey}, eventually using + C{detached_sig}. :param data: The data to be verified. :type data: str - :param pubkey: The public key to be used on verification. :type pubkey: EncryptionKey + :param detached_sig: A detached signature. If given, C{data} is + verified against this sdetached signature. + :type detached_sig: str :return: The signed data. :rtype: str diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 8ec8639..111dfaf 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -30,6 +30,7 @@ import locale from gnupg import GPG from gnupg.gnupg import GPGUtilities +from gnupg._util import _make_binary_stream from leap.common.check import leap_assert, leap_assert_type from leap.keymanager import errors @@ -570,15 +571,18 @@ class OpenPGPScheme(EncryptionScheme): '%s != %s' % (rfprint, kfprint)) return result.data - def verify(self, data, pubkey): + def verify(self, data, pubkey, detached_sig=None): """ - Verify signed C{data} with C{pubkey}. + Verify signed C{data} with C{pubkey}, eventually using + C{detached_sig}. :param data: The data to be verified. :type data: str - :param pubkey: The public key to be used on verification. :type pubkey: OpenPGPKey + :param detached_sig: A detached signature. If given, C{data} is + verified against this detached signature. + :type detached_sig: str :return: The ascii-armored signed data. :rtype: str @@ -586,7 +590,20 @@ class OpenPGPScheme(EncryptionScheme): leap_assert_type(pubkey, OpenPGPKey) leap_assert(pubkey.private is False) with self._temporary_gpgwrapper(pubkey) as gpg: - result = gpg.verify(data) + result = None + if detached_sig is None: + result = gpg.verify(data) + else: + # to verify using a detached sig we have to use + # gpg.verify_file(), which receives the data as a binary + # stream and the name of a file containing the signature. + sf, sfname = tempfile.mkstemp() + sfd = os.fdopen(sf, 'w') + sfd.write(detached_sig) + sfd.close() + df = _make_binary_stream(data, gpg._encoding) + result = gpg.verify_file(df, sig_file=sfname) + df.close() gpgpubkey = gpg.list_keys().pop() valid = result.valid rfprint = result.fingerprint diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 2512604..67676e9 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -294,7 +294,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp.put_ascii_key(PRIVATE_KEY) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) - signed = pgp.sign(data, privkey) + signed = pgp.sign(data, privkey, detach=False) pubkey = pgp.get_key(ADDRESS, private=False) self.assertTrue(pgp.verify(signed, pubkey)) @@ -314,6 +314,16 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): encrypted_and_signed, privkey2, verify=pubkey) self.assertTrue(data, res) + def test_sign_verify_detached_sig(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + pgp.put_ascii_key(PRIVATE_KEY) + data = 'data' + privkey = pgp.get_key(ADDRESS, private=True) + signature = pgp.sign(data, privkey, detach=True) + pubkey = pgp.get_key(ADDRESS, private=False) + self.assertTrue(pgp.verify(data, pubkey, detached_sig=signature)) + class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): @@ -490,7 +500,7 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): privkey = km.get_key( ADDRESS, OpenPGPKey, private=True, fetch_remote=False) # encrypt - signdata = km.sign(self.RAW_DATA, privkey) + signdata = km.sign(self.RAW_DATA, privkey, detach=False) self.assertNotEqual(self.RAW_DATA, signdata) # get public key for verifying pubkey = km.get_key( -- cgit v1.2.3 From 54cad35e3282bc64d772d472abd093b359ad91a5 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 4 Nov 2013 15:15:46 -0200 Subject: Remove key comment on OpenPGP key generation. --- keymanager/src/leap/keymanager/openpgp.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 111dfaf..a4dc1b8 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -27,6 +27,7 @@ import re import shutil import tempfile import locale +from contextlib import closing from gnupg import GPG from gnupg.gnupg import GPGUtilities @@ -47,6 +48,10 @@ from leap.keymanager.keys import ( logger = logging.getLogger(__name__) +# +# A temporary GPG keyring wrapped to provide OpenPGP functionality. +# + class TempGPGWrapper(object): """ A context manager that wraps a temporary GPG keyring which only contains @@ -244,7 +249,7 @@ class OpenPGPScheme(EncryptionScheme): key_length=4096, name_real=address, name_email=address, - name_comment='Generated by LEAP Key Manager.') + name_comment='') logger.info("About to generate keys... This might take SOME time.") gpg.gen_key(params) logger.info("Keys for %s have been successfully " @@ -598,12 +603,10 @@ class OpenPGPScheme(EncryptionScheme): # gpg.verify_file(), which receives the data as a binary # stream and the name of a file containing the signature. sf, sfname = tempfile.mkstemp() - sfd = os.fdopen(sf, 'w') - sfd.write(detached_sig) - sfd.close() - df = _make_binary_stream(data, gpg._encoding) - result = gpg.verify_file(df, sig_file=sfname) - df.close() + with os.fdopen(sf, 'w') as sfd: + sfd.write(detached_sig) + with closing(_make_binary_stream(data, gpg._encoding)) as df: + result = gpg.verify_file(df, sig_file=sfname) gpgpubkey = gpg.list_keys().pop() valid = result.valid rfprint = result.fingerprint -- cgit v1.2.3 From 0f10209dafb330320df387e66a1cc9083a9ae755 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 11 Nov 2013 16:24:28 -0300 Subject: Use 'utf-8' as default/fallback encoding. - Also reorder stdlib imports alphabetically :) --- keymanager/changes/bug-4427_default-encoding-to-utf8 | 2 ++ keymanager/src/leap/keymanager/openpgp.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 keymanager/changes/bug-4427_default-encoding-to-utf8 diff --git a/keymanager/changes/bug-4427_default-encoding-to-utf8 b/keymanager/changes/bug-4427_default-encoding-to-utf8 new file mode 100644 index 0000000..3f7d671 --- /dev/null +++ b/keymanager/changes/bug-4427_default-encoding-to-utf8 @@ -0,0 +1,2 @@ + o Default encoding to 'utf-8' in case of system encodings not set. Closes + #4427. diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index a4dc1b8..1670e1c 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -21,12 +21,14 @@ Infrastructure for using OpenPGP keys in Key Manager. """ +import locale import logging import os import re import shutil +import sys import tempfile -import locale + from contextlib import closing from gnupg import GPG @@ -516,6 +518,10 @@ class OpenPGPScheme(EncryptionScheme): # https://github.com/isislovecruft/python-gnupg/\ # blob/master/gnupg/_meta.py#L121 encoding = locale.getpreferredencoding() + if encoding is None: + encoding = sys.stdin.encoding + if encoding is None: + encoding = 'utf-8' return result.data.decode(encoding, 'replace') def is_encrypted(self, data): -- cgit v1.2.3 From dc5eff74a2ae85c74bca4747217565ca626bda91 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 13 Nov 2013 18:09:15 -0300 Subject: Refactor code to support parsing ascii keys. --- keymanager/src/leap/keymanager/errors.py | 12 ++++++ keymanager/src/leap/keymanager/openpgp.py | 63 ++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/keymanager/src/leap/keymanager/errors.py b/keymanager/src/leap/keymanager/errors.py index 89949d2..27180db 100644 --- a/keymanager/src/leap/keymanager/errors.py +++ b/keymanager/src/leap/keymanager/errors.py @@ -84,3 +84,15 @@ class SignFailed(Exception): Raised when failed to sign. """ pass + + +class KeyAddressMismatch(Exception): + """ + A mismatch between addresses. + """ + + +class KeyFingerprintMismatch(Exception): + """ + A mismatch between fingerprints. + """ diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 1670e1c..78acd6a 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -35,7 +35,7 @@ from gnupg import GPG from gnupg.gnupg import GPGUtilities from gnupg._util import _make_binary_stream -from leap.common.check import leap_assert, leap_assert_type +from leap.common.check import leap_assert, leap_assert_type, leap_check from leap.keymanager import errors from leap.keymanager.keys import ( EncryptionKey, @@ -303,16 +303,22 @@ class OpenPGPScheme(EncryptionScheme): raise errors.KeyNotFound(address) return build_key_from_dict(OpenPGPKey, address, doc.content) - def put_ascii_key(self, key_data): + def parse_ascii_key(self, key_data): """ - Put key contained in ascii-armored C{key_data} in local storage. + Parses an ascii armored key (or key pair) data and returns + the OpenPGPKey keys. - :param key_data: The key data to be stored. + :param key_data: the key data to be parsed. :type key_data: str or unicode + + :returns: the public key and private key (if applies) for that data. + :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey) + the tuple may have one or both components None """ leap_assert_type(key_data, (str, unicode)) # TODO: add more checks for correct key data. leap_assert(key_data is not None, 'Data does not represent a key.') + mail_regex = '.*<([\w.-]+@[\w.-]+)>.*' with self._temporary_gpgwrapper() as gpg: # TODO: inspect result, or use decorator @@ -325,31 +331,54 @@ class OpenPGPScheme(EncryptionScheme): except IndexError: pass pubkey = gpg.list_keys(secret=False).pop() # unitary keyring + # extract adress from first uid on key - match = re.match('.*<([\w.-]+@[\w.-]+)>.*', pubkey['uids'].pop()) + match = re.match(mail_regex, pubkey['uids'].pop()) leap_assert(match is not None, 'No user address in key data.') address = match.group(1) + if privkey is not None: - match = re.match( - '.*<([\w.-]+@[\w.-]+)>.*', privkey['uids'].pop()) + match = re.match(mail_regex, privkey['uids'].pop()) leap_assert(match is not None, 'No user address in key data.') privaddress = match.group(1) - leap_assert( - address == privaddress, - 'Addresses in pub and priv key differ.') - leap_assert( - pubkey['fingerprint'] == privkey['fingerprint'], - 'Fingerprints for pub and priv key differ.') - # insert private key in storage + + # build private key openpgp_privkey = _build_key_from_gpg( - address, privkey, + privaddress, privkey, gpg.export_keys(privkey['fingerprint'], secret=True)) - self.put_key(openpgp_privkey) - # insert public key in storage + + leap_check(address == privaddress, + 'Addresses in public and private key differ.', + errors.KeyAddressMismatch) + leap_check(pubkey['fingerprint'] == privkey['fingerprint'], + 'Fingerprints for public and private key differ.', + errors.KeyFingerprintMismatch) + + # build public key openpgp_pubkey = _build_key_from_gpg( address, pubkey, gpg.export_keys(pubkey['fingerprint'], secret=False)) + + return (openpgp_pubkey, openpgp_privkey) + + def put_ascii_key(self, key_data): + """ + Put key contained in ascii-armored C{key_data} in local storage. + + :param key_data: The key data to be stored. + :type key_data: str or unicode + """ + leap_assert_type(key_data, (str, unicode)) + + try: + openpgp_pubkey, openpgp_privkey = self.parse_ascii_key(key_data) + except (errors.KeyAddressMismatch, errors.KeyFingerprintMismatch) as e: + leap_assert(False, repr(e)) + + if openpgp_pubkey is not None: self.put_key(openpgp_pubkey) + if openpgp_privkey is not None: + self.put_key(openpgp_privkey) def put_key(self, key): """ -- cgit v1.2.3 From edd27d8d9afd60e73acd5b4b509f54178d09f894 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 14 Nov 2013 11:59:28 -0300 Subject: Expose openpgp methods in keymanager. - parse_ascii_key - delete_key - put_key --- keymanager/src/leap/keymanager/__init__.py | 42 ++++++++++++++++++++++++++++++ keymanager/src/leap/keymanager/openpgp.py | 6 ++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 61213d3..dbc5489 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -495,6 +495,48 @@ class KeyManager(object): return self._wrapper_map[pubkey.__class__].verify( data, pubkey, detached_sig=detached_sig) + def parse_openpgp_ascii_key(self, key_data): + """ + Parses an ascii armored key (or key pair) data and returns + the OpenPGPKey keys. + + :param key_data: the key data to be parsed. + :type key_data: str or unicode + + :returns: the public key and private key (if applies) for that data. + :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey) + the tuple may have one or both components None + """ + return self._wrapper_map[OpenPGPKey].parse_ascii_key(key_data) + + def delete_key(self, key): + """ + Remove C{key} from storage. + + May raise: + openpgp.errors.KeyNotFound + openpgp.errors.KeyAttributesDiffer + + :param key: The key to be removed. + :type key: EncryptionKey + """ + try: + self._wrapper_map[type(key)].delete_key(key) + except IndexError as e: + leap_assert(False, "Unsupported key type. Error {0!r}".format(e)) + + def put_key(self, key): + """ + Put C{key} in local storage. + + :param key: The key to be stored. + :type key: OpenPGPKey + """ + try: + self._wrapper_map[type(key)].put_key(key) + except IndexError as e: + leap_assert(False, "Unsupported key type. Error {0!r}".format(e)) + from ._version import get_versions __version__ = get_versions()['version'] del get_versions diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 78acd6a..f6223d5 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -424,10 +424,14 @@ class OpenPGPScheme(EncryptionScheme): """ Remove C{key} from storage. + May raise: + errors.KeyNotFound + errors.KeyAttributesDiffer + :param key: The key to be removed. :type key: EncryptionKey """ - leap_assert(key.__class__ is OpenPGPKey, 'Wrong key type.') + leap_assert_type(key, OpenPGPKey) stored_key = self.get_key(key.address, private=key.private) if stored_key is None: raise errors.KeyNotFound(key) -- cgit v1.2.3 From 00236dedb21b95e607fef7b7912ef4567cbcdb6b Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 14 Nov 2013 12:07:30 -0300 Subject: Add changes file. --- keymanager/changes/feature-expose-methods | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 keymanager/changes/feature-expose-methods diff --git a/keymanager/changes/feature-expose-methods b/keymanager/changes/feature-expose-methods new file mode 100644 index 0000000..92b30d5 --- /dev/null +++ b/keymanager/changes/feature-expose-methods @@ -0,0 +1,2 @@ + o New openpgp method: parse_ascii_keys. + o Expose openpgp methods in keymanager (parse_ascii_keys, put_key, delete_key). -- cgit v1.2.3 From 04274cc916007fec5745a4aee834d6a56a1da134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 15 Nov 2013 10:04:03 -0300 Subject: Fold in changes --- keymanager/CHANGELOG | 8 ++++++++ keymanager/changes/bug-4427_default-encoding-to-utf8 | 2 -- keymanager/changes/feature-4375_add-verification-of-detached-sigs | 1 - keymanager/changes/feature-expose-methods | 2 -- 4 files changed, 8 insertions(+), 5 deletions(-) delete mode 100644 keymanager/changes/bug-4427_default-encoding-to-utf8 delete mode 100644 keymanager/changes/feature-4375_add-verification-of-detached-sigs delete mode 100644 keymanager/changes/feature-expose-methods diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index dc89742..e20b86c 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,11 @@ +0.3.6 Nov 15: + o Default encoding to 'utf-8' in case of system encodings not + set. Closes #4427. + o Add verification of detached signatures. Closes #4375. + o New openpgp method: parse_ascii_keys. + o Expose openpgp methods in keymanager (parse_ascii_keys, put_key, + delete_key). + 0.3.5 Nov 1: o Return unicode decrypted text to avoid encoding issues. Related to #4000. diff --git a/keymanager/changes/bug-4427_default-encoding-to-utf8 b/keymanager/changes/bug-4427_default-encoding-to-utf8 deleted file mode 100644 index 3f7d671..0000000 --- a/keymanager/changes/bug-4427_default-encoding-to-utf8 +++ /dev/null @@ -1,2 +0,0 @@ - o Default encoding to 'utf-8' in case of system encodings not set. Closes - #4427. diff --git a/keymanager/changes/feature-4375_add-verification-of-detached-sigs b/keymanager/changes/feature-4375_add-verification-of-detached-sigs deleted file mode 100644 index ee09cbe..0000000 --- a/keymanager/changes/feature-4375_add-verification-of-detached-sigs +++ /dev/null @@ -1 +0,0 @@ - o Add verification of detached signatures. Closes #4375. diff --git a/keymanager/changes/feature-expose-methods b/keymanager/changes/feature-expose-methods deleted file mode 100644 index 92b30d5..0000000 --- a/keymanager/changes/feature-expose-methods +++ /dev/null @@ -1,2 +0,0 @@ - o New openpgp method: parse_ascii_keys. - o Expose openpgp methods in keymanager (parse_ascii_keys, put_key, delete_key). -- cgit v1.2.3 From 5be02926efeb775e652c217ade02250b0cc7e3ca Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 21 Nov 2013 12:12:31 -0300 Subject: Do not check for a valid address, rely on client. An email address regexp that supports all (or almost) emails is really complex, and is_address uses a simpler one since we restrict the valid usernames and provider names. For that we rely on the email client to check whether the email address is valid or not. --- keymanager/src/leap/keymanager/openpgp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index f6223d5..0c11d17 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -297,7 +297,6 @@ class OpenPGPScheme(EncryptionScheme): :rtype: OpenPGPKey @raise KeyNotFound: If the key was not found on local storage. """ - leap_assert(is_address(address), 'Not an user address: %s' % address) doc = self._get_key_doc(address, private) if doc is None: raise errors.KeyNotFound(address) -- cgit v1.2.3 From dd85ddd5e45b86e60e2d346ead3d09deccbef0ed Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 21 Nov 2013 12:23:33 -0300 Subject: Remove '+' suffix in email address. This allows us to support encrypt mails to 'test_user@provider.com' even if was sent to 'test_user+optional_string@provider.com'. --- keymanager/src/leap/keymanager/openpgp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 0c11d17..cb46371 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -297,6 +297,11 @@ class OpenPGPScheme(EncryptionScheme): :rtype: OpenPGPKey @raise KeyNotFound: If the key was not found on local storage. """ + # Remove the identity suffix after the '+' until the '@' + # e.g.: test_user+something@provider.com becomes test_user@probider.com + # since the key belongs to the identity without the '+' suffix. + address = re.sub(r'\+.*\@', '@', address) + doc = self._get_key_doc(address, private) if doc is None: raise errors.KeyNotFound(address) -- cgit v1.2.3 From c9065eccaaed40d053d16afc9cea14e122e60fe0 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 21 Nov 2013 12:30:14 -0300 Subject: Add changes file for #4491. --- keymanager/changes/email-checks | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 keymanager/changes/email-checks diff --git a/keymanager/changes/email-checks b/keymanager/changes/email-checks new file mode 100644 index 0000000..eee7de7 --- /dev/null +++ b/keymanager/changes/email-checks @@ -0,0 +1,3 @@ + o Remove address check when sending email and rely in the email client to + verify that is correct. Closes #4491. + o Support sending encrypted mails to addresses using the '+' sign. -- cgit v1.2.3 From 58a45c40ae8b5619f6c848feaec329396e09b42f Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 25 Nov 2013 11:20:55 -0200 Subject: Fix error returns on openpgp backend. Also fix: - some docstrings. - variabled referenced without previous assignement. --- keymanager/changes/bug_fix-returns | 1 + keymanager/src/leap/keymanager/__init__.py | 3 +++ keymanager/src/leap/keymanager/openpgp.py | 9 +++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 keymanager/changes/bug_fix-returns diff --git a/keymanager/changes/bug_fix-returns b/keymanager/changes/bug_fix-returns new file mode 100644 index 0000000..3bb7264 --- /dev/null +++ b/keymanager/changes/bug_fix-returns @@ -0,0 +1 @@ + o Fix error return values on openpgp backend. diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index dbc5489..6cfbf71 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -425,6 +425,9 @@ class KeyManager(object): :type data: str :param privkey: The key used to decrypt. :type privkey: OpenPGPKey + :param passphrase: The passphrase for the secret key used for + decryption. + :type passphrase: str :param verify: The key used to verify a signature. :type verify: OpenPGPKey diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index cb46371..5781d67 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -341,6 +341,7 @@ class OpenPGPScheme(EncryptionScheme): leap_assert(match is not None, 'No user address in key data.') address = match.group(1) + openpgp_privkey = None if privkey is not None: match = re.match(mail_regex, privkey['uids'].pop()) leap_assert(match is not None, 'No user address in key data.') @@ -374,6 +375,7 @@ class OpenPGPScheme(EncryptionScheme): """ leap_assert_type(key_data, (str, unicode)) + openpgp_privkey = None try: openpgp_pubkey, openpgp_privkey = self.parse_ascii_key(key_data) except (errors.KeyAddressMismatch, errors.KeyFingerprintMismatch) as e: @@ -524,6 +526,9 @@ class OpenPGPScheme(EncryptionScheme): :type data: str :param privkey: The key used to decrypt. :type privkey: OpenPGPKey + :param passphrase: The passphrase for the secret key used for + decryption. + :type passphrase: str :param verify: The key used to verify a signature. :type verify: OpenPGPKey @@ -549,7 +554,7 @@ class OpenPGPScheme(EncryptionScheme): verify.fingerprint != result.pubkey_fingerprint: raise errors.InvalidSignature( 'Failed to verify signature with key %s: %s' % - (verify.key_id, stderr)) + (verify.key_id, result.stderr)) # XXX: this is the encoding used by gpg module # https://github.com/isislovecruft/python-gnupg/\ @@ -612,7 +617,7 @@ class OpenPGPScheme(EncryptionScheme): if result.fingerprint is None: raise errors.SignFailed( 'Failed to sign with key %s: %s' % - (privkey['keyid'], stderr)) + (privkey['keyid'], result.stderr)) leap_assert( result.fingerprint == kfprint, 'Signature and private key fingerprints mismatch: ' -- cgit v1.2.3 From 4010fa3dca4ff6814a759793ce49fc76f896fe63 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 25 Nov 2013 16:35:27 -0200 Subject: Improve exceptions names and handling. --- .../feature_improve-exceptions-names-and-handling | 1 + keymanager/src/leap/keymanager/errors.py | 6 +- keymanager/src/leap/keymanager/openpgp.py | 70 +++++++++++++--------- 3 files changed, 47 insertions(+), 30 deletions(-) create mode 100644 keymanager/changes/feature_improve-exceptions-names-and-handling diff --git a/keymanager/changes/feature_improve-exceptions-names-and-handling b/keymanager/changes/feature_improve-exceptions-names-and-handling new file mode 100644 index 0000000..936232d --- /dev/null +++ b/keymanager/changes/feature_improve-exceptions-names-and-handling @@ -0,0 +1 @@ + o Improve exception names and handling. diff --git a/keymanager/src/leap/keymanager/errors.py b/keymanager/src/leap/keymanager/errors.py index 27180db..ebe4fd5 100644 --- a/keymanager/src/leap/keymanager/errors.py +++ b/keymanager/src/leap/keymanager/errors.py @@ -58,21 +58,21 @@ class InvalidSignature(Exception): pass -class EncryptionFailed(Exception): +class EncryptError(Exception): """ Raised upon failures of encryption. """ pass -class DecryptionFailed(Exception): +class DecryptError(Exception): """ Raised upon failures of decryption. """ pass -class EncryptionDecryptionFailed(Exception): +class GPGError(Exception): """ Raised upon failures of encryption/decryption. """ diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 5781d67..856b21e 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -471,14 +471,18 @@ class OpenPGPScheme(EncryptionScheme): def _assert_gpg_result_ok(result): """ Check if GPG result is 'ok' and log stderr outputs. - :param result: The GPG results - :type result: + + :param result: GPG results, which have a field calld 'ok' that states + whether the gpg operation was successful or not. + :type result: object + + :raise GPGError: Raised when the gpg operation was not successful. """ stderr = getattr(result, 'stderr', None) if stderr: logger.debug("%s" % (stderr,)) if getattr(result, 'ok', None) is not True: - raise errors.EncryptionDecryptionFailed( + raise errors.GPGError( 'Failed to encrypt/decrypt: %s' % stderr) def encrypt(self, data, pubkey, passphrase=None, sign=None, @@ -497,6 +501,8 @@ class OpenPGPScheme(EncryptionScheme): :return: The encrypted data. :rtype: str + + :raise EncryptError: Raised if failed encrypting for some reason. """ leap_assert_type(pubkey, OpenPGPKey) leap_assert(pubkey.private is False, 'Key is not public.') @@ -515,8 +521,12 @@ class OpenPGPScheme(EncryptionScheme): # in the ciphertext. # result.ok - (bool) indicates if the operation succeeded # result.data - (bool) contains the result of the operation - self._assert_gpg_result_ok(result) - return result.data + try: + self._assert_gpg_result_ok(result) + return result.data + except errors.GPGError as e: + logger.error('Failed to decrypt: %s.' % str(e)) + raise error.EncryptError() def decrypt(self, data, privkey, passphrase=None, verify=None): """ @@ -535,8 +545,9 @@ class OpenPGPScheme(EncryptionScheme): :return: The decrypted data. :rtype: unicode - @raise InvalidSignature: Raised if unable to verify the signature with - C{verify} key. + :raise DecryptError: Raised if failed decrypting for some reason. + :raise InvalidSignature: Raised if unable to verify the signature with + C{verify} key. """ leap_assert(privkey.private is True, 'Key is not private.') keys = [privkey] @@ -545,26 +556,31 @@ class OpenPGPScheme(EncryptionScheme): leap_assert(verify.private is False) keys.append(verify) with self._temporary_gpgwrapper(keys) as gpg: - result = gpg.decrypt( - data, passphrase=passphrase, always_trust=True) - self._assert_gpg_result_ok(result) - # verify signature - if (verify is not None): - if result.valid is False or \ - verify.fingerprint != result.pubkey_fingerprint: - raise errors.InvalidSignature( - 'Failed to verify signature with key %s: %s' % - (verify.key_id, result.stderr)) - - # XXX: this is the encoding used by gpg module - # https://github.com/isislovecruft/python-gnupg/\ - # blob/master/gnupg/_meta.py#L121 - encoding = locale.getpreferredencoding() - if encoding is None: - encoding = sys.stdin.encoding - if encoding is None: - encoding = 'utf-8' - return result.data.decode(encoding, 'replace') + try: + result = gpg.decrypt( + data, passphrase=passphrase, always_trust=True) + self._assert_gpg_result_ok(result) + # verify signature + if (verify is not None): + if result.valid is False or \ + verify.fingerprint != result.pubkey_fingerprint: + raise errors.InvalidSignature( + 'Failed to verify signature with key %s: %s' % + (verify.key_id, result.stderr)) + + # XXX: this is the encoding used by gpg module + # https://github.com/isislovecruft/python-gnupg/\ + # blob/master/gnupg/_meta.py#L121 + encoding = locale.getpreferredencoding() + if encoding is None: + encoding = sys.stdin.encoding + if encoding is None: + encoding = 'utf-8' + return result.data.decode(encoding, 'replace') + except errors.GPGError as e: + logger.error('Failed to decrypt: %s.' % str(e)) + raise errors.DecryptError(str(e)) + def is_encrypted(self, data): """ -- cgit v1.2.3 From fc3a2c88d57c5e663d01d6d78501d64278b8adac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 6 Dec 2013 15:33:25 -0300 Subject: Fold in changes --- keymanager/CHANGELOG | 7 +++++++ keymanager/changes/VERSION_COMPAT | 0 keymanager/changes/bug_fix-returns | 1 - keymanager/changes/email-checks | 3 --- keymanager/changes/feature_improve-exceptions-names-and-handling | 1 - 5 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 keymanager/changes/VERSION_COMPAT delete mode 100644 keymanager/changes/bug_fix-returns delete mode 100644 keymanager/changes/email-checks delete mode 100644 keymanager/changes/feature_improve-exceptions-names-and-handling diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index e20b86c..a081cc5 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,10 @@ +0.3.7 Dec 6: + o Fix error return values on openpgp backend. + o Remove address check when sending email and rely in the email + client to verify that is correct. Closes #4491. + o Support sending encrypted mails to addresses using the '+' sign. + o Improve exception names and handling. + 0.3.6 Nov 15: o Default encoding to 'utf-8' in case of system encodings not set. Closes #4427. diff --git a/keymanager/changes/VERSION_COMPAT b/keymanager/changes/VERSION_COMPAT new file mode 100644 index 0000000..e69de29 diff --git a/keymanager/changes/bug_fix-returns b/keymanager/changes/bug_fix-returns deleted file mode 100644 index 3bb7264..0000000 --- a/keymanager/changes/bug_fix-returns +++ /dev/null @@ -1 +0,0 @@ - o Fix error return values on openpgp backend. diff --git a/keymanager/changes/email-checks b/keymanager/changes/email-checks deleted file mode 100644 index eee7de7..0000000 --- a/keymanager/changes/email-checks +++ /dev/null @@ -1,3 +0,0 @@ - o Remove address check when sending email and rely in the email client to - verify that is correct. Closes #4491. - o Support sending encrypted mails to addresses using the '+' sign. diff --git a/keymanager/changes/feature_improve-exceptions-names-and-handling b/keymanager/changes/feature_improve-exceptions-names-and-handling deleted file mode 100644 index 936232d..0000000 --- a/keymanager/changes/feature_improve-exceptions-names-and-handling +++ /dev/null @@ -1 +0,0 @@ - o Improve exception names and handling. -- cgit v1.2.3 From 6a1e885b19a59c52240cf599a6db7a1748fb3956 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 9 Dec 2013 16:42:08 -0400 Subject: add with_venwrapper script --- keymanager/pkg/with_venvwrapper.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 keymanager/pkg/with_venvwrapper.sh diff --git a/keymanager/pkg/with_venvwrapper.sh b/keymanager/pkg/with_venvwrapper.sh new file mode 100755 index 0000000..2622978 --- /dev/null +++ b/keymanager/pkg/with_venvwrapper.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +#Wraps a command in a virtualenwrapper passed as first argument. +#Example: +#with_virtualenvwrapper.sh leap-bitmask ./run_tests.sh + +wd=`pwd` +source `which virtualenvwrapper.sh` +echo "Activating virtualenv " $1 +echo "------------------------------------" +workon $1 +cd $wd +echo "running version: " `pyver leap.keymanager` +echo "soledad version: " `pyver leap.soledad.common` +$2 $3 $4 $5 -- cgit v1.2.3 From 7b3df5168af160f5b70d8e2074c81b5a4b5438fb Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 9 Dec 2013 17:14:58 -0400 Subject: fix script path! --- keymanager/pkg/tools/with_venvwrapper.sh | 15 +++++++++++++++ keymanager/pkg/with_venvwrapper.sh | 15 --------------- 2 files changed, 15 insertions(+), 15 deletions(-) create mode 100755 keymanager/pkg/tools/with_venvwrapper.sh delete mode 100755 keymanager/pkg/with_venvwrapper.sh diff --git a/keymanager/pkg/tools/with_venvwrapper.sh b/keymanager/pkg/tools/with_venvwrapper.sh new file mode 100755 index 0000000..2622978 --- /dev/null +++ b/keymanager/pkg/tools/with_venvwrapper.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +#Wraps a command in a virtualenwrapper passed as first argument. +#Example: +#with_virtualenvwrapper.sh leap-bitmask ./run_tests.sh + +wd=`pwd` +source `which virtualenvwrapper.sh` +echo "Activating virtualenv " $1 +echo "------------------------------------" +workon $1 +cd $wd +echo "running version: " `pyver leap.keymanager` +echo "soledad version: " `pyver leap.soledad.common` +$2 $3 $4 $5 diff --git a/keymanager/pkg/with_venvwrapper.sh b/keymanager/pkg/with_venvwrapper.sh deleted file mode 100755 index 2622978..0000000 --- a/keymanager/pkg/with_venvwrapper.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -#Wraps a command in a virtualenwrapper passed as first argument. -#Example: -#with_virtualenvwrapper.sh leap-bitmask ./run_tests.sh - -wd=`pwd` -source `which virtualenvwrapper.sh` -echo "Activating virtualenv " $1 -echo "------------------------------------" -workon $1 -cd $wd -echo "running version: " `pyver leap.keymanager` -echo "soledad version: " `pyver leap.soledad.common` -$2 $3 $4 $5 -- cgit v1.2.3 From 90fd6f86e79e1a1abb93306a31f10c34b02ade8f Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 9 Dec 2013 17:19:39 -0400 Subject: pep8 cleanup --- keymanager/src/leap/keymanager/_version.py | 34 ++++++++++++++++++------------ keymanager/src/leap/keymanager/openpgp.py | 5 ----- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/keymanager/src/leap/keymanager/_version.py b/keymanager/src/leap/keymanager/_version.py index 3a514e1..28fca96 100644 --- a/keymanager/src/leap/keymanager/_version.py +++ b/keymanager/src/leap/keymanager/_version.py @@ -17,6 +17,7 @@ git_full = "$Format:%H$" import subprocess import sys + def run_command(args, cwd=None, verbose=False): try: # remember shell=False, so use git.cmd on windows, not just git @@ -37,10 +38,10 @@ def run_command(args, cwd=None, verbose=False): return stdout -import sys import re import os.path + def get_expanded_variables(versionfile_source): # the code embedded in _version.py can just fetch the value of these # variables. When used from setup.py, we don't want to import @@ -48,7 +49,7 @@ def get_expanded_variables(versionfile_source): # used from _version.py. variables = {} try: - f = open(versionfile_source,"r") + f = open(versionfile_source, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) @@ -63,12 +64,13 @@ def get_expanded_variables(versionfile_source): pass return variables + def versions_from_expanded_variables(variables, tag_prefix, verbose=False): refnames = variables["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("variables are unexpanded, not using") - return {} # unexpanded, so not in an unpacked git-archive tarball + return {} # unexpanded, so not in an unpacked git-archive tarball refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. @@ -93,13 +95,14 @@ def versions_from_expanded_variables(variables, tag_prefix, verbose=False): r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) - return { "version": r, - "full": variables["full"].strip() } + return {"version": r, + "full": variables["full"].strip()} # no suitable tags, so we use the full revision id if verbose: print("no suitable tags, using full revision id") - return { "version": variables["full"].strip(), - "full": variables["full"].strip() } + return {"version": variables["full"].strip(), + "full": variables["full"].strip()} + def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): # this runs 'git' from the root of the source tree. That either means @@ -116,7 +119,7 @@ def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): here = os.path.abspath(__file__) except NameError: # some py2exe/bbfreeze/non-CPython implementations don't do __file__ - return {} # not always correct + return {} # not always correct # versionfile_source is the relative path from the top of the source tree # (where the .git directory might live) to this file. Invert this to find @@ -141,7 +144,8 @@ def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): return {} if not stdout.startswith(tag_prefix): if verbose: - print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) + print("tag '%s' doesn't start with prefix '%s'" % + (stdout, tag_prefix)) return {} tag = stdout[len(tag_prefix):] stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) @@ -153,7 +157,8 @@ def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): return {"version": tag, "full": full} -def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): +def versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose=False): if IN_LONG_VERSION_PY: # We're running from _version.py. If it's from a source tree # (execute-in-place), we can work upwards to find the root of the @@ -163,7 +168,7 @@ def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False) here = os.path.abspath(__file__) except NameError: # py2exe/bbfreeze/non-CPython don't have __file__ - return {} # without __file__, we have no hope + return {} # without __file__, we have no hope # versionfile_source is the relative path from the top of the source # tree to _version.py. Invert this to find the root from __file__. root = here @@ -180,7 +185,8 @@ def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False) dirname = os.path.basename(root) if not dirname.startswith(parentdir_prefix): if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % + print("guessing rootdir is '%s', but '%s' doesn't start " + "with prefix '%s'" % (root, dirname, parentdir_prefix)) return None return {"version": dirname[len(parentdir_prefix):], "full": ""} @@ -189,8 +195,9 @@ tag_prefix = "" parentdir_prefix = "leap.keymanager-" versionfile_source = "src/leap/keymanager/_version.py" + def get_versions(default={"version": "unknown", "full": ""}, verbose=False): - variables = { "refnames": git_refnames, "full": git_full } + variables = {"refnames": git_refnames, "full": git_full} ver = versions_from_expanded_variables(variables, tag_prefix, verbose) if not ver: ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) @@ -200,4 +207,3 @@ def get_versions(default={"version": "unknown", "full": ""}, verbose=False): if not ver: ver = default return ver - diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 856b21e..1e424b6 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -14,13 +14,9 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - - """ Infrastructure for using OpenPGP keys in Key Manager. """ - - import locale import logging import os @@ -581,7 +577,6 @@ class OpenPGPScheme(EncryptionScheme): logger.error('Failed to decrypt: %s.' % str(e)) raise errors.DecryptError(str(e)) - def is_encrypted(self, data): """ Return whether C{data} was asymmetrically encrypted using OpenPGP. -- cgit v1.2.3 From 2a04ac69203e80be714daab438a7b9bb85459fd9 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 10 Dec 2013 16:54:25 -0400 Subject: typo --- keymanager/src/leap/keymanager/openpgp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 1e424b6..4276b19 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -294,7 +294,7 @@ class OpenPGPScheme(EncryptionScheme): @raise KeyNotFound: If the key was not found on local storage. """ # Remove the identity suffix after the '+' until the '@' - # e.g.: test_user+something@provider.com becomes test_user@probider.com + # e.g.: test_user+something@provider.com becomes test_user@provider.com # since the key belongs to the identity without the '+' suffix. address = re.sub(r'\+.*\@', '@', address) -- cgit v1.2.3 From 06868daa749cf6ab4fc8038676f55a4500f13ec2 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 17 Dec 2013 00:45:05 -0400 Subject: memoize call to get_key --- keymanager/changes/VERSION_COMPAT | 1 + keymanager/changes/feature_4784-memoize-get-key | 1 + keymanager/src/leap/keymanager/__init__.py | 7 +++++-- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 keymanager/changes/feature_4784-memoize-get-key diff --git a/keymanager/changes/VERSION_COMPAT b/keymanager/changes/VERSION_COMPAT index e69de29..dfe67ab 100644 --- a/keymanager/changes/VERSION_COMPAT +++ b/keymanager/changes/VERSION_COMPAT @@ -0,0 +1 @@ +leap.common >= 0.3.7 # memoize diff --git a/keymanager/changes/feature_4784-memoize-get-key b/keymanager/changes/feature_4784-memoize-get-key new file mode 100644 index 0000000..5284827 --- /dev/null +++ b/keymanager/changes/feature_4784-memoize-get-key @@ -0,0 +1 @@ + o Memoize call to get_key. Closes: #4784 diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 6cfbf71..5b1d8d9 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -46,6 +46,7 @@ import requests from leap.common.check import leap_assert, leap_assert_type from leap.common.events import signal from leap.common.events import events_pb2 as proto +from leap.common.decorators import memoized_method from leap.keymanager.errors import KeyNotFound @@ -250,6 +251,7 @@ class KeyManager(object): self._put(uri, data) signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address) + @memoized_method def get_key(self, address, ktype, private=False, fetch_remote=True): """ Return a key of type C{ktype} bound to C{address}. @@ -266,9 +268,10 @@ class KeyManager(object): :return: A key of type C{ktype} bound to C{address}. :rtype: EncryptionKey - @raise KeyNotFound: If the key was not found both locally and in - keyserver. + :raise KeyNotFound: If the key was not found both locally and in + keyserver. """ + logger.debug("getting key for %s" % (address,)) leap_assert( ktype in self._wrapper_map, 'Unkown key type: %s.' % str(ktype)) -- cgit v1.2.3 From 7e81556f983023e4ba259bc5ef360a940d6ca533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Tue, 17 Dec 2013 15:50:49 -0300 Subject: Use git.exe instead of git.cmd in windows since we use GitBash --- keymanager/versioneer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/keymanager/versioneer.py b/keymanager/versioneer.py index 34e4807..4e2c0a5 100644 --- a/keymanager/versioneer.py +++ b/keymanager/versioneer.py @@ -115,7 +115,7 @@ import sys def run_command(args, cwd=None, verbose=False): try: - # remember shell=False, so use git.cmd on windows, not just git + # remember shell=False, so use git.exe on windows, not just git p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) except EnvironmentError: e = sys.exc_info()[1] @@ -230,7 +230,7 @@ def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): GIT = "git" if sys.platform == "win32": - GIT = "git.cmd" + GIT = "git.exe" stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], cwd=root) if stdout is None: @@ -305,7 +305,7 @@ import sys def run_command(args, cwd=None, verbose=False): try: - # remember shell=False, so use git.cmd on windows, not just git + # remember shell=False, so use git.exe on windows, not just git p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) except EnvironmentError: e = sys.exc_info()[1] @@ -420,7 +420,7 @@ def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): GIT = "git" if sys.platform == "win32": - GIT = "git.cmd" + GIT = "git.exe" stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], cwd=root) if stdout is None: @@ -476,7 +476,7 @@ import sys def do_vcs_install(versionfile_source, ipy): GIT = "git" if sys.platform == "win32": - GIT = "git.cmd" + GIT = "git.exe" run_command([GIT, "add", "versioneer.py"]) run_command([GIT, "add", versionfile_source]) run_command([GIT, "add", ipy]) @@ -489,13 +489,13 @@ def do_vcs_install(versionfile_source, ipy): present = True f.close() except EnvironmentError: - pass + pass if not present: f = open(".gitattributes", "a+") f.write("%s export-subst\n" % versionfile_source) f.close() run_command([GIT, "add", ".gitattributes"]) - + SHORT_VERSION_PY = """ # This file was generated by 'versioneer.py' (0.7+) from -- cgit v1.2.3 From 6559c38b51c28245cdbb06641466defac607855b Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 18 Dec 2013 22:36:20 -0400 Subject: Do not memoize get_key, but a separate method. In this way we can choose which calls to get from cache and we do not mess with the call from send_key --- keymanager/src/leap/keymanager/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 5b1d8d9..aa4e2e7 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -252,6 +252,12 @@ class KeyManager(object): signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address) @memoized_method + def get_key_from_cache(self, *args, **kwargs): + """ + Public interface to `get_key`, that is memoized. + """ + return self.get_key(*args, **kwargs) + def get_key(self, address, ktype, private=False, fetch_remote=True): """ Return a key of type C{ktype} bound to C{address}. -- cgit v1.2.3 From cbaf583dc68661fe4f0cc9ee9aedde6d6ec0de9a Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 9 Jan 2014 16:53:19 -0300 Subject: Use a better version handler. --- keymanager/changes/feature-use-better-version-handler | 1 + keymanager/src/leap/keymanager/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 keymanager/changes/feature-use-better-version-handler diff --git a/keymanager/changes/feature-use-better-version-handler b/keymanager/changes/feature-use-better-version-handler new file mode 100644 index 0000000..57fabcc --- /dev/null +++ b/keymanager/changes/feature-use-better-version-handler @@ -0,0 +1 @@ + o Use a better version handler for the gnupg version check. diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index aa4e2e7..4edcdd0 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -23,9 +23,9 @@ import sys try: from gnupg.gnupg import GPGUtilities assert(GPGUtilities) # pyflakes happy - from gnupg import __version__ - from distutils.version import LooseVersion as V - assert(V(__version__) >= V('1.2.3')) + from gnupg import __version__ as _gnupg_version + from pkg_resources import parse_version + assert(parse_version(_gnupg_version) >= parse_version('1.2.3')) except (ImportError, AssertionError): print "*******" -- cgit v1.2.3 From 92c14ffdbcec74cb6c602fc82633a99e9b8bde40 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 17 Jan 2014 14:53:10 -0300 Subject: Do not decode decrypted data, return as str. --- keymanager/changes/do-not-decode-decrypted-data | 1 + keymanager/src/leap/keymanager/openpgp.py | 10 +--------- 2 files changed, 2 insertions(+), 9 deletions(-) create mode 100644 keymanager/changes/do-not-decode-decrypted-data diff --git a/keymanager/changes/do-not-decode-decrypted-data b/keymanager/changes/do-not-decode-decrypted-data new file mode 100644 index 0000000..a708be6 --- /dev/null +++ b/keymanager/changes/do-not-decode-decrypted-data @@ -0,0 +1 @@ + o Do not decode decrypted data, return as str. diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 4276b19..961e1ed 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -564,15 +564,7 @@ class OpenPGPScheme(EncryptionScheme): 'Failed to verify signature with key %s: %s' % (verify.key_id, result.stderr)) - # XXX: this is the encoding used by gpg module - # https://github.com/isislovecruft/python-gnupg/\ - # blob/master/gnupg/_meta.py#L121 - encoding = locale.getpreferredencoding() - if encoding is None: - encoding = sys.stdin.encoding - if encoding is None: - encoding = 'utf-8' - return result.data.decode(encoding, 'replace') + return result.data except errors.GPGError as e: logger.error('Failed to decrypt: %s.' % str(e)) raise errors.DecryptError(str(e)) -- cgit v1.2.3 From da9191e700895811b657a19148d1d3852e79d157 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 17 Jan 2014 15:37:00 -0300 Subject: Remove unused imports, fix typo. --- keymanager/src/leap/keymanager/openpgp.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 961e1ed..950d022 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -17,12 +17,10 @@ """ Infrastructure for using OpenPGP keys in Key Manager. """ -import locale import logging import os import re import shutil -import sys import tempfile from contextlib import closing @@ -522,7 +520,7 @@ class OpenPGPScheme(EncryptionScheme): return result.data except errors.GPGError as e: logger.error('Failed to decrypt: %s.' % str(e)) - raise error.EncryptError() + raise errors.EncryptError() def decrypt(self, data, privkey, passphrase=None, verify=None): """ -- cgit v1.2.3 From 9c4c725b35b66521d355a70ec0e211d43d4515c7 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 17 Jan 2014 17:47:06 -0300 Subject: Fix failing tests. Add missing MockSharedDB. Update Soledad parameters. --- keymanager/src/leap/keymanager/tests/test_keymanager.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 67676e9..e2558e4 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -118,9 +118,21 @@ class KeyManagerWithSoledadTestCase(BaseLeapTest): Soledad._get_secrets_from_shared_db = Mock(return_value=None) Soledad._put_secrets_in_shared_db = Mock(return_value=None) + class MockSharedDB(object): + + get_doc = Mock(return_value=None) + put_doc = Mock() + lock = Mock(return_value=('atoken', 300)) + unlock = Mock(return_value=True) + + def __call__(self): + return self + + Soledad._shared_db = MockSharedDB() + self._soledad = Soledad( - "leap@leap.se", - "123456", + u"leap@leap.se", + u"123456", secrets_path=self.tempdir + "/secret.gpg", local_db_path=self.tempdir + "/soledad.u1db", server_url='', -- cgit v1.2.3 From d25eafa5f29d3953e3b928afdd74301dec84b977 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 17 Feb 2014 17:01:29 -0300 Subject: Update auth to interact with webapp v2. Closes #5120. --- .../changes/feature_5120_update-auth-to-webapp-v2 | 1 + keymanager/src/leap/keymanager/__init__.py | 26 +++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 keymanager/changes/feature_5120_update-auth-to-webapp-v2 diff --git a/keymanager/changes/feature_5120_update-auth-to-webapp-v2 b/keymanager/changes/feature_5120_update-auth-to-webapp-v2 new file mode 100644 index 0000000..8a22f31 --- /dev/null +++ b/keymanager/changes/feature_5120_update-auth-to-webapp-v2 @@ -0,0 +1 @@ + o Update auth to interact with webapp v2 (#5120). diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 4edcdd0..dfe0415 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -77,7 +77,7 @@ class KeyManager(object): OPENPGP_KEY = 'openpgp' PUBKEY_KEY = "user[public_key]" - def __init__(self, address, nickserver_uri, soledad, session_id=None, + def __init__(self, address, nickserver_uri, soledad, token=None, ca_cert_path=None, api_uri=None, api_version=None, uid=None, gpgbinary=None): """ @@ -90,8 +90,8 @@ class KeyManager(object): :type url: str :param soledad: A Soledad instance for local storage of keys. :type soledad: leap.soledad.Soledad - :param session_id: The session ID for interacting with the webapp API. - :type session_id: str + :param token: The token for interacting with the webapp API. + :type token: str :param ca_cert_path: The path to the CA certificate. :type ca_cert_path: str :param api_uri: The URI of the webapp API. @@ -106,7 +106,7 @@ class KeyManager(object): self._address = address self._nickserver_uri = nickserver_uri self._soledad = soledad - self._session_id = session_id + self._token = token self.ca_cert_path = ca_cert_path self.api_uri = api_uri self.api_version = api_version @@ -180,11 +180,11 @@ class KeyManager(object): self._ca_cert_path is not None, 'We need the CA certificate path!') leap_assert( - self._session_id is not None, - 'We need a session_id to interact with webapp!') + self._token is not None, + 'We need a token to interact with webapp!') res = self._fetcher.put( uri, data=data, verify=self._ca_cert_path, - cookies={'_session_id': self._session_id}) + headers={'Authorization': 'Token token=%s' % self._token}) # assert that the response is valid res.raise_for_status() return res @@ -353,14 +353,14 @@ class KeyManager(object): # Setters/getters # - def _get_session_id(self): - return self._session_id + def _get_token(self): + return self._token - def _set_session_id(self, session_id): - self._session_id = session_id + def _set_token(self, token): + self._token = token - session_id = property( - _get_session_id, _set_session_id, doc='The session id.') + token = property( + _get_token, _set_token, doc='The session token.') def _get_ca_cert_path(self): return self._ca_cert_path -- cgit v1.2.3 From fae320b7e81036426f375f10832a38f160433238 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 31 Mar 2014 17:59:13 -0300 Subject: Properly raise exception when key is not found on server (#5415). --- keymanager/changes/bug_5415_properly-raise-KeyNotFound | 2 ++ keymanager/src/leap/keymanager/__init__.py | 18 +++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 keymanager/changes/bug_5415_properly-raise-KeyNotFound diff --git a/keymanager/changes/bug_5415_properly-raise-KeyNotFound b/keymanager/changes/bug_5415_properly-raise-KeyNotFound new file mode 100644 index 0000000..02e4905 --- /dev/null +++ b/keymanager/changes/bug_5415_properly-raise-KeyNotFound @@ -0,0 +1,2 @@ + o Properly raise KeyNotFound exception when looking for keys on nickserver + (#5415). diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index dfe0415..41f352e 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -197,21 +197,25 @@ class KeyManager(object): :param address: The address bound to the keys. :type address: str - @raise KeyNotFound: If the key was not found on nickserver. + :raise KeyNotFound: If the key was not found on nickserver. """ # request keys from the nickserver res = None try: res = self._get(self._nickserver_uri, {'address': address}) + res.raise_for_status() server_keys = res.json() # insert keys in local database if self.OPENPGP_KEY in server_keys: self._wrapper_map[OpenPGPKey].put_ascii_key( server_keys['openpgp']) + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + raise KeyNotFound(address) + logger.warning("HTTP error retrieving key: %r" % (e,)) + logger.warning("%s" % (res.content,)) except Exception as e: - logger.warning("Error retrieving the keys: %r" % (e,)) - if res: - logger.warning("%s" % (res.content,)) + logger.warning("Error retrieving key: %r" % (e,)) # # key management @@ -233,7 +237,7 @@ class KeyManager(object): :param ktype: The type of the key. :type ktype: KeyType - @raise KeyNotFound: If the key was not found in local database. + :raise KeyNotFound: If the key was not found in local database. """ leap_assert( ktype is OpenPGPKey, @@ -297,7 +301,7 @@ class KeyManager(object): raise signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address) - self._fetch_keys_from_server(address) + self._fetch_keys_from_server(address) # might raise KeyNotFound key = self._wrapper_map[ktype].get_key(address, private=False) signal(proto.KEYMANAGER_KEY_FOUND, address) @@ -443,7 +447,7 @@ class KeyManager(object): :return: The decrypted data. :rtype: str - @raise InvalidSignature: Raised if unable to verify the signature with + :raise InvalidSignature: Raised if unable to verify the signature with C{verify} key. """ leap_assert_type(privkey, EncryptionKey) -- cgit v1.2.3 From b417ed10223dcf234a05cfe63834b89d2eae67ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 4 Apr 2014 16:21:47 -0300 Subject: Update dependencies --- keymanager/changes/VERSION_COMPAT | 11 ++++++++++- keymanager/pkg/requirements.pip | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/keymanager/changes/VERSION_COMPAT b/keymanager/changes/VERSION_COMPAT index dfe67ab..cc00ecf 100644 --- a/keymanager/changes/VERSION_COMPAT +++ b/keymanager/changes/VERSION_COMPAT @@ -1 +1,10 @@ -leap.common >= 0.3.7 # memoize +################################################# +# This file keeps track of the recent changes +# introduced in internal leap dependencies. +# Add your changes here so we can properly update +# requirements.pip during the release process. +# (leave header when resetting) +################################################# +# +# BEGIN DEPENDENCY LIST ------------------------- +# leap.foo.bar>=x.y.z diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index 1515204..8dd84bf 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -1,4 +1,4 @@ -leap.common>=0.3.0 +leap.common>=0.3.7 simplejson requests # if we bump the gnupg version, bump also the sanity check -- cgit v1.2.3 From 134173211a259d28934e47135ee10c26669c3ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Fri, 4 Apr 2014 16:23:39 -0300 Subject: Fold in changes --- keymanager/CHANGELOG | 10 ++++++++++ keymanager/changes/bug_5415_properly-raise-KeyNotFound | 2 -- keymanager/changes/do-not-decode-decrypted-data | 1 - keymanager/changes/feature-use-better-version-handler | 1 - keymanager/changes/feature_4784-memoize-get-key | 1 - keymanager/changes/feature_5120_update-auth-to-webapp-v2 | 1 - 6 files changed, 10 insertions(+), 6 deletions(-) delete mode 100644 keymanager/changes/bug_5415_properly-raise-KeyNotFound delete mode 100644 keymanager/changes/do-not-decode-decrypted-data delete mode 100644 keymanager/changes/feature-use-better-version-handler delete mode 100644 keymanager/changes/feature_4784-memoize-get-key delete mode 100644 keymanager/changes/feature_5120_update-auth-to-webapp-v2 diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index a081cc5..6b24e5b 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,13 @@ +0.3.8 Apr 4: + o Properly raise KeyNotFound exception when looking for keys on + nickserver. Fixes #5415. + o Do not decode decrypted data, return as str. + o Use a better version handler for the gnupg version check. + o Memoize call to get_key. Closes #4784. + o Update auth to interact with webapp v2. Fixes #5120. + +-- 2014 -- + 0.3.7 Dec 6: o Fix error return values on openpgp backend. o Remove address check when sending email and rely in the email diff --git a/keymanager/changes/bug_5415_properly-raise-KeyNotFound b/keymanager/changes/bug_5415_properly-raise-KeyNotFound deleted file mode 100644 index 02e4905..0000000 --- a/keymanager/changes/bug_5415_properly-raise-KeyNotFound +++ /dev/null @@ -1,2 +0,0 @@ - o Properly raise KeyNotFound exception when looking for keys on nickserver - (#5415). diff --git a/keymanager/changes/do-not-decode-decrypted-data b/keymanager/changes/do-not-decode-decrypted-data deleted file mode 100644 index a708be6..0000000 --- a/keymanager/changes/do-not-decode-decrypted-data +++ /dev/null @@ -1 +0,0 @@ - o Do not decode decrypted data, return as str. diff --git a/keymanager/changes/feature-use-better-version-handler b/keymanager/changes/feature-use-better-version-handler deleted file mode 100644 index 57fabcc..0000000 --- a/keymanager/changes/feature-use-better-version-handler +++ /dev/null @@ -1 +0,0 @@ - o Use a better version handler for the gnupg version check. diff --git a/keymanager/changes/feature_4784-memoize-get-key b/keymanager/changes/feature_4784-memoize-get-key deleted file mode 100644 index 5284827..0000000 --- a/keymanager/changes/feature_4784-memoize-get-key +++ /dev/null @@ -1 +0,0 @@ - o Memoize call to get_key. Closes: #4784 diff --git a/keymanager/changes/feature_5120_update-auth-to-webapp-v2 b/keymanager/changes/feature_5120_update-auth-to-webapp-v2 deleted file mode 100644 index 8a22f31..0000000 --- a/keymanager/changes/feature_5120_update-auth-to-webapp-v2 +++ /dev/null @@ -1 +0,0 @@ - o Update auth to interact with webapp v2 (#5120). -- cgit v1.2.3 From 4fe04de3bd69cab6a236bd87086a7de9ab45a66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Touceda?= Date: Wed, 9 Apr 2014 15:22:03 -0300 Subject: Improve changelog readability --- keymanager/CHANGELOG | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index 6b24e5b..fc8333b 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,4 +1,4 @@ -0.3.8 Apr 4: +0.3.8 Apr 4, 2014: o Properly raise KeyNotFound exception when looking for keys on nickserver. Fixes #5415. o Do not decode decrypted data, return as str. @@ -6,16 +6,14 @@ o Memoize call to get_key. Closes #4784. o Update auth to interact with webapp v2. Fixes #5120. --- 2014 -- - -0.3.7 Dec 6: +0.3.7 Dec 6, 2013: o Fix error return values on openpgp backend. o Remove address check when sending email and rely in the email client to verify that is correct. Closes #4491. o Support sending encrypted mails to addresses using the '+' sign. o Improve exception names and handling. -0.3.6 Nov 15: +0.3.6 Nov 15, 2013: o Default encoding to 'utf-8' in case of system encodings not set. Closes #4427. o Add verification of detached signatures. Closes #4375. @@ -23,37 +21,37 @@ o Expose openpgp methods in keymanager (parse_ascii_keys, put_key, delete_key). -0.3.5 Nov 1: +0.3.5 Nov 1, 2013: o Return unicode decrypted text to avoid encoding issues. Related to #4000. -0.3.4 Oct 18: +0.3.4 Oct 18, 2013: o Add option to choose cipher and digest algorithms when signing and encrypting. Closes #4030. -0.3.3 Oct 4: +0.3.3 Oct 4, 2013: o Add a sanity check for the correct version of gnupg. o Update code to use gnupg 1.2.2 python module. Closes #2342. -0.3.2 Sep 6: +0.3.2 Sep 6, 2013: o Do not raise exception when a GET request doesn't return 2XX code. Nickserver uses error codes for more verbosity in the result. o Accept unicode ascii keys along with str. -0.3.1 Aug 23: +0.3.1 Aug 23, 2013: o Signal different key related events, like key generation, key upload. o Update to new soledad package scheme (common, client and server). Closes #3487. o Packaging improvements: add versioneer and parse_requirements. -0.3.0 Aug 9: +0.3.0 Aug 9, 2013: o If a nickserver request fails in any way, notify and continue. o Options parameter in gnupg.GPG isn't supported by all versions, so removing it for the time being. o Add support for bundled gpg. Closes #3397. o Refactor API to include encrypt/decrypt/sign/verify in KeyManager. -0.2.0 Jul 12: +0.2.0 Jul 12, 2013: o Move keymanager to its own package -- cgit v1.2.3 From 93bd48dd8855e4b4c7ebccb0148fd09c4a347f65 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 25 Jul 2014 19:26:19 -0500 Subject: gpg.verify_file() gets the data as a filename not as a binary stream --- keymanager/src/leap/keymanager/openpgp.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 950d022..46ae2aa 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -649,13 +649,17 @@ class OpenPGPScheme(EncryptionScheme): result = gpg.verify(data) else: # to verify using a detached sig we have to use - # gpg.verify_file(), which receives the data as a binary - # stream and the name of a file containing the signature. + # gpg.verify_file(), which receives the name of + # files containing the date and the signature. sf, sfname = tempfile.mkstemp() with os.fdopen(sf, 'w') as sfd: sfd.write(detached_sig) - with closing(_make_binary_stream(data, gpg._encoding)) as df: - result = gpg.verify_file(df, sig_file=sfname) + df, dfname = tempfile.mkstemp() + with os.fdopen(df, 'w') as sdd: + sdd.write(data) + result = gpg.verify_file(dfname, sig_file=sfname) + os.unlink(sfname) + os.unlink(dfname) gpgpubkey = gpg.list_keys().pop() valid = result.valid rfprint = result.fingerprint -- cgit v1.2.3 From 008328cc0ad2738109472869f3644100ca85859d Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 25 Jul 2014 19:28:15 -0500 Subject: Fix test_send_key --- keymanager/src/leap/keymanager/tests/test_keymanager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index e2558e4..f89dcd9 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -147,8 +147,8 @@ class KeyManagerWithSoledadTestCase(BaseLeapTest): for key in km.get_all_keys_in_local_db(private=True): km._wrapper_map[key.__class__].delete_key(key) - def _key_manager(self, user=ADDRESS, url=''): - return KeyManager(user, url, self._soledad, + def _key_manager(self, user=ADDRESS, url='', token=None): + return KeyManager(user, url, self._soledad, token=token, gpgbinary=GPG_BINARY_PATH) @@ -387,7 +387,8 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): """ Test that request is well formed when sending keys to server. """ - km = self._key_manager() + token = "mytoken" + km = self._key_manager(token=token) km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY) km._fetcher.put = Mock() # the following data will be used on the send @@ -404,7 +405,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): url = '%s/%s/users/%s.json' % ('apiuri', 'apiver', 'myuid') km._fetcher.put.assert_called_once_with( url, data=data, verify='capath', - cookies={'_session_id': 'sessionid'}, + headers={'Authorization': 'Token token=%s' % token}, ) def test__fetch_keys_from_server(self): -- cgit v1.2.3 From d3f7c6df9ddde1e2a6311612fbe37efd9b5ddb56 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 27 Aug 2014 12:59:28 -0300 Subject: Fix call to python-gnupg verify_file() method (#6022). --- keymanager/changes/bug_6022_fix-call-to-verify-file | 1 + keymanager/src/leap/keymanager/openpgp.py | 13 ++++--------- 2 files changed, 5 insertions(+), 9 deletions(-) create mode 100644 keymanager/changes/bug_6022_fix-call-to-verify-file diff --git a/keymanager/changes/bug_6022_fix-call-to-verify-file b/keymanager/changes/bug_6022_fix-call-to-verify-file new file mode 100644 index 0000000..121e25a --- /dev/null +++ b/keymanager/changes/bug_6022_fix-call-to-verify-file @@ -0,0 +1 @@ + o Fix call to python-gnupg's verify_file() method (#6022). diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 46ae2aa..ee37a34 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -22,12 +22,11 @@ import os import re import shutil import tempfile +import io -from contextlib import closing from gnupg import GPG from gnupg.gnupg import GPGUtilities -from gnupg._util import _make_binary_stream from leap.common.check import leap_assert, leap_assert_type, leap_check from leap.keymanager import errors @@ -649,17 +648,13 @@ class OpenPGPScheme(EncryptionScheme): result = gpg.verify(data) else: # to verify using a detached sig we have to use - # gpg.verify_file(), which receives the name of - # files containing the date and the signature. + # gpg.verify_file(), which receives the data as a binary + # stream and the name of a file containing the signature. sf, sfname = tempfile.mkstemp() with os.fdopen(sf, 'w') as sfd: sfd.write(detached_sig) - df, dfname = tempfile.mkstemp() - with os.fdopen(df, 'w') as sdd: - sdd.write(data) - result = gpg.verify_file(dfname, sig_file=sfname) + result = gpg.verify_file(io.BytesIO(data), sig_file=sfname) os.unlink(sfname) - os.unlink(dfname) gpgpubkey = gpg.list_keys().pop() valid = result.valid rfprint = result.fingerprint -- cgit v1.2.3 From 792f8236e2fcdb8ba14867db2c1a527a2e5adb4b Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 15 Sep 2014 14:07:26 -0500 Subject: Update docstrings of the public API --- keymanager/src/leap/keymanager/__init__.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 41f352e..65352a8 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -82,12 +82,12 @@ class KeyManager(object): gpgbinary=None): """ Initialize a Key Manager for user's C{address} with provider's - nickserver reachable in C{url}. + nickserver reachable in C{nickserver_uri}. - :param address: The address of the user of this Key Manager. + :param address: The email address of the user of this Key Manager. :type address: str - :param url: The URL of the nickserver. - :type url: str + :param nickserver_uri: The URI of the nickserver. + :type nickserver_uri: str :param soledad: A Soledad instance for local storage of keys. :type soledad: leap.soledad.Soledad :param token: The token for interacting with the webapp API. @@ -98,7 +98,7 @@ class KeyManager(object): :type api_uri: str :param api_version: The version of the webapp API. :type api_version: str - :param uid: The users' UID. + :param uid: The user's UID. :type uid: str :param gpgbinary: Name for GnuPG binary executable. :type gpgbinary: C{str} @@ -228,12 +228,6 @@ class KeyManager(object): Public key bound to user's is sent to provider, which will sign it and replace any prior keys for the same address in its database. - If C{send_private} is True, then the private key is encrypted with - C{password} and sent to server in the same request, together with a - hash string of user's address and password. The encrypted private key - will be saved in the server in a way it is publicly retrievable - through the hash string. - :param ktype: The type of the key. :type ktype: KeyType @@ -275,6 +269,9 @@ class KeyManager(object): :type ktype: KeyType :param private: Look for a private key instead of a public one? :type private: bool + :param fetch_remote: If key not found in local storage try to fetch + from nickserver + :type fetch_remote: bool :return: A key of type C{ktype} bound to C{address}. :rtype: EncryptionKey @@ -311,6 +308,9 @@ class KeyManager(object): """ Return all keys stored in local database. + :param private: Include private keys + :type private: bool + :return: A list with all keys in local db. :rtype: list """ @@ -416,6 +416,9 @@ class KeyManager(object): :type data: str :param pubkey: The key used to encrypt. :type pubkey: EncryptionKey + :param passphrase: The passphrase for the secret key used for the + signature. + :type passphrase: str :param sign: The key used for signing. :type sign: EncryptionKey :param cipher_algo: The cipher algorithm to use. @@ -448,7 +451,7 @@ class KeyManager(object): :rtype: str :raise InvalidSignature: Raised if unable to verify the signature with - C{verify} key. + C{verify} key. """ leap_assert_type(privkey, EncryptionKey) leap_assert( -- cgit v1.2.3 From 7f36a336362a19fe5a426a08863ff4b05b003e31 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 15 Sep 2014 14:07:58 -0500 Subject: Remove refresh_keys Never should be done a bulk upload of keys. The updating of the keys should not be a task for the user of the keymanager. Keys will be updated by the keymanager in a background worker one per one. --- keymanager/src/leap/keymanager/__init__.py | 13 ------------- keymanager/src/leap/keymanager/tests/test_keymanager.py | 17 ----------------- 2 files changed, 30 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 65352a8..4563d80 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -324,19 +324,6 @@ class KeyManager(object): KEYMANAGER_KEY_TAG, '1' if private else '0')) - def refresh_keys(self): - """ - Fetch keys from nickserver and update them locally. - """ - addresses = set(map( - lambda doc: doc.address, - self.get_all_keys_in_local_db(private=False))) - for address in addresses: - # do not attempt to refresh our own key - if address == self._address: - continue - self._fetch_keys_from_server(address) - def gen_key(self, ktype): """ Generate a key of type C{ktype} bound to the user's address. diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index f89dcd9..932f260 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -437,23 +437,6 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): verify='cacertpath', ) - def test_refresh_keys_does_not_refresh_own_key(self): - """ - Test that refreshing keys will not attempt to refresh our own key. - """ - km = self._key_manager() - # we add 2 keys but we expect it to only refresh the second one. - km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY) - km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY_2) - # mock the key fetching - km._fetch_keys_from_server = Mock(return_value=[]) - km.ca_cert_path = '' # some bogus path so the km does not complain. - # do the refreshing - km.refresh_keys() - km._fetch_keys_from_server.assert_called_once_with( - ADDRESS_2 - ) - def test_get_key_fetches_from_server(self): """ Test that getting a key successfuly fetches from server. -- cgit v1.2.3 From 936666b5a2c12c0d9733fb5ffcecd7f512c6b660 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 15 Sep 2014 14:13:49 -0500 Subject: Pass the cipher_algo to the encrypt wrapper --- keymanager/src/leap/keymanager/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 4563d80..fe9f02c 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -418,7 +418,7 @@ class KeyManager(object): leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.') leap_assert(pubkey.private is False, 'Key is not public.') return self._wrapper_map[pubkey.__class__].encrypt( - data, pubkey, passphrase, sign) + data, pubkey, passphrase, sign, cipher_algo=cipher_algo) def decrypt(self, data, privkey, passphrase=None, verify=None): """ -- cgit v1.2.3 From c6fcde7d333d1769dbdfc1c8be255378a20895d6 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Tue, 16 Sep 2014 10:55:16 -0500 Subject: Remove get_key_from_cache We only need to cache the fetch with a sort timeout. The tests that fetches keys now have to use different keys or will be cached. --- keymanager/src/leap/keymanager/__init__.py | 8 +------- keymanager/src/leap/keymanager/tests/test_keymanager.py | 8 ++++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index fe9f02c..37eb223 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -189,6 +189,7 @@ class KeyManager(object): res.raise_for_status() return res + @memoized_method(invalidation=300) def _fetch_keys_from_server(self, address): """ Fetch keys bound to C{address} from nickserver and insert them in @@ -249,13 +250,6 @@ class KeyManager(object): self._put(uri, data) signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address) - @memoized_method - def get_key_from_cache(self, *args, **kwargs): - """ - Public interface to `get_key`, that is memoized. - """ - return self.get_key(*args, **kwargs) - def get_key(self, address, ktype, private=False, fetch_remote=True): """ Return a key of type C{ktype} bound to C{address}. diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 932f260..f9ba04f 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -448,7 +448,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): headers = {'content-type': 'application/json'} def json(self): - return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2} + return {'address': ADDRESS, 'openpgp': PUBLIC_KEY} def raise_for_status(self): pass @@ -458,13 +458,13 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km.ca_cert_path = 'cacertpath' # try to key get without fetching from server self.assertRaises( - KeyNotFound, km.get_key, ADDRESS_2, OpenPGPKey, + KeyNotFound, km.get_key, ADDRESS, OpenPGPKey, fetch_remote=False ) # try to get key fetching from server. - key = km.get_key(ADDRESS_2, OpenPGPKey) + key = km.get_key(ADDRESS, OpenPGPKey) self.assertIsInstance(key, OpenPGPKey) - self.assertEqual(ADDRESS_2, key.address) + self.assertEqual(ADDRESS, key.address) class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): -- cgit v1.2.3 From ac0432fd35d5325110c96db065ef6eb6257a8d31 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Tue, 16 Sep 2014 11:01:43 -0500 Subject: Rename method get_all_keys_in_local_db -> get_all_keys --- keymanager/src/leap/keymanager/__init__.py | 2 +- keymanager/src/leap/keymanager/tests/test_keymanager.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 37eb223..3888c0a 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -298,7 +298,7 @@ class KeyManager(object): return key - def get_all_keys_in_local_db(self, private=False): + def get_all_keys(self, private=False): """ Return all keys stored in local database. diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index f9ba04f..7192bfb 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -142,9 +142,9 @@ class KeyManagerWithSoledadTestCase(BaseLeapTest): def tearDown(self): km = self._key_manager() - for key in km.get_all_keys_in_local_db(): + for key in km.get_all_keys(): km._wrapper_map[key.__class__].delete_key(key) - for key in km.get_all_keys_in_local_db(private=True): + for key in km.get_all_keys(private=True): km._wrapper_map[key.__class__].delete_key(key) def _key_manager(self, user=ADDRESS, url='', token=None): @@ -343,12 +343,12 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km = self._key_manager() km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) # get public keys - keys = km.get_all_keys_in_local_db(False) + keys = km.get_all_keys(False) self.assertEqual(len(keys), 1, 'Wrong number of keys') self.assertEqual(ADDRESS, keys[0].address) self.assertFalse(keys[0].private) # get private keys - keys = km.get_all_keys_in_local_db(True) + keys = km.get_all_keys(True) self.assertEqual(len(keys), 1, 'Wrong number of keys') self.assertEqual(ADDRESS, keys[0].address) self.assertTrue(keys[0].private) -- cgit v1.2.3 From 6053b7f6689a4a168c2b394db456bb8c72e516f7 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Tue, 16 Sep 2014 11:27:40 -0500 Subject: Remove parse_openpgp_ascii_key Don't fit with the logic of the keymanager and it's not use except for some commented code in bitmask_client --- keymanager/src/leap/keymanager/__init__.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 3888c0a..bd85c2d 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -495,20 +495,6 @@ class KeyManager(object): return self._wrapper_map[pubkey.__class__].verify( data, pubkey, detached_sig=detached_sig) - def parse_openpgp_ascii_key(self, key_data): - """ - Parses an ascii armored key (or key pair) data and returns - the OpenPGPKey keys. - - :param key_data: the key data to be parsed. - :type key_data: str or unicode - - :returns: the public key and private key (if applies) for that data. - :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey) - the tuple may have one or both components None - """ - return self._wrapper_map[OpenPGPKey].parse_ascii_key(key_data) - def delete_key(self, key): """ Remove C{key} from storage. -- cgit v1.2.3 From 1fba43384663eda46e41fc55a4d613ca612fef9e Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Tue, 16 Sep 2014 12:32:21 -0500 Subject: Add changes --- keymanager/changes/feature-cleanup_api | 1 + 1 file changed, 1 insertion(+) create mode 100644 keymanager/changes/feature-cleanup_api diff --git a/keymanager/changes/feature-cleanup_api b/keymanager/changes/feature-cleanup_api new file mode 100644 index 0000000..cc7d5d6 --- /dev/null +++ b/keymanager/changes/feature-cleanup_api @@ -0,0 +1 @@ +- Clean up API -- cgit v1.2.3 From 8a59a0f6f861aed9681bc675bfcdcbfec959e194 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 6 Oct 2014 11:31:59 -0500 Subject: Add new leap.common version to VERSION_COMPAT --- keymanager/changes/VERSION_COMPAT | 1 + 1 file changed, 1 insertion(+) diff --git a/keymanager/changes/VERSION_COMPAT b/keymanager/changes/VERSION_COMPAT index cc00ecf..ac65675 100644 --- a/keymanager/changes/VERSION_COMPAT +++ b/keymanager/changes/VERSION_COMPAT @@ -8,3 +8,4 @@ # # BEGIN DEPENDENCY LIST ------------------------- # leap.foo.bar>=x.y.z +leap.common>=0.3.10 -- cgit v1.2.3 From ca81004d13ef48206b860cf4ff70e8bbdfbc61af Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Wed, 8 Oct 2014 19:28:33 -0500 Subject: Accept ascii keys on put_key --- keymanager/changes/feature-put_key_ascii | 1 + keymanager/src/leap/keymanager/__init__.py | 9 ++++++--- keymanager/src/leap/keymanager/tests/test_keymanager.py | 11 +++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 keymanager/changes/feature-put_key_ascii diff --git a/keymanager/changes/feature-put_key_ascii b/keymanager/changes/feature-put_key_ascii new file mode 100644 index 0000000..78f2907 --- /dev/null +++ b/keymanager/changes/feature-put_key_ascii @@ -0,0 +1 @@ +- KeyManager.put_key now accepts also ascii keys diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index bd85c2d..da679ac 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -515,11 +515,14 @@ class KeyManager(object): """ Put C{key} in local storage. - :param key: The key to be stored. - :type key: OpenPGPKey + :param key: The key to be stored. It can be ascii key or an OpenPGPKey + :type key: str or OpenPGPKey """ try: - self._wrapper_map[type(key)].put_key(key) + if isinstance(key, basestring): + self._wrapper_map[OpenPGPKey].put_ascii_key(key) + else: + self._wrapper_map[type(key)].put_key(key) except IndexError as e: leap_assert(False, "Unsupported key type. Error {0!r}".format(e)) diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 7192bfb..39b729d 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -466,6 +466,17 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): self.assertIsInstance(key, OpenPGPKey) self.assertEqual(ADDRESS, key.address) + def test_put_key_ascii(self): + """ + Test that putting ascii key works + """ + km = self._key_manager(url='http://nickserver.domain') + + km.put_key(PUBLIC_KEY) + key = km.get_key(ADDRESS, OpenPGPKey) + self.assertIsInstance(key, OpenPGPKey) + self.assertEqual(ADDRESS, key.address) + class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): -- cgit v1.2.3 From 6e65c0fb2a36182a45ec7851b8404c406910589f Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 1 Aug 2014 10:20:27 -0500 Subject: Implement 'fetch_key' for ascii keys binary keys support is still missing --- keymanager/changes/feature-5932_fetch_keys_uri | 1 + keymanager/src/leap/keymanager/__init__.py | 31 +++++++++++++- keymanager/src/leap/keymanager/openpgp.py | 5 ++- .../src/leap/keymanager/tests/test_keymanager.py | 50 ++++++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 keymanager/changes/feature-5932_fetch_keys_uri diff --git a/keymanager/changes/feature-5932_fetch_keys_uri b/keymanager/changes/feature-5932_fetch_keys_uri new file mode 100644 index 0000000..6f628a6 --- /dev/null +++ b/keymanager/changes/feature-5932_fetch_keys_uri @@ -0,0 +1 @@ +- Add 'fetch_key' method to fetch keys from a URI (closes #5932) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index da679ac..c3423d9 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -48,7 +48,7 @@ from leap.common.events import signal from leap.common.events import events_pb2 as proto from leap.common.decorators import memoized_method -from leap.keymanager.errors import KeyNotFound +from leap.keymanager.errors import KeyNotFound, KeyAttributesDiffer from leap.keymanager.keys import ( EncryptionKey, @@ -526,6 +526,35 @@ class KeyManager(object): except IndexError as e: leap_assert(False, "Unsupported key type. Error {0!r}".format(e)) + def fetch_key(self, address, uri, ktype): + """ + Fetch a public key for C{address} from the network and put it in + local storage. + + Raises C{openpgp.errors.KeyNotFound} if not valid key on C{uri}. + Raises C{openpgp.errors.KeyAttributesDiffer} if address don't match + any uid on the key. + + :param address: The email address of the key. + :type address: str + :param uri: The URI of the key. + :type uri: str + :param ktype: The type of the key. + :type ktype: KeyType + """ + res = self._get(uri) + if not res.ok: + raise KeyNotFound(uri) + + # XXX parse binary keys + pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(res.content) + if pubkey is None: + raise KeyNotFound(uri) + if pubkey.address != address: + raise KeyAttributesDiffer("UID %s found, but expected %s" + % (pubkey.address, address)) + self.put_key(pubkey) + from ._version import get_versions __version__ = get_versions()['version'] del get_versions diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index ee37a34..6a825cd 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -327,7 +327,10 @@ class OpenPGPScheme(EncryptionScheme): privkey = gpg.list_keys(secret=True).pop() except IndexError: pass - pubkey = gpg.list_keys(secret=False).pop() # unitary keyring + try: + pubkey = gpg.list_keys(secret=False).pop() # unitary keyring + except IndexError: + return (None, None) # extract adress from first uid on key match = re.match(mail_regex, pubkey['uids'].pop()) diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 39b729d..65f8f39 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -28,6 +28,7 @@ from leap.keymanager import ( KeyManager, openpgp, KeyNotFound, + KeyAttributesDiffer, errors, ) from leap.keymanager.openpgp import OpenPGPKey @@ -477,6 +478,55 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): self.assertIsInstance(key, OpenPGPKey) self.assertEqual(ADDRESS, key.address) + def test_fetch_uri_ascii_key(self): + """ + Test that fetch key downloads the ascii key and gets included in + the local storage + """ + km = self._key_manager() + + class Response(object): + ok = True + content = PUBLIC_KEY + + km._fetcher.get = Mock(return_value=Response()) + km.ca_cert_path = 'cacertpath' + + km.fetch_key(ADDRESS, "http://site.domain/key", OpenPGPKey) + key = km.get_key(ADDRESS, OpenPGPKey) + self.assertEqual(KEY_FINGERPRINT, key.fingerprint) + + def test_fetch_uri_empty_key(self): + """ + Test that fetch key raises KeyNotFound if no key in the url + """ + km = self._key_manager() + + class Response(object): + ok = True + content = "" + + km._fetcher.get = Mock(return_value=Response()) + km.ca_cert_path = 'cacertpath' + self.assertRaises(KeyNotFound, km.fetch_key, + ADDRESS, "http://site.domain/key", OpenPGPKey) + + def test_fetch_uri_address_differ(self): + """ + Test that fetch key raises KeyAttributesDiffer if the address + don't match + """ + km = self._key_manager() + + class Response(object): + ok = True + content = PUBLIC_KEY + + km._fetcher.get = Mock(return_value=Response()) + km.ca_cert_path = 'cacertpath' + self.assertRaises(KeyAttributesDiffer, km.fetch_key, + ADDRESS_2, "http://site.domain/key", OpenPGPKey) + class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): -- cgit v1.2.3 From 66d4a0de5d405ba761943f83d2b778b9ee63707b Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Sun, 12 Oct 2014 03:21:47 -0500 Subject: Basic validation levels support --- keymanager/src/leap/keymanager/__init__.py | 96 +++++++-- keymanager/src/leap/keymanager/errors.py | 6 + keymanager/src/leap/keymanager/keys.py | 16 +- keymanager/src/leap/keymanager/openpgp.py | 3 +- keymanager/src/leap/keymanager/tests/__init__.py | 236 +++++++++++++++++++++ .../src/leap/keymanager/tests/test_keymanager.py | 234 ++------------------ .../src/leap/keymanager/tests/test_validation.py | 150 +++++++++++++ keymanager/src/leap/keymanager/validation.py | 94 ++++++++ 8 files changed, 594 insertions(+), 241 deletions(-) create mode 100644 keymanager/src/leap/keymanager/tests/test_validation.py create mode 100644 keymanager/src/leap/keymanager/validation.py diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index c3423d9..156aaf8 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -48,7 +48,12 @@ from leap.common.events import signal from leap.common.events import events_pb2 as proto from leap.common.decorators import memoized_method -from leap.keymanager.errors import KeyNotFound, KeyAttributesDiffer +from leap.keymanager.errors import ( + KeyNotFound, + KeyAddressMismatch, + KeyNotValidUpgrade +) +from leap.keymanager.validation import ValidationLevel, can_upgrade from leap.keymanager.keys import ( EncryptionKey, @@ -208,8 +213,11 @@ class KeyManager(object): server_keys = res.json() # insert keys in local database if self.OPENPGP_KEY in server_keys: - self._wrapper_map[OpenPGPKey].put_ascii_key( - server_keys['openpgp']) + self.put_raw_key( + server_keys['openpgp'], + OpenPGPKey, + address=address, + validation=ValidationLevel.Provider_Trust) except requests.exceptions.HTTPError as e: if e.response.status_code == 404: raise KeyNotFound(address) @@ -230,7 +238,7 @@ class KeyManager(object): replace any prior keys for the same address in its database. :param ktype: The type of the key. - :type ktype: KeyType + :type ktype: subclass of EncryptionKey :raise KeyNotFound: If the key was not found in local database. """ @@ -260,7 +268,7 @@ class KeyManager(object): :param address: The address bound to the key. :type address: str :param ktype: The type of the key. - :type ktype: KeyType + :type ktype: subclass of EncryptionKey :param private: Look for a private key instead of a public one? :type private: bool :param fetch_remote: If key not found in local storage try to fetch @@ -323,7 +331,7 @@ class KeyManager(object): Generate a key of type C{ktype} bound to the user's address. :param ktype: The type of the key. - :type ktype: KeyType + :type ktype: subclass of EncryptionKey :return: The generated key. :rtype: EncryptionKey @@ -515,32 +523,74 @@ class KeyManager(object): """ Put C{key} in local storage. - :param key: The key to be stored. It can be ascii key or an OpenPGPKey - :type key: str or OpenPGPKey + :param key: The key to be stored + :type key: EncryptionKey + :raises KeyNotValidUpdate: if a key with the same uid exists and the + new one is not a valid update for it """ try: - if isinstance(key, basestring): - self._wrapper_map[OpenPGPKey].put_ascii_key(key) - else: + old_key = self._wrapper_map[type(key)].get_key(key.address, + private=key.private) + except KeyNotFound: + old_key = None + + if key.private or can_upgrade(key, old_key): + try: self._wrapper_map[type(key)].put_key(key) - except IndexError as e: - leap_assert(False, "Unsupported key type. Error {0!r}".format(e)) + except IndexError as e: + leap_assert( + False, "Unsupported key type. Error {0!r}".format(e)) + else: + raise KeyNotValidUpgrade("Key %s can not be upgraded by new key %s" + % (old_key.key_id, key.key_id)) - def fetch_key(self, address, uri, ktype): + def put_raw_key(self, key, ktype, address=None, + validation=ValidationLevel.Weak_Chain): + """ + Put C{key} in local storage. + + :param key: The ascii key to be stored + :type key: str + :param ktype: the type of the key. + :type ktype: subclass of EncryptionKey + :param address: if set used to check that the key is for this address + :type address: str + :param validation: validation level for this key + (default: 'Weak_Chain') + :type validation: ValidationLevel + + :raises KeyAddressMismatch: if address doesn't match any uid on the key + :raises KeyNotValidUpdate: if a key with the same uid exists and the + new one is not a valid update for it + """ + pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(key) + if address is not None and address != pubkey.address: + raise KeyAddressMismatch("Key UID %s, but expected %s" + % (pubkey.address, address)) + + pubkey.validation = validation + self.put_key(pubkey) + + def fetch_key(self, address, uri, ktype, + validation=ValidationLevel.Weak_Chain): """ Fetch a public key for C{address} from the network and put it in local storage. - Raises C{openpgp.errors.KeyNotFound} if not valid key on C{uri}. - Raises C{openpgp.errors.KeyAttributesDiffer} if address don't match - any uid on the key. - :param address: The email address of the key. :type address: str :param uri: The URI of the key. :type uri: str - :param ktype: The type of the key. - :type ktype: KeyType + :param ktype: the type of the key. + :type ktype: subclass of EncryptionKey + :param validation: validation level for this key + (default: 'Weak_Chain') + :type validation: ValidationLevel + + :raises KeyNotFound: if not valid key on C{uri} + :raises KeyAddressMismatch: if address doesn't match any uid on the key + :raises KeyNotValidUpdate: if a key with the same uid exists and the + new one is not a valid update for it """ res = self._get(uri) if not res.ok: @@ -551,8 +601,10 @@ class KeyManager(object): if pubkey is None: raise KeyNotFound(uri) if pubkey.address != address: - raise KeyAttributesDiffer("UID %s found, but expected %s" - % (pubkey.address, address)) + raise KeyAddressMismatch("UID %s found, but expected %s" + % (pubkey.address, address)) + + pubkey.validation = validation self.put_key(pubkey) from ._version import get_versions diff --git a/keymanager/src/leap/keymanager/errors.py b/keymanager/src/leap/keymanager/errors.py index ebe4fd5..f896582 100644 --- a/keymanager/src/leap/keymanager/errors.py +++ b/keymanager/src/leap/keymanager/errors.py @@ -96,3 +96,9 @@ class KeyFingerprintMismatch(Exception): """ A mismatch between fingerprints. """ + + +class KeyNotValidUpgrade(Exception): + """ + Already existing key can not be upgraded with the new key + """ diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index ec1bfeb..ecb0a36 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -25,12 +25,17 @@ try: import simplejson as json except ImportError: import json # noqa +import logging import re from abc import ABCMeta, abstractmethod from leap.common.check import leap_assert +from leap.keymanager.validation import ValidationLevel, toValidationLevel + +logger = logging.getLogger(__name__) + # # Dictionary keys used for storing cryptographic keys. @@ -106,6 +111,13 @@ def build_key_from_dict(kClass, address, kdict): leap_assert( address == kdict[KEY_ADDRESS_KEY], 'Wrong address in key data.') + try: + validation = toValidationLevel(kdict[KEY_VALIDATION_KEY]) + except ValueError: + logger.error("Not valid validation level (%s) for key %s", + (kdict[KEY_VALIDATION_KEY], kdict[KEY_ID_KEY])) + validation = ValidationLevel.Weak_Chain + return kClass( address, key_id=kdict[KEY_ID_KEY], @@ -116,7 +128,7 @@ def build_key_from_dict(kClass, address, kdict): expiry_date=kdict[KEY_EXPIRY_DATE_KEY], first_seen_at=kdict[KEY_FIRST_SEEN_AT_KEY], last_audited_at=kdict[KEY_LAST_AUDITED_AT_KEY], - validation=kdict[KEY_VALIDATION_KEY], # TODO: verify for validation. + validation=validation, ) @@ -173,7 +185,7 @@ class EncryptionKey(object): KEY_PRIVATE_KEY: self.private, KEY_LENGTH_KEY: self.length, KEY_EXPIRY_DATE_KEY: self.expiry_date, - KEY_VALIDATION_KEY: self.validation, + KEY_VALIDATION_KEY: str(self.validation), KEY_FIRST_SEEN_AT_KEY: self.first_seen_at, KEY_LAST_AUDITED_AT_KEY: self.last_audited_at, KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG], diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 6a825cd..57a7754 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -38,6 +38,7 @@ from leap.keymanager.keys import ( KEYMANAGER_KEY_TAG, TAGS_ADDRESS_PRIVATE_INDEX, ) +from leap.keymanager.validation import ValidationLevel logger = logging.getLogger(__name__) @@ -183,7 +184,7 @@ def _build_key_from_gpg(address, key, key_data): private=True if key['type'] == 'sec' else False, length=key['length'], expiry_date=key['expires'], - validation=None, # TODO: verify for validation. + validation=ValidationLevel.Weak_Chain, ) diff --git a/keymanager/src/leap/keymanager/tests/__init__.py b/keymanager/src/leap/keymanager/tests/__init__.py index e69de29..1ea33b5 100644 --- a/keymanager/src/leap/keymanager/tests/__init__.py +++ b/keymanager/src/leap/keymanager/tests/__init__.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# test_keymanager.py +# Copyright (C) 2013 LEAP +# +# 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 . +""" +Base classes for the Key Manager tests. +""" + +from mock import Mock + +from leap.common.testing.basetest import BaseLeapTest +from leap.soledad.client import Soledad +from leap.keymanager import KeyManager + + +ADDRESS = 'leap@leap.se' +# XXX discover the gpg binary path +GPG_BINARY_PATH = '/usr/bin/gpg' + + +class KeyManagerWithSoledadTestCase(BaseLeapTest): + + def setUp(self): + # mock key fetching and storing so Soledad doesn't fail when trying to + # reach the server. + Soledad._get_secrets_from_shared_db = Mock(return_value=None) + Soledad._put_secrets_in_shared_db = Mock(return_value=None) + + class MockSharedDB(object): + + get_doc = Mock(return_value=None) + put_doc = Mock() + lock = Mock(return_value=('atoken', 300)) + unlock = Mock(return_value=True) + + def __call__(self): + return self + + Soledad._shared_db = MockSharedDB() + + self._soledad = Soledad( + u"leap@leap.se", + u"123456", + secrets_path=self.tempdir + "/secret.gpg", + local_db_path=self.tempdir + "/soledad.u1db", + server_url='', + cert_file=None, + auth_token=None, + ) + + def tearDown(self): + km = self._key_manager() + for key in km.get_all_keys(): + km._wrapper_map[key.__class__].delete_key(key) + for key in km.get_all_keys(private=True): + km._wrapper_map[key.__class__].delete_key(key) + + def _key_manager(self, user=ADDRESS, url='', token=None): + return KeyManager(user, url, self._soledad, token=token, + gpgbinary=GPG_BINARY_PATH) + + +# key 24D18DDF: public key "Leap Test Key " +KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" +PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +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 +=MuOY +-----END PGP PUBLIC KEY BLOCK----- +""" +PRIVATE_KEY = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +lQcYBFC9+dkBEADNRfwV23TWEoGc/x0wWH1P7PlXt8MnC2Z1kKaKKmfnglVrpOiz +iLWoiU58sfZ0L5vHkzXHXCBf6Eiy/EtUIvdiWAn+yASJ1mk5jZTBKO/WMAHD8wTO +zpMsFmWyg3xc4DkmFa9KQ5EVU0o/nqPeyQxNMQN7px5pPwrJtJFmPxnxm+aDkPYx +irDmz/4DeDNqXliazGJKw7efqBdlwTHkl9Akw2gwy178pmsKwHHEMOBOFFvX61AT +huKqHYmlCGSliwbrJppTG7jc1/ls3itrK+CWTg4txREkSpEVmfcASvw/ZqLbjgfs +d/INMwXnR9U81O8+7LT6yw/ca4ppcFoJD7/XJbkRiML6+bJ4Dakiy6i727BzV17g +wI1zqNvm5rAhtALKfACha6YO43aJzairO4II1wxVHvRDHZn2IuKDDephQ3Ii7/vb +hUOf6XCSmchkAcpKXUOvbxm1yfB1LRa64mMc2RcZxf4mW7KQkulBsdV5QG2276lv +U2UUy2IutXcGP5nXC+f6sJJGJeEToKJ57yiO/VWJFjKN8SvP+7AYsQSqINUuEf6H +T5gCPCraGMkTUTPXrREvu7NOohU78q6zZNaL3GW8ai7eSeANSuQ8Vzffx7Wd8Y7i +Pw9sYj0SMFs1UgjbuL6pO5ueHh+qyumbtAq2K0Bci0kqOcU4E9fNtdiovQARAQAB +AA/+JHtlL39G1wsH9R6UEfUQJGXR9MiIiwZoKcnRB2o8+DS+OLjg0JOh8XehtuCs +E/8oGQKtQqa5bEIstX7IZoYmYFiUQi9LOzIblmp2vxOm+HKkxa4JszWci2/ZmC3t +KtaA4adl9XVnshoQ7pijuCMUKB3naBEOAxd8s9d/JeReGIYkJErdrnVfNk5N71Ds +FmH5Ll3XtEDvgBUQP3nkA6QFjpsaB94FHjL3gDwum/cxzj6pCglcvHOzEhfY0Ddb +J967FozQTaf2JW3O+w3LOqtcKWpq87B7+O61tVidQPSSuzPjCtFF0D2LC9R/Hpky +KTMQ6CaKja4MPhjwywd4QPcHGYSqjMpflvJqi+kYIt8psUK/YswWjnr3r4fbuqVY +VhtiHvnBHQjz135lUqWvEz4hM3Xpnxydx7aRlv5NlevK8+YIO5oFbWbGNTWsPZI5 +jpoFBpSsnR1Q5tnvtNHauvoWV+XN2qAOBTG+/nEbDYH6Ak3aaE9jrpTdYh0CotYF +q7csANsDy3JvkAzeU6WnYpsHHaAjqOGyiZGsLej1UcXPFMosE/aUo4WQhiS8Zx2c +zOVKOi/X5vQ2GdNT9Qolz8AriwzsvFR+bxPzyd8V6ALwDsoXvwEYinYBKK8j0OPv +OOihSR6HVsuP9NUZNU9ewiGzte/+/r6pNXHvR7wTQ8EWLcEIAN6Zyrb0bHZTIlxt +VWur/Ht2mIZrBaO50qmM5RD3T5oXzWXi/pjLrIpBMfeZR9DWfwQwjYzwqi7pxtYx +nJvbMuY505rfnMoYxb4J+cpRXV8MS7Dr1vjjLVUC9KiwSbM3gg6emfd2yuA93ihv +Pe3mffzLIiQa4mRE3wtGcioC43nWuV2K2e1KjxeFg07JhrezA/1Cak505ab/tmvP +4YmjR5c44+yL/YcQ3HdFgs4mV+nVbptRXvRcPpolJsgxPccGNdvHhsoR4gwXMS3F +RRPD2z6x8xeN73Q4KH3bm01swQdwFBZbWVfmUGLxvN7leCdfs9+iFJyqHiCIB6Iv +mQfp8F0IAOwSo8JhWN+V1dwML4EkIrM8wUb4yecNLkyR6TpPH/qXx4PxVMC+vy6x +sCtjeHIwKE+9vqnlhd5zOYh7qYXEJtYwdeDDmDbL8oks1LFfd+FyAuZXY33DLwn0 +cRYsr2OEZmaajqUB3NVmj3H4uJBN9+paFHyFSXrH68K1Fk2o3n+RSf2EiX+eICwI +L6rqoF5sSVUghBWdNegV7qfy4anwTQwrIMGjgU5S6PKW0Dr/3iO5z3qQpGPAj5OW +ATqPWkDICLbObPxD5cJlyyNE2wCA9VVc6/1d6w4EVwSq9h3/WTpATEreXXxTGptd +LNiTA1nmakBYNO2Iyo3djhaqBdWjk+EIAKtVEnJH9FAVwWOvaj1RoZMA5DnDMo7e +SnhrCXl8AL7Z1WInEaybasTJXn1uQ8xY52Ua4b8cbuEKRKzw/70NesFRoMLYoHTO +dyeszvhoDHberpGRTciVmpMu7Hyi33rM31K9epA4ib6QbbCHnxkWOZB+Bhgj1hJ8 +xb4RBYWiWpAYcg0+DAC3w9gfxQhtUlZPIbmbrBmrVkO2GVGUj8kH6k4UV6kUHEGY +HQWQR0HcbKcXW81ZXCCD0l7ROuEWQtTe5Jw7dJ4/QFuqZnPutXVRNOZqpl6eRShw +7X2/a29VXBpmHA95a88rSQsL+qm7Fb3prqRmuMCtrUZgFz7HLSTuUMR867QcTGVh +cCBUZXN0IEtleSA8bGVhcEBsZWFwLnNlPokCNwQTAQgAIQUCUL352QIbAwULCQgH +AwUVCgkICwUWAgMBAAIeAQIXgAAKCRAvRV4oJNGN30+xEACh9yLkZ4jqW0/wwyIM +MI896MQf1tAwzMj16MJYUjrjNK4Bn57QaQW926HsxF8C/OjT0MTRhq7heYZJnnEo +rj0rzpkJapUveTRkKeoTRtGGigqJYfkOTU7KRVwgJBXIfaKlI3tC3cX0j0H1fVKX +hLxsj5pNSPRCVf2A5mePg44HtXe6oVWSJ8+EcdTa0shf03NhAtFaY0BbFGPSm9mA +QUe4rxugwXPLctIyV4uweFo5BXFBCb4kKTBdnQi3aJwnoWLNT6rDdTe4/nhY0Hfo +alTCYGLkhio77gBHwpTOjEMO/hZhcDMi4CvxMPw7bRxAwq4u+0j0pDhkiLcQs4U4 +Ou/fH+pia+1nF5h19cNVXIm+RX2fL0wxVYc/14AIAK3YT6PVev9XYEkogSj0P7Kb +HKOruYpnToXJBERNJZwGL1U+ihPNUyroRf29t7u8flnXsOpCtBEIWAO8Muy5pWjV +3O6zAUCfWetAieCQ7WrQVmdJDa7dlX3Qx1XagUzqZdAq2jVI1hOWDA2rKytnReSF +/A97rmLaWZ8aoNCs8i4NLcy9Lbzi9QtornYGVCEmTTym0tM9L/mn7gAJ8dqUwt7n +s24dibfElky4ZZeItD+D7OZGeh0FDuejvv2dXFqL1/pkHpGBZhEckg0fZ95NbgMC +4pSZkZnqRpr2GwfB5aFfB6sIIJ0HGARQvfnZARAAtCP8Z9bm6KzIA7wbXx9LBIcJ +1wQvOPf99s4nOrnQev9xH5PZ820qS9xUjrlyE2bGYAhz5Cmq56ENs7THErIdZHtQ +uYEBprO+VFZjP50vtmCOL2PDl/xgv6J9r1Mp3KnR/m0esR+YceDW1qX07IkB5s+Q +us80v5LmmxnWcikWmR7dt1kOyV/+M6Y6mwvfQ4x3D/QUpO7SfMCOG5DGA7hVUHU/ +Tuh8MihmMFFOLAEEQI+1wkxr1W09HaYCcB+EZqxLSaBwMeFoYPJie9dBNBgps39o +6pDbjsO+or4JNuyoHvh8NNI5iY0IR2NMlW4mqCcHEazys2koxFTYK6YD95Vz0RkA +K4BErCDk3lVR0PH4StmLU3gmPayIjvi9Dl9saPRyu4Xx2WVi+q6spl3ckn4c4f3+ +iD8hxVp74+wa5ew0fIXjIpMoHCar/nndsse4i8glCddINdiOPPmhI9Wi3nT+5Z2t +9omPP2dEh0CzR+j1zvUpT3KtmhVICqhO+QP9BTJOwrp81NTlq9mbUyzTtVk/9dy3 +zoYbhKvY08k6LJ9FsQYySqtfJZ4cwl5WsOhALWwOwlMLA9wkz0eemgFxStyOylzl +QKoIK7zHuU6XYOXa32KSPIWaLy+WgIG/u2ObWtdE3CXVIUuSt5BQFnv7XVNHJllD +Az9VDEkOSYOiSEFVoUsAEQEAAQAP/1AagnZQZyzHDEgw4QELAspYHCWLXE5aZInX +wTUJhK31IgIXNn9bJ0hFiSpQR2xeMs9oYtRuPOu0P8oOFMn4/z374fkjZy8QVY3e +PlL+3EUeqYtkMwlGNmVw5a/NbNuNfm5Darb7pEfbYd1gPcni4MAYw7R2SG/57GbC +9gucvspHIfOSfBNLBthDzmK8xEKe1yD2eimfc2T7IRYb6hmkYfeds5GsqvGI6mwI +85h4uUHWRc5JOlhVM6yX8hSWx0L60Z3DZLChmc8maWnFXd7C8eQ6P1azJJbW71Ih +7CoK0XW4LE82vlQurSRFgTwfl7wFYszW2bOzCuhHDDtYnwH86Nsu0DC78ZVRnvxn +E8Ke/AJgrdhIOo4UAyR+aZD2+2mKd7/waOUTUrUtTzc7i8N3YXGi/EIaNReBXaq+ +ZNOp24BlFzRp+FCF/pptDW9HjPdiV09x0DgICmeZS4Gq/4vFFIahWctg52NGebT0 +Idxngjj+xDtLaZlLQoOz0n5ByjO/Wi0ANmMv1sMKCHhGvdaSws2/PbMR2r4caj8m +KXpIgdinM/wUzHJ5pZyF2U/qejsRj8Kw8KH/tfX4JCLhiaP/mgeTuWGDHeZQERAT +xPmRFHaLP9/ZhvGNh6okIYtrKjWTLGoXvKLHcrKNisBLSq+P2WeFrlme1vjvJMo/ +jPwLT5o9CADQmcbKZ+QQ1ZM9v99iDZol7SAMZX43JC019sx6GK0u6xouJBcLfeB4 +OXacTgmSYdTa9RM9fbfVpti01tJ84LV2SyL/VJq/enJF4XQPSynT/tFTn1PAor6o +tEAAd8fjKdJ6LnD5wb92SPHfQfXqI84rFEO8rUNIE/1ErT6DYifDzVCbfD2KZdoF +cOSp7TpD77sY1bs74ocBX5ejKtd+aH99D78bJSMM4pSDZsIEwnomkBHTziubPwJb +OwnATy0LmSMAWOw5rKbsh5nfwCiUTM20xp0t5JeXd+wPVWbpWqI2EnkCEN+RJr9i +7dp/ymDQ+Yt5wrsN3NwoyiexPOG91WQVCADdErHsnglVZZq9Z8Wx7KwecGCUurJ2 +H6lKudv5YOxPnAzqZS5HbpZd/nRTMZh2rdXCr5m2YOuewyYjvM757AkmUpM09zJX +MQ1S67/UX2y8/74TcRF97Ncx9HeELs92innBRXoFitnNguvcO6Esx4BTe1OdU6qR +ER3zAmVf22Le9ciXbu24DN4mleOH+OmBx7X2PqJSYW9GAMTsRB081R6EWKH7romQ +waxFrZ4DJzZ9ltyosEJn5F32StyLrFxpcrdLUoEaclZCv2qka7sZvi0EvovDVEBU +e10jOx9AOwf8Gj2ufhquQ6qgVYCzbP+YrodtkFrXRS3IsljIchj1M2ffB/0bfoUs +rtER9pLvYzCjBPg8IfGLw0o754Qbhh/ReplCRTusP/fQMybvCvfxreS3oyEriu/G +GufRomjewZ8EMHDIgUsLcYo2UHZsfF7tcazgxMGmMvazp4r8vpgrvW/8fIN/6Adu +tF+WjWDTvJLFJCe6O+BFJOWrssNrrra1zGtLC1s8s+Wfpe+bGPL5zpHeebGTwH1U +22eqgJArlEKxrfarz7W5+uHZJHSjF/K9ZvunLGD0n9GOPMpji3UO3zeM8IYoWn7E +/EWK1XbjnssNemeeTZ+sDh+qrD7BOi+vCX1IyBxbfqnQfJZvmcPWpruy1UsO+aIC +0GY8Jr3OL69dDQ21jueJAh8EGAEIAAkFAlC9+dkCGwwACgkQL0VeKCTRjd9HCw/+ +LQSVgLLF4ulYlPCjWIIuQwrPbJfWUVVr2dPUFVM85DCv8gBzk5c121snXh9Swovm +laBbw6ate3BmbXLh64jVE9Za5sbTWi7PCcbO/bpRy4d6oLmitmNw6cq0vjTLxUYy +bwuiJxWREkfxuU85EKdouN062YDevH+/YResmlJrcCE7LRlJFeRlKsrrwBU3BqYd +GgFJjKjQC1peeQ9fj62Y7xfwE9+PXbkiWO5u/Bk8hb1VZH1SoIRU98NHVcp6BVvp +VK0jLAXuSauSczULmpRjbyt1lhaAqivDTWEEZXiNNbRyp17c3nVdPWOcgBr42hdQ +z25CgZgyLCsvu82wuXLKJblrIPJX3Yf+si6KqEWBsmwdOWybsjygaF5HvzgFqAAD +U0goPWoQ71PorP2XOUNp5ZLkBQp5etvtkksjVNMIhnHn8PGMuoxO39EUGlWj2B5l +Cu8tSosAzB1pS8NcLZzoNoI9dOHrmgJmP+GrOUkcf5GhNZbMoj4GNfGBRYX0SZlQ +GuDrwNKYj73C4MWyNnnUFyq8nDHJ/G1NpaF2hiof9RBL4PUU/f92JkceXPBXA8gL +Mz2ig1OButwPPLFGQhWqxXAGrsS3Ny+BhTJfnfIbbkaLLphBpDZm1D9XKbAUvdd1 +RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc= +=JTFu +-----END PGP PRIVATE KEY BLOCK----- +""" diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 65f8f39..1bd6a2e 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -1,4 +1,4 @@ -## -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # test_keymanager.py # Copyright (C) 2013 LEAP # @@ -23,12 +23,10 @@ Tests for the Key Manager. from mock import Mock from leap.common.testing.basetest import BaseLeapTest -from leap.soledad.client import Soledad from leap.keymanager import ( - KeyManager, openpgp, KeyNotFound, - KeyAttributesDiffer, + KeyAddressMismatch, errors, ) from leap.keymanager.openpgp import OpenPGPKey @@ -36,11 +34,21 @@ from leap.keymanager.keys import ( is_address, build_key_from_dict, ) +from leap.keymanager.validation import ( + ValidationLevel, + toValidationLevel +) +from leap.keymanager.tests import ( + KeyManagerWithSoledadTestCase, + ADDRESS, + KEY_FINGERPRINT, + PUBLIC_KEY, + PRIVATE_KEY, + GPG_BINARY_PATH +) -ADDRESS = 'leap@leap.se' ADDRESS_2 = 'anotheruser@leap.se' -GPG_BINARY_PATH = '/usr/bin/gpg' class KeyManagerUtilTestCase(BaseLeapTest): @@ -76,7 +84,7 @@ class KeyManagerUtilTestCase(BaseLeapTest): 'expiry_date': 'expiry_date', 'first_seen_at': 'first_seen_at', 'last_audited_at': 'last_audited_at', - 'validation': 'validation', + 'validation': str(ValidationLevel.Weak_Chain), } key = build_key_from_dict(OpenPGPKey, ADDRESS, kdict) self.assertEqual( @@ -107,52 +115,10 @@ class KeyManagerUtilTestCase(BaseLeapTest): kdict['last_audited_at'], key.last_audited_at, 'Wrong data in key.') self.assertEqual( - kdict['validation'], key.validation, + toValidationLevel(kdict['validation']), key.validation, 'Wrong data in key.') -class KeyManagerWithSoledadTestCase(BaseLeapTest): - - def setUp(self): - # mock key fetching and storing so Soledad doesn't fail when trying to - # reach the server. - Soledad._get_secrets_from_shared_db = Mock(return_value=None) - Soledad._put_secrets_in_shared_db = Mock(return_value=None) - - class MockSharedDB(object): - - get_doc = Mock(return_value=None) - put_doc = Mock() - lock = Mock(return_value=('atoken', 300)) - unlock = Mock(return_value=True) - - def __call__(self): - return self - - Soledad._shared_db = MockSharedDB() - - self._soledad = Soledad( - u"leap@leap.se", - u"123456", - secrets_path=self.tempdir + "/secret.gpg", - local_db_path=self.tempdir + "/soledad.u1db", - server_url='', - cert_file=None, - auth_token=None, - ) - - def tearDown(self): - km = self._key_manager() - for key in km.get_all_keys(): - km._wrapper_map[key.__class__].delete_key(key) - for key in km.get_all_keys(private=True): - km._wrapper_map[key.__class__].delete_key(key) - - def _key_manager(self, user=ADDRESS, url='', token=None): - return KeyManager(user, url, self._soledad, token=token, - gpgbinary=GPG_BINARY_PATH) - - class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def _test_openpgp_gen_key(self): @@ -220,7 +186,6 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): KeyNotFound, pgp.get_key, ADDRESS, private=True) pgp.put_ascii_key(PRIVATE_KEY) privkey = pgp.get_key(ADDRESS, private=True) - plaintext = pgp.decrypt(cyphertext, privkey) pgp.delete_key(pubkey) pgp.delete_key(privkey) self.assertRaises( @@ -473,7 +438,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): """ km = self._key_manager(url='http://nickserver.domain') - km.put_key(PUBLIC_KEY) + km.put_raw_key(PUBLIC_KEY, OpenPGPKey) key = km.get_key(ADDRESS, OpenPGPKey) self.assertIsInstance(key, OpenPGPKey) self.assertEqual(ADDRESS, key.address) @@ -524,7 +489,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km._fetcher.get = Mock(return_value=Response()) km.ca_cert_path = 'cacertpath' - self.assertRaises(KeyAttributesDiffer, km.fetch_key, + self.assertRaises(KeyAddressMismatch, km.fetch_key, ADDRESS_2, "http://site.domain/key", OpenPGPKey) @@ -568,169 +533,6 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): # Key material for testing -# key 24D18DDF: public key "Leap Test Key " -KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" -PUBLIC_KEY = """ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -=MuOY ------END PGP PUBLIC KEY BLOCK----- -""" -PRIVATE_KEY = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -=JTFu ------END PGP PRIVATE KEY BLOCK----- -""" - # key 7FEE575A: public key "anotheruser " PUBLIC_KEY_2 = """ -----BEGIN PGP PUBLIC KEY BLOCK----- diff --git a/keymanager/src/leap/keymanager/tests/test_validation.py b/keymanager/src/leap/keymanager/tests/test_validation.py new file mode 100644 index 0000000..c7170ab --- /dev/null +++ b/keymanager/src/leap/keymanager/tests/test_validation.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2014 LEAP +# +# 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 . +""" +Tests for the Validation Levels +""" + +from leap.keymanager.openpgp import OpenPGPKey +from leap.keymanager.errors import ( + KeyNotValidUpgrade +) +from leap.keymanager.tests import ( + KeyManagerWithSoledadTestCase, + ADDRESS, + PUBLIC_KEY, + KEY_FINGERPRINT +) +from leap.keymanager.validation import ValidationLevel + + +class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): + + def test_none_old_key(self): + km = self._key_manager() + km.put_raw_key(PUBLIC_KEY, OpenPGPKey) + key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + self.assertEqual(key.fingerprint, KEY_FINGERPRINT) + + def test_cant_upgrade(self): + km = self._key_manager() + km.put_raw_key(PUBLIC_KEY, OpenPGPKey, + validation=ValidationLevel.Provider_Trust) + self.assertRaises(KeyNotValidUpgrade, km.put_raw_key, UNRELATED_KEY, + OpenPGPKey) + + def test_fingerprint_level(self): + km = self._key_manager() + km.put_raw_key(PUBLIC_KEY, OpenPGPKey) + km.put_raw_key(UNRELATED_KEY, OpenPGPKey, + validation=ValidationLevel.Fingerprint) + key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT) + + def test_expired_key(self): + km = self._key_manager() + km.put_raw_key(EXPIRED_KEY, OpenPGPKey) + km.put_raw_key(UNRELATED_KEY, OpenPGPKey) + key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT) + + def test_expired_fail_lower_level(self): + km = self._key_manager() + km.put_raw_key(EXPIRED_KEY, OpenPGPKey, + validation=ValidationLevel.Third_Party_Endorsement) + self.assertRaises( + KeyNotValidUpgrade, + km.put_raw_key, + UNRELATED_KEY, + OpenPGPKey, + validation=ValidationLevel.Provider_Trust) + + +# Key material for testing + +# key 901FBCA5: public key "Leap Test Key " +UNRELATED_FINGERPRINT = "ABCCD9C8270B6A8D5633FAC9D04DB2E4901FBCA5" +UNRELATED_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mQENBFQ9VDoBCACbKflcEhUXZULOT4Fwc2ifRUllJpusd2uX5oeDlZdZ15uLY2eF +LcxnAdIWkI/PsXimh0ev/Pf4oCynfmt02I3c2d9F0N6JXWnRiP+p098oPOcqeEqL +N3CrkH1RVnEXNeJ/Fu7tkD61SBXl1MytMfcHyhN5arg8OcVAjcmghX53+92jFhC9 +8ss87H/qEe5vEX/ahP3tiL5ULvaS4GIX+XB0O3yCVdRoRG9lqMIBP/ZqCkKrNll8 +dT12a6ByG/rWharZUeUETiM4Y+JjDUUaEC2YhNF9k52JNGanLH9LTTtlKy5WTT+E +C6T6VMAtkwcBDpkXr5sBB/N+Y1z0Fp359lIXABEBAAG0HExlYXAgVGVzdCBLZXkg +PGxlYXBAbGVhcC5zZT6JATgEEwECACIFAlQ9VDoCGwMGCwkIBwMCBhUIAgkKCwQW +AgMBAh4BAheAAAoJENBNsuSQH7ylsSUIAIxUFbkeTdHbCF/LVA2U+ktnR1iVikAY +vFK+U+Bto11/AO4Kew2eWniDch/sqLQOoSydtP42z2z3/Al3u7LhQ8bElQHPDY78 +t49qweyJi00V3vCKCdWwPJnPM5eJOIrZHCbwIgeXCsXxVNJVyziVqMuum+px1h2d +1YJZXYejT8rzwa3yBPAsGWRAWETeTvUuyjPMFa59scbnaDuY+bwQ2r/qG9m7UyHU +h2kAHC5sf1rixVOY6rLhw75gQHE/L2BZJRfVsDQqIpEMh2OgMfNbL928jncjwQvc +/IXXwSUx7y50ll+uNh+TVLf0MlUjKdHmHqnGBMlIIWojWJuKxYmOOoO5AQ0EVD1U +OgEIAM/TlhWVSI+tl5XBUAcf60RxjpHQkmdfq1i1jgwUgu/638EKzBfLcnRYX8Rn +DO9CWnHcql/4hp226fIWZN/SyReE81n7UkLDMAglhHgiezHMSH1GYVu4IlfpLVXn +brLVo83KioH5MPFWmZv5tigpU/G8dTx9yVGv1//YW2qqRYYqeIKJfapBaY/bNqyD +vYRfZo1K2brtHx4bToY6mALRF4ruV5SVZGS69e4Sh692C2pXSVbCpRhQ/2WnvkZH +leFIdmNmQN61MC1k26A620Rm+pAsXX71dln0u96xbrCgEVbi6ccfXzbFKtVmThVB +w11CLvVTviOm99TmcgpmDS4cf08AEQEAAYkBHwQYAQIACQUCVD1UOgIbDAAKCRDQ +TbLkkB+8pR+fB/0SeTcRr1duN7VYWdtng1+jO0ornIBtUraglN01dEEmiwN83DTi +J37i+nll+4is7BtiXqhumRptKh1v8UUMyFX/rjjoojCJBg5NExsiOYl3O4le68oF +3+XC+n7yrlyNmI15+3dcQmC9F6HN8EBZgrn5YPKGIOMHTGatB5PryMKg2IKiN5GZ +E0hmrOQgmcGrkeqysKACQYUHTasSk2IY1l1G5YQglqCaBh4+UC82Dmg5fTBbHjxP +YhhojkP4aD/0YW7dgql3nzYqvPCAjBH1Cf6rA9HvAJwUP9Ig/okcrrPEKm638+mG ++vNIuLqIkA4oFLBAAIrgMiQZ+NZz9uD6DJE7 +=FO7G +-----END PGP PUBLIC KEY BLOCK----- +""" + +# key A1885A7C: public key "Leap Test Key " +EXPIRED_FINGERPRINT = "7C1F68B0E14157B09B5F4ADE6F15F004A1885A7C" +EXPIRED_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +mQENBBvrfd0BCADGNpspaNhsbhSjKioCWrE2MTTYC+Sdpes22RabdhQyOCWvlSbj +b8p0y3kmnMOtVBT+c22/w7eu2YBfIpS4RswgE5ypr/1kZLFQueVe/cp29GjPvLwJ +82A3EOHcmXs8rSJ76h2bnkySvbJawz9rwCcaXhpdAwC+sjWvbqiwZYEL+90I4Xp3 +acDh9vNtPxDCg5RdI0bfdIEBGgHTfsda3kWGvo1wH5SgrTRq0+EcTI7aJgkMmM/A +IhnpACE52NvGdG9eB3x7xyQFsQqK8F0XvEev2UJH4SR7vb+Z7FNTJKCy6likYbSV +wGGFuowFSESnzXuUI6PcjyuO6FUbMgeM5euFABEBAAG0HExlYXAgVGVzdCBLZXkg +PGxlYXBAbGVhcC5zZT6JAT4EEwECACgFAhvrfd0CGwMFCQABUYAGCwkIBwMCBhUI +AgkKCwQWAgMBAh4BAheAAAoJEG8V8AShiFp8VNkH/iCQcXkTfMOVlL2rQRyZtJEO +Lr5uTyyY8O6ubeNCHqZzlIopiPAsv4hIYjjMDvOfZ9R53YgmbacUm0rvh1B4MSUf +k+sa9/tequ3y44LUKp7AB6NyyLgVOU5ngl2w+bi7CgXAep3oP4joYKcU0mmSAc2S +2Gj85DVqP0kdzNs47esvyj7g1TOfdBwmLsTx/219H+w3dNBeyCQWkYCYNh7MX/Ba +SZ+P0xr4FetcOVPM3wAzUtDG7hKsgccoIXt0FWhG/nn8cETfGH+o3W/ky7Jktatx +DGDHoZJvAaG2B2ey1pAQlezr8p/O+ZVABiigHk1S+myBHyhlXzUcjhQnEG7aHZ65 +AQ0EG+t93QEIAKqRq/2sBDW4g3FU+11LhixT+GosrfVvnitz3S9k2tBXok/wYpI1 +XeA+kTHiF0LaqoaciDRvkA9DvhDbSrNM1yeuYRyZiHlTmoPZ/Fkl60oA2cyLd1L5 +sXbuipY3TEiakugdSU4rzgi0hFycm6Go6yq2G6eC6UALvD9CTMdZHw40TadG9xpm +4thYPuJ1kPH8/bkbTi9sLHoApYgL+7ssje8w4epr0qD4IGxeKwJPf/tbTRpnd8w3 +leldixHHKAutNt49p0pkXlORAHRpUmp+KMZhFvCvIPwe9o5mYtMR7sDRxjY61ZEQ +KLyKoh5wsJsaPXBjdG7cf6G/cBcwvnQVUHcAEQEAAYkBJQQYAQIADwUCG+t93QIb +DAUJAAFRgAAKCRBvFfAEoYhafOPgB/9z4YCyT/N0262HtegHykhsyykuqEeNb1LV +D9INcP+RbCX/0IjFgP4DTMPP7qqF1OBwR276maALT321Gqxc5HN5YrwxGdmoyBLm +unaQJJlD+7B1C+jnO6r4m44obvJ/NMERxVyzkXap3J2VgRIO1wNLI9I0sH6Kj5/j +Mgy06OwXDcqIc+jB4sIJ3Tnm8LZ3phJzNEm9mI8Ak0oJ7IEcMndR6DzmRt1rJQcq +K/D7hOG02zvyRhxF27U1qR1MxeU/gNnOx8q4dnVyWB+EiV1sFl4iTOyYHEsoyd7W +Osuse7+NkyUHgMXMVW7cz+nU7iO+ht2rkBtv+Z5LGlzgHTeFjKci +=WhX+ +-----END PGP PUBLIC KEY BLOCK----- +""" + +import unittest +if __name__ == "__main__": + unittest.main() diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py new file mode 100644 index 0000000..6dceb78 --- /dev/null +++ b/keymanager/src/leap/keymanager/validation.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# __init__.py +# Copyright (C) 2014 LEAP +# +# 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 . + +""" +Validation levels implementation for key managment. + +See: + https://lists.riseup.net/www/arc/leap-discuss/2014-09/msg00000.html +""" + + +from datetime import datetime +from enum import Enum + + +ValidationLevel = Enum( + "Weak_Chain", + "Provider_Trust", + "Provider_Endorsement", + "Third_Party_Endorsement", + "Third_Party_Consensus", + "Historically_Auditing", + "Known_Key", + "Fingerprint") + + +def toValidationLevel(value): + """ + Convert a string representation of a validation level into + C{ValidationLevel} + + :param value: validation level + :type value: str + :rtype: ValidationLevel + :raises ValueError: if C{value} is not a validation level + """ + for level in ValidationLevel: + if value == str(level): + return level + raise ValueError("Not valid validation level: %s" % (value,)) + + +def can_upgrade(new_key, old_key): + """ + :type new_key: EncryptionKey + :type old_key: EncryptionKey + :rtype: bool + """ + # XXX not succesfully used and strict high validation level (#6211) + # XXX implement key signature checking (#6120) + + # First contact + if old_key is None: + return True + + if new_key.address != old_key.address: + # XXX how do we map multiple IDs? (#6212) + return False + + # An update of the same key + if new_key.fingerprint == old_key.fingerprint: + # XXX wich one is newer? is that a downgrade attack? (#6210) + return True + + # Manually verified fingerprint + if new_key.validation == ValidationLevel.Fingerprint: + return True + + # Expired key and higher validation level + if old_key.expiry_date: + old_expiry_date = datetime.fromtimestamp(int(old_key.expiry_date)) + if (old_expiry_date < datetime.now() and + new_key.validation >= old_key.validation): + return True + + # No expiration date and higher validation level + elif new_key.validation >= old_key.validation: + return True + + return False -- cgit v1.2.3 From 12a1627e820ed543158678326032814daebe139d Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 20 Oct 2014 18:35:11 -0500 Subject: Update doc string of OpenPGPScheme._temporary_gpgwrapper --- keymanager/src/leap/keymanager/openpgp.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 57a7754..e84cd29 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -452,10 +452,8 @@ class OpenPGPScheme(EncryptionScheme): Return a gpg wrapper that implements the context manager protocol and contains C{keys}. - :param key_data: ASCII armored key data. - :type key_data: str - :param gpgbinary: Name for GnuPG binary executable. - :type gpgbinary: C{str} + :param keys: keys to conform the keyring. + :type key: list(OpenPGPKey) :return: a TempGPGWrapper instance :rtype: TempGPGWrapper -- cgit v1.2.3 From 1fac28ed61bde2b8db8cff8aea45b20705a072e3 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 30 Oct 2014 21:54:32 -0600 Subject: Merge keys when updating an exisiting key This is needed to prevent roll back attacks where the attacker push us to accept a key with an old expiration date that could be use to push an untrusted key when after it's expiration. --- keymanager/src/leap/keymanager/openpgp.py | 12 ++++++ .../src/leap/keymanager/tests/test_validation.py | 43 ++++++++++++++++++++++ keymanager/src/leap/keymanager/validation.py | 1 - 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index e84cd29..f86b35d 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -37,6 +37,8 @@ from leap.keymanager.keys import ( build_key_from_dict, KEYMANAGER_KEY_TAG, TAGS_ADDRESS_PRIVATE_INDEX, + KEY_FINGERPRINT_KEY, + KEY_DATA_KEY, ) from leap.keymanager.validation import ValidationLevel @@ -394,6 +396,16 @@ class OpenPGPScheme(EncryptionScheme): if doc is None: self._soledad.create_doc_from_json(key.get_json()) else: + if key.fingerprint == doc.content[KEY_FINGERPRINT_KEY]: + # in case of an update of the key merge them with gnupg + with self._temporary_gpgwrapper() as gpg: + gpg.import_keys(doc.content[KEY_DATA_KEY]) + gpg.import_keys(key.key_data) + gpgkey = gpg.list_keys(secret=key.private).pop() + key = _build_key_from_gpg( + key.address, gpgkey, + gpg.export_keys(gpgkey['fingerprint'], + secret=key.private)) doc.set_json(key.get_json()) self._soledad.put_doc(doc) diff --git a/keymanager/src/leap/keymanager/tests/test_validation.py b/keymanager/src/leap/keymanager/tests/test_validation.py index c7170ab..0f4d81a 100644 --- a/keymanager/src/leap/keymanager/tests/test_validation.py +++ b/keymanager/src/leap/keymanager/tests/test_validation.py @@ -72,6 +72,13 @@ class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): OpenPGPKey, validation=ValidationLevel.Provider_Trust) + def test_roll_back(self): + km = self._key_manager() + km.put_raw_key(EXPIRED_KEY_UPDATED, OpenPGPKey) + km.put_raw_key(EXPIRED_KEY, OpenPGPKey) + key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + self.assertEqual(key.expiry_date, EXPIRED_KEY_NEW_EXPIRY_DATE) + # Key material for testing @@ -144,6 +151,42 @@ Osuse7+NkyUHgMXMVW7cz+nU7iO+ht2rkBtv+Z5LGlzgHTeFjKci =WhX+ -----END PGP PUBLIC KEY BLOCK----- """ +# updated expiration date +# Tue 24 Oct 2034 05:13:00 PM BST +EXPIRED_KEY_NEW_EXPIRY_DATE = "2045319180" +EXPIRED_KEY_UPDATED = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +mQENBBvrfd0BCADGNpspaNhsbhSjKioCWrE2MTTYC+Sdpes22RabdhQyOCWvlSbj +b8p0y3kmnMOtVBT+c22/w7eu2YBfIpS4RswgE5ypr/1kZLFQueVe/cp29GjPvLwJ +82A3EOHcmXs8rSJ76h2bnkySvbJawz9rwCcaXhpdAwC+sjWvbqiwZYEL+90I4Xp3 +acDh9vNtPxDCg5RdI0bfdIEBGgHTfsda3kWGvo1wH5SgrTRq0+EcTI7aJgkMmM/A +IhnpACE52NvGdG9eB3x7xyQFsQqK8F0XvEev2UJH4SR7vb+Z7FNTJKCy6likYbSV +wGGFuowFSESnzXuUI6PcjyuO6FUbMgeM5euFABEBAAG0HExlYXAgVGVzdCBLZXkg +PGxlYXBAbGVhcC5zZT6JAT4EEwECACgCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B +AheABQJUURIXBQld/ZovAAoJEG8V8AShiFp8xUcIALcAHZbaxvyhHRGOrwDddbH0 +fFDK0AqKTsIT7y4D/HLFCP5zG3Ck7qGPZdkHXZfzq8rIb+zUjW3oJIVI1IucHxG2 +T5kppa8RFCBAFlRWYf6R3isX3YL0d3QSragjoxRNPcHNU8ALHcvfSonFHBoi4fH4 +4rvgksAiT68SsdPaoXDlabx5T15evu/7T5e/DGMQVPMxiaSuSQhbOKuMk2wcFdmL +tBYHLZPa54hHPNhEDyxLgtKKph0gObk9ojKfH9kPvLveIcpS5CqTJfN/kqBz7CJW +wEeAi2iG3H1OEB25aCUdTxXSRNlGqEgcWPaWxtc1RzlARu7LB64OUZuRy4puiAG5 +AQ0EG+t93QEIAKqRq/2sBDW4g3FU+11LhixT+GosrfVvnitz3S9k2tBXok/wYpI1 +XeA+kTHiF0LaqoaciDRvkA9DvhDbSrNM1yeuYRyZiHlTmoPZ/Fkl60oA2cyLd1L5 +sXbuipY3TEiakugdSU4rzgi0hFycm6Go6yq2G6eC6UALvD9CTMdZHw40TadG9xpm +4thYPuJ1kPH8/bkbTi9sLHoApYgL+7ssje8w4epr0qD4IGxeKwJPf/tbTRpnd8w3 +leldixHHKAutNt49p0pkXlORAHRpUmp+KMZhFvCvIPwe9o5mYtMR7sDRxjY61ZEQ +KLyKoh5wsJsaPXBjdG7cf6G/cBcwvnQVUHcAEQEAAYkBJQQYAQIADwUCG+t93QIb +DAUJAAFRgAAKCRBvFfAEoYhafOPgB/9z4YCyT/N0262HtegHykhsyykuqEeNb1LV +D9INcP+RbCX/0IjFgP4DTMPP7qqF1OBwR276maALT321Gqxc5HN5YrwxGdmoyBLm +unaQJJlD+7B1C+jnO6r4m44obvJ/NMERxVyzkXap3J2VgRIO1wNLI9I0sH6Kj5/j +Mgy06OwXDcqIc+jB4sIJ3Tnm8LZ3phJzNEm9mI8Ak0oJ7IEcMndR6DzmRt1rJQcq +K/D7hOG02zvyRhxF27U1qR1MxeU/gNnOx8q4dnVyWB+EiV1sFl4iTOyYHEsoyd7W +Osuse7+NkyUHgMXMVW7cz+nU7iO+ht2rkBtv+Z5LGlzgHTeFjKci +=79Ll +-----END PGP PUBLIC KEY BLOCK----- +""" + import unittest if __name__ == "__main__": diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index 6dceb78..7d68966 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -73,7 +73,6 @@ def can_upgrade(new_key, old_key): # An update of the same key if new_key.fingerprint == old_key.fingerprint: - # XXX wich one is newer? is that a downgrade attack? (#6210) return True # Manually verified fingerprint -- cgit v1.2.3 From fe41ebea35e17abb402b2ba0d786897ac102e760 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 30 Oct 2014 21:57:48 -0600 Subject: Remove outdated comment --- keymanager/src/leap/keymanager/keys.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index ecb0a36..b5c9118 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -141,16 +141,7 @@ class EncryptionKey(object): Abstract class for encryption keys. A key is "validated" if the nicknym agent has bound the user address to a - public key. Nicknym supports three different levels of key validation: - - * Level 3 - path trusted: A path of cryptographic signatures can be traced - from a trusted key to the key under evaluation. By default, only the - provider key from the user's provider is a "trusted key". - * level 2 - provider signed: The key has been signed by a provider key for - the same domain, but the provider key is not validated using a trust - path (i.e. it is only registered) - * level 1 - registered: The key has been encountered and saved, it has no - signatures (that are meaningful to the nicknym agent). + public key. """ __metaclass__ = ABCMeta -- cgit v1.2.3 From 68565379fe73fb8a07e96925785ab9a733880b4b Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Tue, 4 Nov 2014 11:53:56 -0600 Subject: Use datetime for key expiration --- keymanager/src/leap/keymanager/keys.py | 13 +++++++++++-- keymanager/src/leap/keymanager/openpgp.py | 7 ++++++- keymanager/src/leap/keymanager/tests/test_keymanager.py | 4 ++-- keymanager/src/leap/keymanager/tests/test_validation.py | 5 +++-- keymanager/src/leap/keymanager/validation.py | 9 ++++----- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index b5c9118..a61a8c7 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -30,6 +30,7 @@ import re from abc import ABCMeta, abstractmethod +from datetime import datetime from leap.common.check import leap_assert from leap.keymanager.validation import ValidationLevel, toValidationLevel @@ -118,6 +119,10 @@ def build_key_from_dict(kClass, address, kdict): (kdict[KEY_VALIDATION_KEY], kdict[KEY_ID_KEY])) validation = ValidationLevel.Weak_Chain + expiry_date = None + if kdict[KEY_EXPIRY_DATE_KEY]: + expiry_date = datetime.fromtimestamp(int(kdict[KEY_EXPIRY_DATE_KEY])) + return kClass( address, key_id=kdict[KEY_ID_KEY], @@ -125,7 +130,7 @@ def build_key_from_dict(kClass, address, kdict): key_data=kdict[KEY_DATA_KEY], private=kdict[KEY_PRIVATE_KEY], length=kdict[KEY_LENGTH_KEY], - expiry_date=kdict[KEY_EXPIRY_DATE_KEY], + expiry_date=expiry_date, first_seen_at=kdict[KEY_FIRST_SEEN_AT_KEY], last_audited_at=kdict[KEY_LAST_AUDITED_AT_KEY], validation=validation, @@ -167,6 +172,10 @@ class EncryptionKey(object): :return: The JSON string describing this key. :rtype: str """ + expiry_str = "" + if self.expiry_date is not None: + expiry_str = self.expiry_date.strftime("%s") + return json.dumps({ KEY_ADDRESS_KEY: self.address, KEY_TYPE_KEY: str(self.__class__), @@ -175,7 +184,7 @@ class EncryptionKey(object): KEY_DATA_KEY: self.key_data, KEY_PRIVATE_KEY: self.private, KEY_LENGTH_KEY: self.length, - KEY_EXPIRY_DATE_KEY: self.expiry_date, + KEY_EXPIRY_DATE_KEY: expiry_str, KEY_VALIDATION_KEY: str(self.validation), KEY_FIRST_SEEN_AT_KEY: self.first_seen_at, KEY_LAST_AUDITED_AT_KEY: self.last_audited_at, diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index f86b35d..d3c305e 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -25,6 +25,7 @@ import tempfile import io +from datetime import datetime from gnupg import GPG from gnupg.gnupg import GPGUtilities @@ -178,6 +179,10 @@ def _build_key_from_gpg(address, key, key_data): :return: An instance of the key. :rtype: OpenPGPKey """ + expiry_date = None + if key['expires']: + expiry_date = datetime.fromtimestamp(int(key['expires'])) + return OpenPGPKey( address, key_id=key['keyid'], @@ -185,7 +190,7 @@ def _build_key_from_gpg(address, key, key_data): key_data=key_data, private=True if key['type'] == 'sec' else False, length=key['length'], - expiry_date=key['expires'], + expiry_date=expiry_date, validation=ValidationLevel.Weak_Chain, ) diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 1bd6a2e..6a877bc 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -81,7 +81,7 @@ class KeyManagerUtilTestCase(BaseLeapTest): 'key_data': 'key_data', 'private': 'private', 'length': 'length', - 'expiry_date': 'expiry_date', + 'expiry_date': '', 'first_seen_at': 'first_seen_at', 'last_audited_at': 'last_audited_at', 'validation': str(ValidationLevel.Weak_Chain), @@ -106,7 +106,7 @@ class KeyManagerUtilTestCase(BaseLeapTest): kdict['length'], key.length, 'Wrong data in key.') self.assertEqual( - kdict['expiry_date'], key.expiry_date, + None, key.expiry_date, 'Wrong data in key.') self.assertEqual( kdict['first_seen_at'], key.first_seen_at, diff --git a/keymanager/src/leap/keymanager/tests/test_validation.py b/keymanager/src/leap/keymanager/tests/test_validation.py index 0f4d81a..3ae873d 100644 --- a/keymanager/src/leap/keymanager/tests/test_validation.py +++ b/keymanager/src/leap/keymanager/tests/test_validation.py @@ -18,6 +18,8 @@ Tests for the Validation Levels """ +from datetime import datetime + from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.errors import ( KeyNotValidUpgrade @@ -152,8 +154,7 @@ Osuse7+NkyUHgMXMVW7cz+nU7iO+ht2rkBtv+Z5LGlzgHTeFjKci -----END PGP PUBLIC KEY BLOCK----- """ # updated expiration date -# Tue 24 Oct 2034 05:13:00 PM BST -EXPIRED_KEY_NEW_EXPIRY_DATE = "2045319180" +EXPIRED_KEY_NEW_EXPIRY_DATE = datetime.fromtimestamp(2045319180) EXPIRED_KEY_UPDATED = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index 7d68966..cf5b4a8 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -80,11 +80,10 @@ def can_upgrade(new_key, old_key): return True # Expired key and higher validation level - if old_key.expiry_date: - old_expiry_date = datetime.fromtimestamp(int(old_key.expiry_date)) - if (old_expiry_date < datetime.now() and - new_key.validation >= old_key.validation): - return True + if (old_key.expiry_date is not None and + old_key.expiry_date < datetime.now() and + new_key.validation >= old_key.validation): + return True # No expiration date and higher validation level elif new_key.validation >= old_key.validation: -- cgit v1.2.3 From 88b2416234ed88bc7652c452f779c95f17bf210f Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 6 Nov 2014 00:47:32 -0600 Subject: Implement the new encryption-key soledad document --- keymanager/src/leap/keymanager/__init__.py | 10 ++-- keymanager/src/leap/keymanager/keys.py | 60 +++++++++++++++------- keymanager/src/leap/keymanager/openpgp.py | 22 ++++---- .../src/leap/keymanager/tests/test_keymanager.py | 55 +++++++++++--------- 4 files changed, 89 insertions(+), 58 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 156aaf8..53dd9a7 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -319,7 +319,7 @@ class KeyManager(object): return map( lambda doc: build_key_from_dict( self._key_class_from_type(doc.content['type']), - doc.content['address'], + doc.content['address'][0], doc.content), self._soledad.get_from_index( TAGS_PRIVATE_INDEX, @@ -529,7 +529,7 @@ class KeyManager(object): new one is not a valid update for it """ try: - old_key = self._wrapper_map[type(key)].get_key(key.address, + old_key = self._wrapper_map[type(key)].get_key(key.address[0], private=key.private) except KeyNotFound: old_key = None @@ -564,7 +564,7 @@ class KeyManager(object): new one is not a valid update for it """ pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(key) - if address is not None and address != pubkey.address: + if address is not None and address not in pubkey.address: raise KeyAddressMismatch("Key UID %s, but expected %s" % (pubkey.address, address)) @@ -600,9 +600,9 @@ class KeyManager(object): pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(res.content) if pubkey is None: raise KeyNotFound(uri) - if pubkey.address != address: + if address not in pubkey.address: raise KeyAddressMismatch("UID %s found, but expected %s" - % (pubkey.address, address)) + % (str(pubkey.address), address)) pubkey.validation = validation self.put_key(pubkey) diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index a61a8c7..4952b9b 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -27,6 +27,7 @@ except ImportError: import json # noqa import logging import re +import time from abc import ABCMeta, abstractmethod @@ -50,9 +51,11 @@ KEY_DATA_KEY = 'key_data' KEY_PRIVATE_KEY = 'private' KEY_LENGTH_KEY = 'length' KEY_EXPIRY_DATE_KEY = 'expiry_date' -KEY_FIRST_SEEN_AT_KEY = 'first_seen_at' KEY_LAST_AUDITED_AT_KEY = 'last_audited_at' +KEY_REFRESHED_AT_KEY = 'refreshed_at' KEY_VALIDATION_KEY = 'validation' +KEY_ENCR_USED_KEY = 'encr_used' +KEY_SIGN_USED_KEY = 'sign_used' KEY_TAGS_KEY = 'tags' @@ -110,7 +113,7 @@ def build_key_from_dict(kClass, address, kdict): :rtype: C{kClass} """ leap_assert( - address == kdict[KEY_ADDRESS_KEY], + address in kdict[KEY_ADDRESS_KEY], 'Wrong address in key data.') try: validation = toValidationLevel(kdict[KEY_VALIDATION_KEY]) @@ -119,24 +122,40 @@ def build_key_from_dict(kClass, address, kdict): (kdict[KEY_VALIDATION_KEY], kdict[KEY_ID_KEY])) validation = ValidationLevel.Weak_Chain - expiry_date = None - if kdict[KEY_EXPIRY_DATE_KEY]: - expiry_date = datetime.fromtimestamp(int(kdict[KEY_EXPIRY_DATE_KEY])) + expiry_date = _to_datetime(kdict[KEY_EXPIRY_DATE_KEY]) + last_audited_at = _to_datetime(kdict[KEY_LAST_AUDITED_AT_KEY]) + refreshed_at = _to_datetime(kdict[KEY_REFRESHED_AT_KEY]) return kClass( - address, + [address], key_id=kdict[KEY_ID_KEY], fingerprint=kdict[KEY_FINGERPRINT_KEY], key_data=kdict[KEY_DATA_KEY], private=kdict[KEY_PRIVATE_KEY], length=kdict[KEY_LENGTH_KEY], expiry_date=expiry_date, - first_seen_at=kdict[KEY_FIRST_SEEN_AT_KEY], - last_audited_at=kdict[KEY_LAST_AUDITED_AT_KEY], + last_audited_at=last_audited_at, + refreshed_at=refreshed_at, validation=validation, + encr_used=kdict[KEY_ENCR_USED_KEY], + sign_used=kdict[KEY_SIGN_USED_KEY], ) +def _to_datetime(unix_time): + if unix_time != 0: + return datetime.fromtimestamp(unix_time) + else: + return None + + +def _to_unix_time(date): + if date is not None: + return int(time.mktime(date.timetuple())) + else: + return 0 + + # # Abstraction for encryption keys # @@ -151,9 +170,10 @@ class EncryptionKey(object): __metaclass__ = ABCMeta - def __init__(self, address, key_id=None, fingerprint=None, - key_data=None, private=None, length=None, expiry_date=None, - validation=None, first_seen_at=None, last_audited_at=None): + def __init__(self, address, key_id="", fingerprint="", + key_data="", private=False, length=0, expiry_date=None, + validation=ValidationLevel.Weak_Chain, last_audited_at=None, + refreshed_at=None, encr_used=False, sign_used=False): self.address = address self.key_id = key_id self.fingerprint = fingerprint @@ -162,8 +182,10 @@ class EncryptionKey(object): self.length = length self.expiry_date = expiry_date self.validation = validation - self.first_seen_at = first_seen_at self.last_audited_at = last_audited_at + self.refreshed_at = refreshed_at + self.encr_used = encr_used + self.sign_used = sign_used def get_json(self): """ @@ -172,9 +194,9 @@ class EncryptionKey(object): :return: The JSON string describing this key. :rtype: str """ - expiry_str = "" - if self.expiry_date is not None: - expiry_str = self.expiry_date.strftime("%s") + expiry_date = _to_unix_time(self.expiry_date) + last_audited_at = _to_unix_time(self.last_audited_at) + refreshed_at = _to_unix_time(self.refreshed_at) return json.dumps({ KEY_ADDRESS_KEY: self.address, @@ -184,10 +206,12 @@ class EncryptionKey(object): KEY_DATA_KEY: self.key_data, KEY_PRIVATE_KEY: self.private, KEY_LENGTH_KEY: self.length, - KEY_EXPIRY_DATE_KEY: expiry_str, + KEY_EXPIRY_DATE_KEY: expiry_date, + KEY_LAST_AUDITED_AT_KEY: last_audited_at, + KEY_REFRESHED_AT_KEY: refreshed_at, KEY_VALIDATION_KEY: str(self.validation), - KEY_FIRST_SEEN_AT_KEY: self.first_seen_at, - KEY_LAST_AUDITED_AT_KEY: self.last_audited_at, + KEY_ENCR_USED_KEY: self.encr_used, + KEY_SIGN_USED_KEY: self.sign_used, KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG], }) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index d3c305e..1160434 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -41,7 +41,6 @@ from leap.keymanager.keys import ( KEY_FINGERPRINT_KEY, KEY_DATA_KEY, ) -from leap.keymanager.validation import ValidationLevel logger = logging.getLogger(__name__) @@ -109,9 +108,9 @@ class TempGPGWrapper(object): # itself is enough to also have the public key in the keyring, # and we want to count the keys afterwards. - privaddrs = map(lambda privkey: privkey.address, privkeys) + privaddrs = map(lambda privkey: privkey.address[0], privkeys) publkeys = filter( - lambda pubkey: pubkey.address not in privaddrs, publkeys) + lambda pubkey: pubkey.address[0] not in privaddrs, publkeys) listkeys = lambda: self._gpg.list_keys() listsecretkeys = lambda: self._gpg.list_keys(secret=True) @@ -184,14 +183,14 @@ def _build_key_from_gpg(address, key, key_data): expiry_date = datetime.fromtimestamp(int(key['expires'])) return OpenPGPKey( - address, + [address], key_id=key['keyid'], fingerprint=key['fingerprint'], key_data=key_data, private=True if key['type'] == 'sec' else False, - length=key['length'], + length=int(key['length']), expiry_date=expiry_date, - validation=ValidationLevel.Weak_Chain, + refreshed_at=datetime.now(), ) @@ -397,7 +396,7 @@ class OpenPGPScheme(EncryptionScheme): :param key: The key to be stored. :type key: OpenPGPKey """ - doc = self._get_key_doc(key.address, private=key.private) + doc = self._get_key_doc(key.address[0], private=key.private) if doc is None: self._soledad.create_doc_from_json(key.get_json()) else: @@ -408,7 +407,7 @@ class OpenPGPScheme(EncryptionScheme): gpg.import_keys(key.key_data) gpgkey = gpg.list_keys(secret=key.private).pop() key = _build_key_from_gpg( - key.address, gpgkey, + key.address[0], gpgkey, gpg.export_keys(gpgkey['fingerprint'], secret=key.private)) doc.set_json(key.get_json()) @@ -452,12 +451,11 @@ class OpenPGPScheme(EncryptionScheme): :type key: EncryptionKey """ leap_assert_type(key, OpenPGPKey) - stored_key = self.get_key(key.address, private=key.private) - if stored_key is None: + doc = self._get_key_doc(key.address[0], key.private) + if doc is None: raise errors.KeyNotFound(key) - if stored_key.__dict__ != key.__dict__: + if doc.content[KEY_FINGERPRINT_KEY] != key.fingerprint: raise errors.KeyAttributesDiffer(key) - doc = self._get_key_doc(key.address, key.private) self._soledad.delete_doc(doc) # diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 6a877bc..4daf346 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -21,6 +21,7 @@ Tests for the Key Manager. """ +from datetime import datetime from mock import Mock from leap.common.testing.basetest import BaseLeapTest from leap.keymanager import ( @@ -75,16 +76,18 @@ class KeyManagerUtilTestCase(BaseLeapTest): def test_build_key_from_dict(self): kdict = { - 'address': ADDRESS, - 'key_id': 'key_id', - 'fingerprint': 'fingerprint', - 'key_data': 'key_data', - 'private': 'private', - 'length': 'length', - 'expiry_date': '', - 'first_seen_at': 'first_seen_at', - 'last_audited_at': 'last_audited_at', + 'address': [ADDRESS], + 'key_id': KEY_FINGERPRINT[-16:], + 'fingerprint': KEY_FINGERPRINT, + 'key_data': PUBLIC_KEY, + 'private': False, + 'length': 4096, + 'expiry_date': 0, + 'last_audited_at': 0, + 'refreshed_at': 1311239602, 'validation': str(ValidationLevel.Weak_Chain), + 'encr_used': False, + 'sign_used': True, } key = build_key_from_dict(OpenPGPKey, ADDRESS, kdict) self.assertEqual( @@ -109,14 +112,20 @@ class KeyManagerUtilTestCase(BaseLeapTest): None, key.expiry_date, 'Wrong data in key.') self.assertEqual( - kdict['first_seen_at'], key.first_seen_at, + None, key.last_audited_at, 'Wrong data in key.') self.assertEqual( - kdict['last_audited_at'], key.last_audited_at, + datetime.fromtimestamp(kdict['refreshed_at']), key.refreshed_at, 'Wrong data in key.') self.assertEqual( toValidationLevel(kdict['validation']), key.validation, 'Wrong data in key.') + self.assertEqual( + kdict['encr_used'], key.encr_used, + 'Wrong data in key.') + self.assertEqual( + kdict['sign_used'], key.sign_used, + 'Wrong data in key.') class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): @@ -127,9 +136,9 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): key = pgp.gen_key('user@leap.se') self.assertIsInstance(key, openpgp.OpenPGPKey) self.assertEqual( - 'user@leap.se', key.address, 'Wrong address bound to key.') + ['user@leap.se'], key.address, 'Wrong address bound to key.') self.assertEqual( - '4096', key.length, 'Wrong key length.') + 4096, key.length, 'Wrong key length.') def test_openpgp_put_delete_key(self): pgp = openpgp.OpenPGPScheme( @@ -147,10 +156,10 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp.put_ascii_key(PUBLIC_KEY) key = pgp.get_key(ADDRESS, private=False) self.assertIsInstance(key, openpgp.OpenPGPKey) + self.assertTrue( + ADDRESS in key.address, 'Wrong address bound to key.') self.assertEqual( - ADDRESS, key.address, 'Wrong address bound to key.') - self.assertEqual( - '4096', key.length, 'Wrong key length.') + 4096, key.length, 'Wrong key length.') pgp.delete_key(key) self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) @@ -162,7 +171,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): self.assertRaises( KeyNotFound, pgp.get_key, ADDRESS, private=True) key = pgp.get_key(ADDRESS, private=False) - self.assertEqual(ADDRESS, key.address) + self.assertTrue(ADDRESS in key.address) self.assertFalse(key.private) self.assertEqual(KEY_FINGERPRINT, key.fingerprint) pgp.delete_key(key) @@ -311,12 +320,12 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): # get public keys keys = km.get_all_keys(False) self.assertEqual(len(keys), 1, 'Wrong number of keys') - self.assertEqual(ADDRESS, keys[0].address) + self.assertTrue(ADDRESS in keys[0].address) self.assertFalse(keys[0].private) # get private keys keys = km.get_all_keys(True) self.assertEqual(len(keys), 1, 'Wrong number of keys') - self.assertEqual(ADDRESS, keys[0].address) + self.assertTrue(ADDRESS in keys[0].address) self.assertTrue(keys[0].private) def test_get_public_key(self): @@ -326,7 +335,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): key = km.get_key(ADDRESS, OpenPGPKey, private=False, fetch_remote=False) self.assertTrue(key is not None) - self.assertEqual(key.address, ADDRESS) + self.assertTrue(ADDRESS in key.address) self.assertEqual( key.fingerprint.lower(), KEY_FINGERPRINT.lower()) self.assertFalse(key.private) @@ -338,7 +347,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): key = km.get_key(ADDRESS, OpenPGPKey, private=True, fetch_remote=False) self.assertTrue(key is not None) - self.assertEqual(key.address, ADDRESS) + self.assertTrue(ADDRESS in key.address) self.assertEqual( key.fingerprint.lower(), KEY_FINGERPRINT.lower()) self.assertTrue(key.private) @@ -430,7 +439,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): # try to get key fetching from server. key = km.get_key(ADDRESS, OpenPGPKey) self.assertIsInstance(key, OpenPGPKey) - self.assertEqual(ADDRESS, key.address) + self.assertTrue(ADDRESS in key.address) def test_put_key_ascii(self): """ @@ -441,7 +450,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km.put_raw_key(PUBLIC_KEY, OpenPGPKey) key = km.get_key(ADDRESS, OpenPGPKey) self.assertIsInstance(key, OpenPGPKey) - self.assertEqual(ADDRESS, key.address) + self.assertTrue(ADDRESS in key.address) def test_fetch_uri_ascii_key(self): """ -- cgit v1.2.3 From 5d5f60afb1878aa65156fa294cc97a12bfe497d3 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 10 Nov 2014 09:50:56 -0600 Subject: Use type instead of tags to get docs in openpgp For that that now the type is the class.__name__ instead of str(class) --- keymanager/src/leap/keymanager/__init__.py | 2 +- keymanager/src/leap/keymanager/keys.py | 8 ++++---- keymanager/src/leap/keymanager/openpgp.py | 10 ++++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 53dd9a7..0ffb6fc 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -134,7 +134,7 @@ class KeyManager(object): Return key class from string representation of key type. """ return filter( - lambda klass: str(klass) == ktype, + lambda klass: klass.__name__ == ktype, self._wrapper_map).pop() def _get(self, uri, data=None): diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index 4952b9b..5aeb794 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -71,14 +71,14 @@ KEYMANAGER_KEY_TAG = 'keymanager-key' # TAGS_PRIVATE_INDEX = 'by-tags-private' -TAGS_ADDRESS_PRIVATE_INDEX = 'by-tags-address-private' +TYPE_ADDRESS_PRIVATE_INDEX = 'by-type-address-private' INDEXES = { TAGS_PRIVATE_INDEX: [ KEY_TAGS_KEY, 'bool(%s)' % KEY_PRIVATE_KEY, ], - TAGS_ADDRESS_PRIVATE_INDEX: [ - KEY_TAGS_KEY, + TYPE_ADDRESS_PRIVATE_INDEX: [ + KEY_TYPE_KEY, KEY_ADDRESS_KEY, 'bool(%s)' % KEY_PRIVATE_KEY, ] @@ -200,7 +200,7 @@ class EncryptionKey(object): return json.dumps({ KEY_ADDRESS_KEY: self.address, - KEY_TYPE_KEY: str(self.__class__), + KEY_TYPE_KEY: self.__class__.__name__, KEY_ID_KEY: self.key_id, KEY_FINGERPRINT_KEY: self.fingerprint, KEY_DATA_KEY: self.key_data, diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 1160434..38db178 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -36,8 +36,7 @@ from leap.keymanager.keys import ( EncryptionScheme, is_address, build_key_from_dict, - KEYMANAGER_KEY_TAG, - TAGS_ADDRESS_PRIVATE_INDEX, + TYPE_ADDRESS_PRIVATE_INDEX, KEY_FINGERPRINT_KEY, KEY_DATA_KEY, ) @@ -210,6 +209,9 @@ class OpenPGPScheme(EncryptionScheme): signing and verification). """ + # type used on the soledad documents + OPENPGP_KEY_TYPE = OpenPGPKey.__name__ + def __init__(self, soledad, gpgbinary=None): """ Initialize the OpenPGP wrapper. @@ -427,8 +429,8 @@ class OpenPGPScheme(EncryptionScheme): :rtype: leap.soledad.document.SoledadDocument """ doclist = self._soledad.get_from_index( - TAGS_ADDRESS_PRIVATE_INDEX, - KEYMANAGER_KEY_TAG, + TYPE_ADDRESS_PRIVATE_INDEX, + self.OPENPGP_KEY_TYPE, address, '1' if private else '0') if len(doclist) is 0: -- cgit v1.2.3 From 4ec8d345a32b9169300974941c5bdd87d85f3915 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 10 Nov 2014 13:36:35 -0600 Subject: Implement active key document --- keymanager/changes/feature-6299_new_doc | 2 + keymanager/src/leap/keymanager/__init__.py | 25 ++--- keymanager/src/leap/keymanager/keys.py | 25 +++++ keymanager/src/leap/keymanager/openpgp.py | 146 ++++++++++++++++++++++++----- 4 files changed, 163 insertions(+), 35 deletions(-) create mode 100644 keymanager/changes/feature-6299_new_doc diff --git a/keymanager/changes/feature-6299_new_doc b/keymanager/changes/feature-6299_new_doc new file mode 100644 index 0000000..c893338 --- /dev/null +++ b/keymanager/changes/feature-6299_new_doc @@ -0,0 +1,2 @@ +- new soledad doc struct for encryption-keys (closes #6299) +- keep old key after upgrade (closes #6262) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 0ffb6fc..562bfbf 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -519,15 +519,24 @@ class KeyManager(object): except IndexError as e: leap_assert(False, "Unsupported key type. Error {0!r}".format(e)) - def put_key(self, key): + def put_key(self, key, address=None): """ Put C{key} in local storage. :param key: The key to be stored :type key: EncryptionKey + :param address: address for which this key will be active. If not set + all the uids will be activated + :type address: str + + :raises KeyAddressMismatch: if address doesn't match any uid on the key :raises KeyNotValidUpdate: if a key with the same uid exists and the new one is not a valid update for it """ + if address is not None and address not in key.address: + raise KeyAddressMismatch("UID %s found, but expected %s" + % (str(key.address), address)) + try: old_key = self._wrapper_map[type(key)].get_key(key.address[0], private=key.private) @@ -536,7 +545,7 @@ class KeyManager(object): if key.private or can_upgrade(key, old_key): try: - self._wrapper_map[type(key)].put_key(key) + self._wrapper_map[type(key)].put_key(key, address) except IndexError as e: leap_assert( False, "Unsupported key type. Error {0!r}".format(e)) @@ -553,7 +562,7 @@ class KeyManager(object): :type key: str :param ktype: the type of the key. :type ktype: subclass of EncryptionKey - :param address: if set used to check that the key is for this address + :param address: address for which this key will be active :type address: str :param validation: validation level for this key (default: 'Weak_Chain') @@ -564,12 +573,9 @@ class KeyManager(object): new one is not a valid update for it """ pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(key) - if address is not None and address not in pubkey.address: - raise KeyAddressMismatch("Key UID %s, but expected %s" - % (pubkey.address, address)) pubkey.validation = validation - self.put_key(pubkey) + self.put_key(pubkey, address) def fetch_key(self, address, uri, ktype, validation=ValidationLevel.Weak_Chain): @@ -600,12 +606,9 @@ class KeyManager(object): pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(res.content) if pubkey is None: raise KeyNotFound(uri) - if address not in pubkey.address: - raise KeyAddressMismatch("UID %s found, but expected %s" - % (str(pubkey.address), address)) pubkey.validation = validation - self.put_key(pubkey) + self.put_key(pubkey, address) from ._version import get_versions __version__ = get_versions()['version'] diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index 5aeb794..d1b5d93 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -64,6 +64,8 @@ KEY_TAGS_KEY = 'tags' # KEYMANAGER_KEY_TAG = 'keymanager-key' +KEYMANAGER_ACTIVE_TAG = 'keymanager-active' +KEYMANAGER_ACTIVE_TYPE = '-active' # @@ -71,12 +73,18 @@ KEYMANAGER_KEY_TAG = 'keymanager-key' # TAGS_PRIVATE_INDEX = 'by-tags-private' +TYPE_ID_PRIVATE_INDEX = 'by-type-id-private' TYPE_ADDRESS_PRIVATE_INDEX = 'by-type-address-private' INDEXES = { TAGS_PRIVATE_INDEX: [ KEY_TAGS_KEY, 'bool(%s)' % KEY_PRIVATE_KEY, ], + TYPE_ID_PRIVATE_INDEX: [ + KEY_TYPE_KEY, + KEY_ID_KEY, + 'bool(%s)' % KEY_PRIVATE_KEY, + ], TYPE_ADDRESS_PRIVATE_INDEX: [ KEY_TYPE_KEY, KEY_ADDRESS_KEY, @@ -215,6 +223,23 @@ class EncryptionKey(object): KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG], }) + def get_active_json(self, address): + """ + Return a JSON string describing this key. + + :param address: Address for wich the key is active + :type address: str + :return: The JSON string describing this key. + :rtype: str + """ + return json.dumps({ + KEY_ADDRESS_KEY: address, + KEY_TYPE_KEY: self.__class__.__name__ + KEYMANAGER_ACTIVE_TYPE, + KEY_ID_KEY: self.key_id, + KEY_PRIVATE_KEY: self.private, + KEY_TAGS_KEY: [KEYMANAGER_ACTIVE_TAG], + }) + def __repr__(self): """ Representation of this class diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 38db178..52655d0 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -36,9 +36,12 @@ from leap.keymanager.keys import ( EncryptionScheme, is_address, build_key_from_dict, + TYPE_ID_PRIVATE_INDEX, TYPE_ADDRESS_PRIVATE_INDEX, KEY_FINGERPRINT_KEY, KEY_DATA_KEY, + KEY_ID_KEY, + KEYMANAGER_ACTIVE_TYPE, ) @@ -193,6 +196,18 @@ def _build_key_from_gpg(address, key, key_data): ) +def _parse_address(address): + """ + Remove the identity suffix after the '+' until the '@' + e.g.: test_user+something@provider.com becomes test_user@provider.com + since the key belongs to the identity without the '+' suffix. + + :type address: str + :rtype: str + """ + return re.sub(r'\+.*\@', '@', address) + + # # The OpenPGP wrapper # @@ -210,7 +225,8 @@ class OpenPGPScheme(EncryptionScheme): """ # type used on the soledad documents - OPENPGP_KEY_TYPE = OpenPGPKey.__name__ + KEY_TYPE = OpenPGPKey.__name__ + ACTIVE_TYPE = KEY_TYPE + KEYMANAGER_ACTIVE_TYPE def __init__(self, soledad, gpgbinary=None): """ @@ -282,7 +298,7 @@ class OpenPGPScheme(EncryptionScheme): openpgp_key = _build_key_from_gpg( address, key, gpg.export_keys(key['fingerprint'], secret=secret)) - self.put_key(openpgp_key) + self.put_key(openpgp_key, address) return self.get_key(address, private=True) @@ -299,10 +315,7 @@ class OpenPGPScheme(EncryptionScheme): :rtype: OpenPGPKey @raise KeyNotFound: If the key was not found on local storage. """ - # Remove the identity suffix after the '+' until the '@' - # e.g.: test_user+something@provider.com becomes test_user@provider.com - # since the key belongs to the identity without the '+' suffix. - address = re.sub(r'\+.*\@', '@', address) + address = _parse_address(address) doc = self._get_key_doc(address, private) if doc is None: @@ -371,12 +384,15 @@ class OpenPGPScheme(EncryptionScheme): return (openpgp_pubkey, openpgp_privkey) - def put_ascii_key(self, key_data): + def put_ascii_key(self, key_data, address=None): """ Put key contained in ascii-armored C{key_data} in local storage. :param key_data: The key data to be stored. :type key_data: str or unicode + :param address: address for which this key will be active. If not set + all the uids will be activated + :type address: str """ leap_assert_type(key_data, (str, unicode)) @@ -387,21 +403,41 @@ class OpenPGPScheme(EncryptionScheme): leap_assert(False, repr(e)) if openpgp_pubkey is not None: - self.put_key(openpgp_pubkey) + self.put_key(openpgp_pubkey, address) if openpgp_privkey is not None: - self.put_key(openpgp_privkey) + self.put_key(openpgp_privkey, address) - def put_key(self, key): + def put_key(self, key, address=None): """ Put C{key} in local storage. :param key: The key to be stored. :type key: OpenPGPKey + :param address: address for which this key will be active. If not set + all the uids will be activated + :type address: str """ - doc = self._get_key_doc(key.address[0], private=key.private) - if doc is None: - self._soledad.create_doc_from_json(key.get_json()) + if address is not None: + active_address = [_parse_address(address)] else: + active_address = key.address + + self._put_key_doc(key) + self._put_active_doc(key, active_address) + + def _put_key_doc(self, key): + """ + Put key document in soledad + + :type key: OpenPGPKey + """ + docs = self._soledad.get_from_index( + TYPE_ID_PRIVATE_INDEX, + self.KEY_TYPE, + key.key_id, + '1' if key.private else '0') + if len(docs) != 0: + doc = docs.pop() if key.fingerprint == doc.content[KEY_FINGERPRINT_KEY]: # in case of an update of the key merge them with gnupg with self._temporary_gpgwrapper() as gpg: @@ -412,8 +448,41 @@ class OpenPGPScheme(EncryptionScheme): key.address[0], gpgkey, gpg.export_keys(gpgkey['fingerprint'], secret=key.private)) - doc.set_json(key.get_json()) - self._soledad.put_doc(doc) + doc.set_json(key.get_json()) + self._soledad.put_doc(doc) + else: + logger.critical( + "Can't put a key whith the same key_id and different " + "fingerprint: %s, %s" + % (key.fingerprint, doc.content[KEY_FINGERPRINT_KEY])) + else: + self._soledad.create_doc_from_json(key.get_json()) + + def _put_active_doc(self, key, addresses): + """ + Put active key document in soledad + + :type key: OpenPGPKey + :type addresses: list(str) + """ + for address in addresses: + docs = self._soledad.get_from_index( + TYPE_ADDRESS_PRIVATE_INDEX, + self.ACTIVE_TYPE, + address, + '1' if key.private else '0') + if len(docs) == 1: + doc = docs.pop() + doc.set_json(key.get_active_json(address)) + self._soledad.put_doc(doc) + else: + if len(docs) > 1: + logger.error("There is more than one active key document " + "for the address %s" % (address,)) + for doc in docs: + self._soledad.delete_doc(doc) + self._soledad.create_doc_from_json( + key.get_active_json(address)) def _get_key_doc(self, address, private=False): """ @@ -428,17 +497,26 @@ class OpenPGPScheme(EncryptionScheme): :return: The document with the key or None if it does not exist. :rtype: leap.soledad.document.SoledadDocument """ - doclist = self._soledad.get_from_index( + activedoc = self._soledad.get_from_index( TYPE_ADDRESS_PRIVATE_INDEX, - self.OPENPGP_KEY_TYPE, + self.ACTIVE_TYPE, address, '1' if private else '0') - if len(doclist) is 0: + if len(activedoc) is 0: return None + leap_assert( + len(activedoc) is 1, + 'Found more than one key for address %s!' % (address,)) + + key_id = activedoc[0].content[KEY_ID_KEY] + doclist = self._soledad.get_from_index( + TYPE_ID_PRIVATE_INDEX, + self.KEY_TYPE, + key_id, + '1' if private else '0') leap_assert( len(doclist) is 1, - 'Found more than one %s key for address!' % - 'private' if private else 'public') + 'There is %d keys for id %s!' % (len(doclist), key_id)) return doclist.pop() def delete_key(self, key): @@ -447,17 +525,37 @@ class OpenPGPScheme(EncryptionScheme): May raise: errors.KeyNotFound - errors.KeyAttributesDiffer :param key: The key to be removed. :type key: EncryptionKey """ leap_assert_type(key, OpenPGPKey) - doc = self._get_key_doc(key.address[0], key.private) + activedocs = self._soledad.get_from_index( + TYPE_ID_PRIVATE_INDEX, + self.ACTIVE_TYPE, + key.key_id, + '1' if key.private else '0') + for doc in activedocs: + self._soledad.delete_doc(doc) + + docs = self._soledad.get_from_index( + TYPE_ID_PRIVATE_INDEX, + self.KEY_TYPE, + key.key_id, + '1' if key.private else '0') + if len(docs) == 0: + raise errors.KeyNotFound(key) + if len(docs) > 1: + logger.critical("There is more than one key for key_id %s" + % key.key_id) + + doc = None + for d in docs: + if d.content['fingerprint'] == key.fingerprint: + doc = d + break if doc is None: raise errors.KeyNotFound(key) - if doc.content[KEY_FINGERPRINT_KEY] != key.fingerprint: - raise errors.KeyAttributesDiffer(key) self._soledad.delete_doc(doc) # -- cgit v1.2.3 From 1d1274e57f5e466fb195c829a628f519ef51186a Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 10 Nov 2014 18:36:59 -0600 Subject: Implement multi uid support --- keymanager/changes/feature-6212_multi_uid_support | 1 + keymanager/src/leap/keymanager/__init__.py | 12 +-- keymanager/src/leap/keymanager/keys.py | 15 ++- keymanager/src/leap/keymanager/openpgp.py | 120 ++++++++++----------- .../src/leap/keymanager/tests/test_keymanager.py | 48 ++++----- .../src/leap/keymanager/tests/test_validation.py | 21 ++-- keymanager/src/leap/keymanager/validation.py | 4 - 7 files changed, 101 insertions(+), 120 deletions(-) create mode 100644 keymanager/changes/feature-6212_multi_uid_support diff --git a/keymanager/changes/feature-6212_multi_uid_support b/keymanager/changes/feature-6212_multi_uid_support new file mode 100644 index 0000000..495b8c4 --- /dev/null +++ b/keymanager/changes/feature-6212_multi_uid_support @@ -0,0 +1 @@ +- Multi uid support (closes #6212) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 562bfbf..c64cdea 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -319,7 +319,6 @@ class KeyManager(object): return map( lambda doc: build_key_from_dict( self._key_class_from_type(doc.content['type']), - doc.content['address'][0], doc.content), self._soledad.get_from_index( TAGS_PRIVATE_INDEX, @@ -519,26 +518,25 @@ class KeyManager(object): except IndexError as e: leap_assert(False, "Unsupported key type. Error {0!r}".format(e)) - def put_key(self, key, address=None): + def put_key(self, key, address): """ Put C{key} in local storage. :param key: The key to be stored :type key: EncryptionKey - :param address: address for which this key will be active. If not set - all the uids will be activated + :param address: address for which this key will be active :type address: str :raises KeyAddressMismatch: if address doesn't match any uid on the key :raises KeyNotValidUpdate: if a key with the same uid exists and the new one is not a valid update for it """ - if address is not None and address not in key.address: + if address not in key.address: raise KeyAddressMismatch("UID %s found, but expected %s" % (str(key.address), address)) try: - old_key = self._wrapper_map[type(key)].get_key(key.address[0], + old_key = self._wrapper_map[type(key)].get_key(address, private=key.private) except KeyNotFound: old_key = None @@ -553,7 +551,7 @@ class KeyManager(object): raise KeyNotValidUpgrade("Key %s can not be upgraded by new key %s" % (old_key.key_id, key.key_id)) - def put_raw_key(self, key, ktype, address=None, + def put_raw_key(self, key, ktype, address, validation=ValidationLevel.Weak_Chain): """ Put C{key} in local storage. diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index d1b5d93..2108aa2 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -109,20 +109,15 @@ def is_address(address): return bool(re.match('[\w.-]+@[\w.-]+', address)) -def build_key_from_dict(kClass, address, kdict): +def build_key_from_dict(kClass, kdict): """ - Build an C{kClass} key bound to C{address} based on info in C{kdict}. + Build an C{kClass} key based on info in C{kdict}. - :param address: The address bound to the key. - :type address: str :param kdict: Dictionary with key data. :type kdict: dict :return: An instance of the key. :rtype: C{kClass} """ - leap_assert( - address in kdict[KEY_ADDRESS_KEY], - 'Wrong address in key data.') try: validation = toValidationLevel(kdict[KEY_VALIDATION_KEY]) except ValueError: @@ -135,7 +130,7 @@ def build_key_from_dict(kClass, address, kdict): refreshed_at = _to_datetime(kdict[KEY_REFRESHED_AT_KEY]) return kClass( - [address], + kdict[KEY_ADDRESS_KEY], key_id=kdict[KEY_ID_KEY], fingerprint=kdict[KEY_FINGERPRINT_KEY], key_data=kdict[KEY_DATA_KEY], @@ -315,12 +310,14 @@ class EncryptionScheme(object): pass @abstractmethod - def put_key(self, key): + def put_key(self, key, address): """ Put a key in local storage. :param key: The key to be stored. :type key: EncryptionKey + :param address: address for which this key will be active. + :type address: str """ pass diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 52655d0..4f96574 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -38,6 +38,7 @@ from leap.keymanager.keys import ( build_key_from_dict, TYPE_ID_PRIVATE_INDEX, TYPE_ADDRESS_PRIVATE_INDEX, + KEY_ADDRESS_KEY, KEY_FINGERPRINT_KEY, KEY_DATA_KEY, KEY_ID_KEY, @@ -110,9 +111,9 @@ class TempGPGWrapper(object): # itself is enough to also have the public key in the keyring, # and we want to count the keys afterwards. - privaddrs = map(lambda privkey: privkey.address[0], privkeys) + privids = map(lambda privkey: privkey.key_id, privkeys) publkeys = filter( - lambda pubkey: pubkey.address[0] not in privaddrs, publkeys) + lambda pubkey: pubkey.key_id not in privids, publkeys) listkeys = lambda: self._gpg.list_keys() listsecretkeys = lambda: self._gpg.list_keys(secret=True) @@ -163,16 +164,13 @@ class TempGPGWrapper(object): shutil.rmtree(self._gpg.homedir) -def _build_key_from_gpg(address, key, key_data): +def _build_key_from_gpg(key, key_data): """ - Build an OpenPGPKey for C{address} based on C{key} from - local gpg storage. + Build an OpenPGPKey based on C{key} from local gpg storage. ASCII armored GPG key data has to be queried independently in this wrapper, so we receive it in C{key_data}. - :param address: The address bound to the key. - :type address: str :param key: Key obtained from GPG storage. :type key: dict :param key_data: Key data obtained from GPG storage. @@ -183,9 +181,12 @@ def _build_key_from_gpg(address, key, key_data): expiry_date = None if key['expires']: expiry_date = datetime.fromtimestamp(int(key['expires'])) + address = [] + for uid in key['uids']: + address.append(_parse_address(uid)) return OpenPGPKey( - [address], + address, key_id=key['keyid'], fingerprint=key['fingerprint'], key_data=key_data, @@ -198,14 +199,18 @@ def _build_key_from_gpg(address, key, key_data): def _parse_address(address): """ - Remove the identity suffix after the '+' until the '@' + Remove name, '<', '>' and the identity suffix after the '+' until the '@' e.g.: test_user+something@provider.com becomes test_user@provider.com since the key belongs to the identity without the '+' suffix. :type address: str :rtype: str """ - return re.sub(r'\+.*\@', '@', address) + mail_regex = '(.*<)?([\w.-]+)(\+.*)?(@[\w.-]+)(>.*)?' + match = re.match(mail_regex, address) + if match is None: + return None + return ''.join(match.group(2, 4)) # @@ -289,15 +294,17 @@ class OpenPGPScheme(EncryptionScheme): leap_assert( len(key['uids']) is 1, # with just one uid! 'Wrong number of uids for key: %d.' % len(key['uids'])) - leap_assert( - re.match('.*<%s>$' % address, key['uids'][0]) is not None, - 'Key not correctly bound to address.') + uid_match = False + for uid in key['uids']: + if re.match('.*<%s>$' % address, uid) is not None: + uid_match = True + return + leap_assert(uid_match, 'Key not correctly bound to address.') # insert both public and private keys in storage for secret in [True, False]: key = gpg.list_keys(secret=secret).pop() openpgp_key = _build_key_from_gpg( - address, key, - gpg.export_keys(key['fingerprint'], secret=secret)) + key, gpg.export_keys(key['fingerprint'], secret=secret)) self.put_key(openpgp_key, address) return self.get_key(address, private=True) @@ -320,7 +327,10 @@ class OpenPGPScheme(EncryptionScheme): doc = self._get_key_doc(address, private) if doc is None: raise errors.KeyNotFound(address) - return build_key_from_dict(OpenPGPKey, address, doc.content) + leap_assert( + address in doc.content[KEY_ADDRESS_KEY], + 'Wrong address in key data.') + return build_key_from_dict(OpenPGPKey, doc.content) def parse_ascii_key(self, key_data): """ @@ -337,7 +347,6 @@ class OpenPGPScheme(EncryptionScheme): leap_assert_type(key_data, (str, unicode)) # TODO: add more checks for correct key data. leap_assert(key_data is not None, 'Data does not represent a key.') - mail_regex = '.*<([\w.-]+@[\w.-]+)>.*' with self._temporary_gpgwrapper() as gpg: # TODO: inspect result, or use decorator @@ -354,44 +363,30 @@ class OpenPGPScheme(EncryptionScheme): except IndexError: return (None, None) - # extract adress from first uid on key - match = re.match(mail_regex, pubkey['uids'].pop()) - leap_assert(match is not None, 'No user address in key data.') - address = match.group(1) - openpgp_privkey = None if privkey is not None: - match = re.match(mail_regex, privkey['uids'].pop()) - leap_assert(match is not None, 'No user address in key data.') - privaddress = match.group(1) - # build private key openpgp_privkey = _build_key_from_gpg( - privaddress, privkey, + privkey, gpg.export_keys(privkey['fingerprint'], secret=True)) - - leap_check(address == privaddress, - 'Addresses in public and private key differ.', - errors.KeyAddressMismatch) leap_check(pubkey['fingerprint'] == privkey['fingerprint'], 'Fingerprints for public and private key differ.', errors.KeyFingerprintMismatch) # build public key openpgp_pubkey = _build_key_from_gpg( - address, pubkey, + pubkey, gpg.export_keys(pubkey['fingerprint'], secret=False)) return (openpgp_pubkey, openpgp_privkey) - def put_ascii_key(self, key_data, address=None): + def put_ascii_key(self, key_data, address): """ Put key contained in ascii-armored C{key_data} in local storage. :param key_data: The key data to be stored. :type key_data: str or unicode - :param address: address for which this key will be active. If not set - all the uids will be activated + :param address: address for which this key will be active :type address: str """ leap_assert_type(key_data, (str, unicode)) @@ -407,23 +402,17 @@ class OpenPGPScheme(EncryptionScheme): if openpgp_privkey is not None: self.put_key(openpgp_privkey, address) - def put_key(self, key, address=None): + def put_key(self, key, address): """ Put C{key} in local storage. :param key: The key to be stored. :type key: OpenPGPKey - :param address: address for which this key will be active. If not set - all the uids will be activated + :param address: address for which this key will be active. :type address: str """ - if address is not None: - active_address = [_parse_address(address)] - else: - active_address = key.address - self._put_key_doc(key) - self._put_active_doc(key, active_address) + self._put_active_doc(key, address) def _put_key_doc(self, key): """ @@ -445,7 +434,7 @@ class OpenPGPScheme(EncryptionScheme): gpg.import_keys(key.key_data) gpgkey = gpg.list_keys(secret=key.private).pop() key = _build_key_from_gpg( - key.address[0], gpgkey, + gpgkey, gpg.export_keys(gpgkey['fingerprint'], secret=key.private)) doc.set_json(key.get_json()) @@ -458,31 +447,30 @@ class OpenPGPScheme(EncryptionScheme): else: self._soledad.create_doc_from_json(key.get_json()) - def _put_active_doc(self, key, addresses): + def _put_active_doc(self, key, address): """ Put active key document in soledad :type key: OpenPGPKey - :type addresses: list(str) - """ - for address in addresses: - docs = self._soledad.get_from_index( - TYPE_ADDRESS_PRIVATE_INDEX, - self.ACTIVE_TYPE, - address, - '1' if key.private else '0') - if len(docs) == 1: - doc = docs.pop() - doc.set_json(key.get_active_json(address)) - self._soledad.put_doc(doc) - else: - if len(docs) > 1: - logger.error("There is more than one active key document " - "for the address %s" % (address,)) - for doc in docs: - self._soledad.delete_doc(doc) - self._soledad.create_doc_from_json( - key.get_active_json(address)) + :type addresses: str + """ + docs = self._soledad.get_from_index( + TYPE_ADDRESS_PRIVATE_INDEX, + self.ACTIVE_TYPE, + address, + '1' if key.private else '0') + if len(docs) == 1: + doc = docs.pop() + doc.set_json(key.get_active_json(address)) + self._soledad.put_doc(doc) + else: + if len(docs) > 1: + logger.error("There is more than one active key document " + "for the address %s" % (address,)) + for doc in docs: + self._soledad.delete_doc(doc) + self._soledad.create_doc_from_json( + key.get_active_json(address)) def _get_key_doc(self, address, private=False): """ diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 4daf346..6aeb67a 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -89,7 +89,7 @@ class KeyManagerUtilTestCase(BaseLeapTest): 'encr_used': False, 'sign_used': True, } - key = build_key_from_dict(OpenPGPKey, ADDRESS, kdict) + key = build_key_from_dict(OpenPGPKey, kdict) self.assertEqual( kdict['address'], key.address, 'Wrong data in key.') @@ -144,7 +144,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - pgp.put_ascii_key(PUBLIC_KEY) + pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) key = pgp.get_key(ADDRESS, private=False) pgp.delete_key(key) self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) @@ -153,7 +153,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - pgp.put_ascii_key(PUBLIC_KEY) + pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) key = pgp.get_key(ADDRESS, private=False) self.assertIsInstance(key, openpgp.OpenPGPKey) self.assertTrue( @@ -167,7 +167,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - pgp.put_ascii_key(PUBLIC_KEY) + pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) self.assertRaises( KeyNotFound, pgp.get_key, ADDRESS, private=True) key = pgp.get_key(ADDRESS, private=False) @@ -181,7 +181,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): # encrypt pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PUBLIC_KEY) + pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) pubkey = pgp.get_key(ADDRESS, private=False) cyphertext = pgp.encrypt('data', pubkey) # assert @@ -193,7 +193,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): # decrypt self.assertRaises( KeyNotFound, pgp.get_key, ADDRESS, private=True) - pgp.put_ascii_key(PRIVATE_KEY) + pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = pgp.get_key(ADDRESS, private=True) pgp.delete_key(pubkey) pgp.delete_key(privkey) @@ -216,7 +216,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_sign_with_public_raises(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PUBLIC_KEY) + pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) data = 'data' pubkey = pgp.get_key(ADDRESS, private=False) self.assertRaises( @@ -226,11 +226,11 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_verify_with_wrong_key_raises(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY) + pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) signed = pgp.sign(data, privkey) - pgp.put_ascii_key(PUBLIC_KEY_2) + pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) wrongkey = pgp.get_key(ADDRESS_2) self.assertRaises( errors.InvalidSignature, @@ -239,7 +239,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_encrypt_sign_with_public_raises(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY) + pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) pubkey = pgp.get_key(ADDRESS, private=False) @@ -250,7 +250,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_decrypt_verify_with_private_raises(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY) + pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) pubkey = pgp.get_key(ADDRESS, private=False) @@ -264,12 +264,12 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_decrypt_verify_with_wrong_key_raises(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY) + pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) pubkey = pgp.get_key(ADDRESS, private=False) encrypted_and_signed = pgp.encrypt(data, pubkey, sign=privkey) - pgp.put_ascii_key(PUBLIC_KEY_2) + pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) wrongkey = pgp.get_key(ADDRESS_2) self.assertRaises( errors.InvalidSignature, @@ -278,7 +278,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_sign_verify(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY) + pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) signed = pgp.sign(data, privkey, detach=False) @@ -288,10 +288,10 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_encrypt_sign_decrypt_verify(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY) + pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) pubkey = pgp.get_key(ADDRESS, private=False) privkey = pgp.get_key(ADDRESS, private=True) - pgp.put_ascii_key(PRIVATE_KEY_2) + pgp.put_ascii_key(PRIVATE_KEY_2, ADDRESS_2) pubkey2 = pgp.get_key(ADDRESS_2, private=False) privkey2 = pgp.get_key(ADDRESS_2, private=True) data = 'data' @@ -304,7 +304,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_sign_verify_detached_sig(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY) + pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) signature = pgp.sign(data, privkey, detach=True) @@ -316,7 +316,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): def test_get_all_keys_in_db(self): km = self._key_manager() - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) # get public keys keys = km.get_all_keys(False) self.assertEqual(len(keys), 1, 'Wrong number of keys') @@ -330,7 +330,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): def test_get_public_key(self): km = self._key_manager() - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) # get the key key = km.get_key(ADDRESS, OpenPGPKey, private=False, fetch_remote=False) @@ -342,7 +342,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): def test_get_private_key(self): km = self._key_manager() - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) # get the key key = km.get_key(ADDRESS, OpenPGPKey, private=True, fetch_remote=False) @@ -364,7 +364,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): """ token = "mytoken" km = self._key_manager(token=token) - km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY) + km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY, ADDRESS) km._fetcher.put = Mock() # the following data will be used on the send km.ca_cert_path = 'capath' @@ -447,7 +447,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): """ km = self._key_manager(url='http://nickserver.domain') - km.put_raw_key(PUBLIC_KEY, OpenPGPKey) + km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) key = km.get_key(ADDRESS, OpenPGPKey) self.assertIsInstance(key, OpenPGPKey) self.assertTrue(ADDRESS in key.address) @@ -509,7 +509,7 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): def test_keymanager_openpgp_encrypt_decrypt(self): km = self._key_manager() # put raw private key - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) # get public key pubkey = km.get_key( ADDRESS, OpenPGPKey, private=False, fetch_remote=False) @@ -526,7 +526,7 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): def test_keymanager_openpgp_sign_verify(self): km = self._key_manager() # put raw private keys - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY) + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) # get private key for signing privkey = km.get_key( ADDRESS, OpenPGPKey, private=True, fetch_remote=False) diff --git a/keymanager/src/leap/keymanager/tests/test_validation.py b/keymanager/src/leap/keymanager/tests/test_validation.py index 3ae873d..400d36e 100644 --- a/keymanager/src/leap/keymanager/tests/test_validation.py +++ b/keymanager/src/leap/keymanager/tests/test_validation.py @@ -37,47 +37,48 @@ class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): def test_none_old_key(self): km = self._key_manager() - km.put_raw_key(PUBLIC_KEY, OpenPGPKey) + km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.fingerprint, KEY_FINGERPRINT) def test_cant_upgrade(self): km = self._key_manager() - km.put_raw_key(PUBLIC_KEY, OpenPGPKey, + km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS, validation=ValidationLevel.Provider_Trust) self.assertRaises(KeyNotValidUpgrade, km.put_raw_key, UNRELATED_KEY, - OpenPGPKey) + OpenPGPKey, ADDRESS) def test_fingerprint_level(self): km = self._key_manager() - km.put_raw_key(PUBLIC_KEY, OpenPGPKey) - km.put_raw_key(UNRELATED_KEY, OpenPGPKey, + km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) + km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS, validation=ValidationLevel.Fingerprint) key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT) def test_expired_key(self): km = self._key_manager() - km.put_raw_key(EXPIRED_KEY, OpenPGPKey) - km.put_raw_key(UNRELATED_KEY, OpenPGPKey) + km.put_raw_key(EXPIRED_KEY, OpenPGPKey, ADDRESS) + km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS) key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT) def test_expired_fail_lower_level(self): km = self._key_manager() - km.put_raw_key(EXPIRED_KEY, OpenPGPKey, + km.put_raw_key(EXPIRED_KEY, OpenPGPKey, ADDRESS, validation=ValidationLevel.Third_Party_Endorsement) self.assertRaises( KeyNotValidUpgrade, km.put_raw_key, UNRELATED_KEY, OpenPGPKey, + ADDRESS, validation=ValidationLevel.Provider_Trust) def test_roll_back(self): km = self._key_manager() - km.put_raw_key(EXPIRED_KEY_UPDATED, OpenPGPKey) - km.put_raw_key(EXPIRED_KEY, OpenPGPKey) + km.put_raw_key(EXPIRED_KEY_UPDATED, OpenPGPKey, ADDRESS) + km.put_raw_key(EXPIRED_KEY, OpenPGPKey, ADDRESS) key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.expiry_date, EXPIRED_KEY_NEW_EXPIRY_DATE) diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index cf5b4a8..245013e 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -67,10 +67,6 @@ def can_upgrade(new_key, old_key): if old_key is None: return True - if new_key.address != old_key.address: - # XXX how do we map multiple IDs? (#6212) - return False - # An update of the same key if new_key.fingerprint == old_key.fingerprint: return True -- cgit v1.2.3 From b40e61f4d7624250325378d51a289a92773e785c Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 10 Nov 2014 19:00:04 -0600 Subject: Fix comments --- keymanager/src/leap/keymanager/__init__.py | 4 ++-- keymanager/src/leap/keymanager/keys.py | 4 ++-- keymanager/src/leap/keymanager/openpgp.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index c64cdea..e4be9c4 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -493,8 +493,8 @@ class KeyManager(object): verified using this detached signature. :type detached_sig: str - :return: The signed data. - :rtype: str + :return: signature matches + :rtype: bool """ leap_assert_type(pubkey, EncryptionKey) leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.') diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index 2108aa2..7c732e3 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -411,7 +411,7 @@ class EncryptionScheme(object): verified against this sdetached signature. :type detached_sig: str - :return: The signed data. - :rtype: str + :return: signature matches + :rtype: bool """ pass diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 4f96574..3f298f7 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -742,8 +742,8 @@ class OpenPGPScheme(EncryptionScheme): verified against this detached signature. :type detached_sig: str - :return: The ascii-armored signed data. - :rtype: str + :return: signature matches + :rtype: bool """ leap_assert_type(pubkey, OpenPGPKey) leap_assert(pubkey.private is False) -- cgit v1.2.3 From 66118b3797ca554c293ea2459856af2480de2b1b Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 25 Nov 2014 12:35:52 -0200 Subject: Add enum as a dependency. --- keymanager/pkg/requirements.pip | 1 + 1 file changed, 1 insertion(+) diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index 8dd84bf..f3cdae9 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -4,3 +4,4 @@ requests # if we bump the gnupg version, bump also the sanity check # in keymanager.__init__ gnupg>=1.2.3 +enum -- cgit v1.2.3 From d3a7f97ebc32434cd012f4cd5d334e775e2ead49 Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 25 Nov 2014 12:36:24 -0200 Subject: Fix test to use correct new signature for openpgp.put_ascii_key(). --- keymanager/src/leap/keymanager/tests/test_keymanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 6aeb67a..319d2e1 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -205,7 +205,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_verify_with_private_raises(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY) + pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) data = 'data' privkey = pgp.get_key(ADDRESS, private=True) signed = pgp.sign(data, privkey) -- cgit v1.2.3 From ae11553bc7b7cf94aa1672e6227d75f2c287fd9b Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 20 Nov 2014 10:55:09 -0600 Subject: Use addresses instead of keys on the public API --- keymanager/changes/feature-6346_use_addresses | 1 + keymanager/src/leap/keymanager/__init__.py | 125 +++++++++++++-------- .../src/leap/keymanager/tests/test_keymanager.py | 25 ++--- 3 files changed, 86 insertions(+), 65 deletions(-) create mode 100644 keymanager/changes/feature-6346_use_addresses diff --git a/keymanager/changes/feature-6346_use_addresses b/keymanager/changes/feature-6346_use_addresses new file mode 100644 index 0000000..2f32c98 --- /dev/null +++ b/keymanager/changes/feature-6346_use_addresses @@ -0,0 +1 @@ +- Use addresses instead of keys for encrypt, decrypt, sign & verify (closes #6346) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index e4be9c4..1704e0b 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -43,7 +43,7 @@ except (ImportError, AssertionError): import logging import requests -from leap.common.check import leap_assert, leap_assert_type +from leap.common.check import leap_assert from leap.common.events import signal from leap.common.events import events_pb2 as proto from leap.common.decorators import memoized_method @@ -56,7 +56,6 @@ from leap.keymanager.errors import ( from leap.keymanager.validation import ValidationLevel, can_upgrade from leap.keymanager.keys import ( - EncryptionKey, build_key_from_dict, KEYMANAGER_KEY_TAG, TAGS_PRIVATE_INDEX, @@ -395,70 +394,92 @@ class KeyManager(object): # encrypt/decrypt and sign/verify API # - def encrypt(self, data, pubkey, passphrase=None, sign=None, - cipher_algo='AES256'): + def encrypt(self, data, address, ktype, passphrase=None, sign=None, + cipher_algo='AES256', fetch_remote=True): """ - Encrypt C{data} using public @{key} and sign with C{sign} key. + Encrypt C{data} for C{address} and sign with C{sign} address. :param data: The data to be encrypted. :type data: str - :param pubkey: The key used to encrypt. - :type pubkey: EncryptionKey + :param address: The address to encrypt it for. + :type address: str + :param ktype: The type of the key. + :type ktype: subclass of EncryptionKey :param passphrase: The passphrase for the secret key used for the signature. :type passphrase: str - :param sign: The key used for signing. - :type sign: EncryptionKey + :param sign: The address to be used for signature. + :type sign: str :param cipher_algo: The cipher algorithm to use. :type cipher_algo: str + :param fetch_remote: If key not found in local storage try to fetch + from nickserver + :type fetch_remote: bool :return: The encrypted data. :rtype: str + + :raise KeyNotFound: If any of the keys was not found both locally and + in keyserver. + :raise EncryptError: Raised if failed encrypting for some reason. """ - leap_assert_type(pubkey, EncryptionKey) - leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.') - leap_assert(pubkey.private is False, 'Key is not public.') - return self._wrapper_map[pubkey.__class__].encrypt( - data, pubkey, passphrase, sign, cipher_algo=cipher_algo) + pubkey = self.get_key(address, ktype, private=False, + fetch_remote=fetch_remote) + privkey = None + if sign is not None: + privkey = self.get_key(sign, ktype, private=True) + return self._wrapper_map[ktype].encrypt( + data, pubkey, passphrase, privkey, cipher_algo=cipher_algo) - def decrypt(self, data, privkey, passphrase=None, verify=None): + def decrypt(self, data, address, ktype, passphrase=None, verify=None, + fetch_remote=True): """ - Decrypt C{data} using private @{privkey} and verify with C{verify} key. + Decrypt C{data} using private key from @{address} and verify with + C{verify} address. :param data: The data to be decrypted. :type data: str - :param privkey: The key used to decrypt. - :type privkey: OpenPGPKey + :param address: The address to who was encrypted. + :type address: str + :param ktype: The type of the key. + :type ktype: subclass of EncryptionKey :param passphrase: The passphrase for the secret key used for decryption. :type passphrase: str - :param verify: The key used to verify a signature. - :type verify: OpenPGPKey + :param verify: The address to be used for signature. + :type verify: str + :param fetch_remote: If key for verify not found in local storage try + to fetch from nickserver + :type fetch_remote: bool :return: The decrypted data. :rtype: str + :raise KeyNotFound: If any of the keys was not found both locally and + in keyserver. + :raise DecryptError: Raised if failed decrypting for some reason. :raise InvalidSignature: Raised if unable to verify the signature with - C{verify} key. + C{verify} address. """ - leap_assert_type(privkey, EncryptionKey) - leap_assert( - privkey.__class__ in self._wrapper_map, - 'Unknown key type.') - leap_assert(privkey.private is True, 'Key is not private.') - return self._wrapper_map[privkey.__class__].decrypt( - data, privkey, passphrase, verify) + privkey = self.get_key(address, ktype, private=True) + pubkey = None + if verify is not None: + pubkey = self.get_key(verify, ktype, private=False, + fetch_remote=fetch_remote) + return self._wrapper_map[ktype].decrypt( + data, privkey, passphrase, pubkey) - def sign(self, data, privkey, digest_algo='SHA512', clearsign=False, + def sign(self, data, address, ktype, digest_algo='SHA512', clearsign=False, detach=True, binary=False): """ - Sign C{data} with C{privkey}. + Sign C{data} with C{address}. :param data: The data to be signed. :type data: str - - :param privkey: The private key to be used to sign. - :type privkey: EncryptionKey + :param address: The address to be used to sign. + :type address: EncryptionKey + :param ktype: The type of the key. + :type ktype: subclass of EncryptionKey :param digest_algo: The hash digest to use. :type digest_algo: str :param clearsign: If True, create a cleartext signature. @@ -470,36 +491,46 @@ class KeyManager(object): :return: The signed data. :rtype: str + + :raise KeyNotFound: If the key was not found both locally and + in keyserver. + :raise SignFailed: If there was any error signing. """ - leap_assert_type(privkey, EncryptionKey) - leap_assert( - privkey.__class__ in self._wrapper_map, - 'Unknown key type.') - leap_assert(privkey.private is True, 'Key is not private.') - return self._wrapper_map[privkey.__class__].sign( + privkey = self.get_key(address, ktype, private=True) + return self._wrapper_map[ktype].sign( data, privkey, digest_algo=digest_algo, clearsign=clearsign, detach=detach, binary=binary) - def verify(self, data, pubkey, detached_sig=None): + def verify(self, data, address, ktype, detached_sig=None, + fetch_remote=True): """ - Verify signed C{data} with C{pubkey}, eventually using + Verify signed C{data} with C{address}, eventually using C{detached_sig}. :param data: The data to be verified. :type data: str - :param pubkey: The public key to be used on verification. - :type pubkey: EncryptionKey + :param address: The address to be used to verify. + :type address: EncryptionKey + :param ktype: The type of the key. + :type ktype: subclass of EncryptionKey :param detached_sig: A detached signature. If given, C{data} is verified using this detached signature. :type detached_sig: str + :param fetch_remote: If key for verify not found in local storage try + to fetch from nickserver + :type fetch_remote: bool :return: signature matches :rtype: bool + + :raise KeyNotFound: If the key was not found both locally and + in keyserver. + :raise InvalidSignature: Raised if unable to verify the signature with + C{verify} address. """ - leap_assert_type(pubkey, EncryptionKey) - leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.') - leap_assert(pubkey.private is False, 'Key is not public.') - return self._wrapper_map[pubkey.__class__].verify( + pubkey = self.get_key(address, ktype, private=False, + fetch_remote=fetch_remote) + return self._wrapper_map[ktype].verify( data, pubkey, detached_sig=detached_sig) def delete_key(self, key): diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 319d2e1..8ae12bf 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -218,10 +218,9 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) data = 'data' - pubkey = pgp.get_key(ADDRESS, private=False) self.assertRaises( AssertionError, - pgp.sign, data, pubkey) + pgp.sign, data, ADDRESS, OpenPGPKey) def test_verify_with_wrong_key_raises(self): pgp = openpgp.OpenPGPScheme( @@ -510,34 +509,24 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): km = self._key_manager() # put raw private key km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) - # get public key - pubkey = km.get_key( - ADDRESS, OpenPGPKey, private=False, fetch_remote=False) # encrypt - encdata = km.encrypt(self.RAW_DATA, pubkey) + encdata = km.encrypt(self.RAW_DATA, ADDRESS, OpenPGPKey, + fetch_remote=False) self.assertNotEqual(self.RAW_DATA, encdata) - # get private key - privkey = km.get_key( - ADDRESS, OpenPGPKey, private=True, fetch_remote=False) # decrypt - rawdata = km.decrypt(encdata, privkey) + rawdata = km.decrypt(encdata, ADDRESS, OpenPGPKey) self.assertEqual(self.RAW_DATA, rawdata) def test_keymanager_openpgp_sign_verify(self): km = self._key_manager() # put raw private keys km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) - # get private key for signing - privkey = km.get_key( - ADDRESS, OpenPGPKey, private=True, fetch_remote=False) # encrypt - signdata = km.sign(self.RAW_DATA, privkey, detach=False) + signdata = km.sign(self.RAW_DATA, ADDRESS, OpenPGPKey, detach=False) self.assertNotEqual(self.RAW_DATA, signdata) - # get public key for verifying - pubkey = km.get_key( - ADDRESS, OpenPGPKey, private=False, fetch_remote=False) # decrypt - self.assertTrue(km.verify(signdata, pubkey)) + self.assertTrue(km.verify(signdata, ADDRESS, OpenPGPKey, + fetch_remote=False)) # Key material for testing -- cgit v1.2.3 From 0ca8dbbac78f3c2cd91342a4ae78d94f76495046 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 20 Nov 2014 10:56:21 -0600 Subject: Return signing key on signature verification Don't throw an exception if verification fails --- keymanager/changes/feature-6366_expose_signing_key | 1 + keymanager/src/leap/keymanager/__init__.py | 18 +++---- keymanager/src/leap/keymanager/errors.py | 7 --- keymanager/src/leap/keymanager/keys.py | 7 ++- keymanager/src/leap/keymanager/openpgp.py | 27 ++++------ .../src/leap/keymanager/tests/test_keymanager.py | 62 +++++++++++++++------- 6 files changed, 66 insertions(+), 56 deletions(-) create mode 100644 keymanager/changes/feature-6366_expose_signing_key diff --git a/keymanager/changes/feature-6366_expose_signing_key b/keymanager/changes/feature-6366_expose_signing_key new file mode 100644 index 0000000..d594a15 --- /dev/null +++ b/keymanager/changes/feature-6366_expose_signing_key @@ -0,0 +1 @@ +- expose info about the signing key (closes #6366) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 1704e0b..b2b05f4 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -452,22 +452,21 @@ class KeyManager(object): to fetch from nickserver :type fetch_remote: bool - :return: The decrypted data. - :rtype: str + :return: The decrypted data and the signing key if signature verifies + :rtype: (unicode, EncryptionKey) :raise KeyNotFound: If any of the keys was not found both locally and in keyserver. :raise DecryptError: Raised if failed decrypting for some reason. - :raise InvalidSignature: Raised if unable to verify the signature with - C{verify} address. """ privkey = self.get_key(address, ktype, private=True) pubkey = None if verify is not None: pubkey = self.get_key(verify, ktype, private=False, fetch_remote=fetch_remote) - return self._wrapper_map[ktype].decrypt( + decrypted, signed = self._wrapper_map[ktype].decrypt( data, privkey, passphrase, pubkey) + return (decrypted, pubkey if signed else None) def sign(self, data, address, ktype, digest_algo='SHA512', clearsign=False, detach=True, binary=False): @@ -520,18 +519,17 @@ class KeyManager(object): to fetch from nickserver :type fetch_remote: bool - :return: signature matches - :rtype: bool + :return: The signing key if signature verifies else None + :rtype: EncryptionKey :raise KeyNotFound: If the key was not found both locally and in keyserver. - :raise InvalidSignature: Raised if unable to verify the signature with - C{verify} address. """ pubkey = self.get_key(address, ktype, private=False, fetch_remote=fetch_remote) - return self._wrapper_map[ktype].verify( + signed = self._wrapper_map[ktype].verify( data, pubkey, detached_sig=detached_sig) + return pubkey if signed else None def delete_key(self, key): """ diff --git a/keymanager/src/leap/keymanager/errors.py b/keymanager/src/leap/keymanager/errors.py index f896582..c068b27 100644 --- a/keymanager/src/leap/keymanager/errors.py +++ b/keymanager/src/leap/keymanager/errors.py @@ -51,13 +51,6 @@ class NoPasswordGiven(Exception): pass -class InvalidSignature(Exception): - """ - Raised when signature could not be verified. - """ - pass - - class EncryptError(Exception): """ Raised upon failures of encryption. diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index 7c732e3..0e243ba 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -373,11 +373,10 @@ class EncryptionScheme(object): :param verify: The key used to verify a signature. :type verify: OpenPGPKey - :return: The decrypted data. - :rtype: str + :return: The decrypted data and if signature verifies + :rtype: (unicode, bool) - @raise InvalidSignature: Raised if unable to verify the signature with - C{verify} key. + :raise DecryptError: Raised if failed decrypting for some reason. """ pass diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 3f298f7..1d1de98 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -640,12 +640,10 @@ class OpenPGPScheme(EncryptionScheme): :param verify: The key used to verify a signature. :type verify: OpenPGPKey - :return: The decrypted data. - :rtype: unicode + :return: The decrypted data and if signature verifies + :rtype: (unicode, bool) :raise DecryptError: Raised if failed decrypting for some reason. - :raise InvalidSignature: Raised if unable to verify the signature with - C{verify} key. """ leap_assert(privkey.private is True, 'Key is not private.') keys = [privkey] @@ -658,15 +656,15 @@ class OpenPGPScheme(EncryptionScheme): result = gpg.decrypt( data, passphrase=passphrase, always_trust=True) self._assert_gpg_result_ok(result) + # verify signature - if (verify is not None): - if result.valid is False or \ - verify.fingerprint != result.pubkey_fingerprint: - raise errors.InvalidSignature( - 'Failed to verify signature with key %s: %s' % - (verify.key_id, result.stderr)) + sign_valid = False + if (verify is not None and + result.valid is True and + verify.fingerprint == result.pubkey_fingerprint): + sign_valid = True - return result.data + return (result.data, sign_valid) except errors.GPGError as e: logger.error('Failed to decrypt: %s.' % str(e)) raise errors.DecryptError(str(e)) @@ -764,9 +762,4 @@ class OpenPGPScheme(EncryptionScheme): valid = result.valid rfprint = result.fingerprint kfprint = gpgpubkey['fingerprint'] - # raise in case sig is invalid - if valid is False or rfprint != kfprint: - raise errors.InvalidSignature( - 'Failed to verify signature ' - 'with key %s.' % gpgpubkey['keyid']) - return True + return valid and rfprint == kfprint diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 8ae12bf..ee4462a 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -183,11 +183,12 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) pubkey = pgp.get_key(ADDRESS, private=False) - cyphertext = pgp.encrypt('data', pubkey) + data = 'data' + cyphertext = pgp.encrypt(data, pubkey) # assert self.assertTrue(cyphertext is not None) self.assertTrue(cyphertext != '') - self.assertTrue(cyphertext != 'data') + self.assertTrue(cyphertext != data) self.assertTrue(pgp.is_encrypted(cyphertext)) self.assertTrue(pgp.is_encrypted(cyphertext)) # decrypt @@ -195,6 +196,8 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): KeyNotFound, pgp.get_key, ADDRESS, private=True) pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = pgp.get_key(ADDRESS, private=True) + decrypted, _ = pgp.decrypt(cyphertext, privkey) + self.assertEqual(decrypted, data) pgp.delete_key(pubkey) pgp.delete_key(privkey) self.assertRaises( @@ -231,9 +234,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): signed = pgp.sign(data, privkey) pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) wrongkey = pgp.get_key(ADDRESS_2) - self.assertRaises( - errors.InvalidSignature, - pgp.verify, signed, wrongkey) + self.assertFalse(pgp.verify(signed, wrongkey)) def test_encrypt_sign_with_public_raises(self): pgp = openpgp.OpenPGPScheme( @@ -260,7 +261,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): pgp.decrypt, encrypted_and_signed, privkey, verify=privkey) - def test_decrypt_verify_with_wrong_key_raises(self): + def test_decrypt_verify_with_wrong_key(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=GPG_BINARY_PATH) pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) @@ -270,9 +271,10 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): encrypted_and_signed = pgp.encrypt(data, pubkey, sign=privkey) pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) wrongkey = pgp.get_key(ADDRESS_2) - self.assertRaises( - errors.InvalidSignature, - pgp.verify, encrypted_and_signed, wrongkey) + decrypted, validsign = pgp.decrypt(encrypted_and_signed, privkey, + verify=wrongkey) + self.assertEqual(decrypted, data) + self.assertFalse(validsign) def test_sign_verify(self): pgp = openpgp.OpenPGPScheme( @@ -296,9 +298,10 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): data = 'data' encrypted_and_signed = pgp.encrypt( data, pubkey2, sign=privkey) - res = pgp.decrypt( + res, validsign = pgp.decrypt( encrypted_and_signed, privkey2, verify=pubkey) - self.assertTrue(data, res) + self.assertEqual(data, res) + self.assertTrue(validsign) def test_sign_verify_detached_sig(self): pgp = openpgp.OpenPGPScheme( @@ -308,7 +311,8 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): privkey = pgp.get_key(ADDRESS, private=True) signature = pgp.sign(data, privkey, detach=True) pubkey = pgp.get_key(ADDRESS, private=False) - self.assertTrue(pgp.verify(data, pubkey, detached_sig=signature)) + validsign = pgp.verify(data, pubkey, detached_sig=signature) + self.assertTrue(validsign) class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): @@ -509,24 +513,46 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): km = self._key_manager() # put raw private key km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY_2, ADDRESS_2) # encrypt encdata = km.encrypt(self.RAW_DATA, ADDRESS, OpenPGPKey, - fetch_remote=False) + sign=ADDRESS_2, fetch_remote=False) self.assertNotEqual(self.RAW_DATA, encdata) # decrypt - rawdata = km.decrypt(encdata, ADDRESS, OpenPGPKey) + rawdata, signingkey = km.decrypt(encdata, ADDRESS, OpenPGPKey, + verify=ADDRESS_2, fetch_remote=False) self.assertEqual(self.RAW_DATA, rawdata) + key = km.get_key(ADDRESS_2, OpenPGPKey, private=False, + fetch_remote=False) + self.assertEqual(signingkey.fingerprint, key.fingerprint) + + def test_keymanager_openpgp_encrypt_decrypt_wrong_sign(self): + km = self._key_manager() + # put raw keys + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY_2, ADDRESS_2) + # encrypt + encdata = km.encrypt(self.RAW_DATA, ADDRESS, OpenPGPKey, + sign=ADDRESS_2, fetch_remote=False) + self.assertNotEqual(self.RAW_DATA, encdata) + # verify + rawdata, signingkey = km.decrypt(encdata, ADDRESS, OpenPGPKey, + verify=ADDRESS, fetch_remote=False) + self.assertEqual(self.RAW_DATA, rawdata) + self.assertTrue(signingkey is None) def test_keymanager_openpgp_sign_verify(self): km = self._key_manager() # put raw private keys km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) - # encrypt signdata = km.sign(self.RAW_DATA, ADDRESS, OpenPGPKey, detach=False) self.assertNotEqual(self.RAW_DATA, signdata) - # decrypt - self.assertTrue(km.verify(signdata, ADDRESS, OpenPGPKey, - fetch_remote=False)) + # verify + signingkey = km.verify(signdata, ADDRESS, OpenPGPKey, + fetch_remote=False) + key = km.get_key(ADDRESS, OpenPGPKey, private=False, + fetch_remote=False) + self.assertEqual(signingkey.fingerprint, key.fingerprint) # Key material for testing -- cgit v1.2.3 From 7094981e43794558fe3542c3d381dfe43c4fa56b Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 28 Nov 2014 10:37:00 -0600 Subject: Port to soledad new async API --- keymanager/README.rst | 7 + keymanager/changes/feature-6368_async_api | 1 + keymanager/src/leap/keymanager/__init__.py | 438 ++++++++++++++------- keymanager/src/leap/keymanager/errors.py | 6 + keymanager/src/leap/keymanager/keys.py | 86 +++- keymanager/src/leap/keymanager/openpgp.py | 366 ++++++++++------- keymanager/src/leap/keymanager/tests/__init__.py | 109 +++-- .../src/leap/keymanager/tests/test_keymanager.py | 394 ++++-------------- .../src/leap/keymanager/tests/test_openpgp.py | 250 ++++++++++++ .../src/leap/keymanager/tests/test_validation.py | 49 ++- 10 files changed, 1032 insertions(+), 674 deletions(-) create mode 100644 keymanager/changes/feature-6368_async_api create mode 100644 keymanager/src/leap/keymanager/tests/test_openpgp.py diff --git a/keymanager/README.rst b/keymanager/README.rst index 8aedfb4..acf2335 100644 --- a/keymanager/README.rst +++ b/keymanager/README.rst @@ -6,3 +6,10 @@ LEAP's Key Manager The Key Manager is a Nicknym agent for the LEAP project: https://leap.se/pt/docs/design/nicknym + +running tests +------------- + +Use trial to run the test suite:: + + trial leap.keymanager diff --git a/keymanager/changes/feature-6368_async_api b/keymanager/changes/feature-6368_async_api new file mode 100644 index 0000000..b584daa --- /dev/null +++ b/keymanager/changes/feature-6368_async_api @@ -0,0 +1 @@ +- Port keymanager to the new soledad async API (Closes 6368) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index b2b05f4..7ff437e 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -43,6 +43,8 @@ except (ImportError, AssertionError): import logging import requests +from twisted.internet import defer + from leap.common.check import leap_assert from leap.common.events import signal from leap.common.events import events_pb2 as proto @@ -51,7 +53,8 @@ from leap.common.decorators import memoized_method from leap.keymanager.errors import ( KeyNotFound, KeyAddressMismatch, - KeyNotValidUpgrade + KeyNotValidUpgrade, + UnsupportedKeyTypeError ) from leap.keymanager.validation import ValidationLevel, can_upgrade @@ -196,15 +199,20 @@ class KeyManager(object): @memoized_method(invalidation=300) def _fetch_keys_from_server(self, address): """ - Fetch keys bound to C{address} from nickserver and insert them in + Fetch keys bound to address from nickserver and insert them in local database. :param address: The address bound to the keys. :type address: str - :raise KeyNotFound: If the key was not found on nickserver. + :return: A Deferred which fires when the key is in the storage, + or which fails with KeyNotFound if the key was not found on + nickserver. + :rtype: Deferred + """ # request keys from the nickserver + d = defer.succeed(None) res = None try: res = self._get(self._nickserver_uri, {'address': address}) @@ -212,18 +220,22 @@ class KeyManager(object): server_keys = res.json() # insert keys in local database if self.OPENPGP_KEY in server_keys: - self.put_raw_key( + d = self.put_raw_key( server_keys['openpgp'], OpenPGPKey, address=address, validation=ValidationLevel.Provider_Trust) except requests.exceptions.HTTPError as e: if e.response.status_code == 404: - raise KeyNotFound(address) + d = defer.fail(KeyNotFound(address)) + else: + d = defer.fail(e) logger.warning("HTTP error retrieving key: %r" % (e,)) logger.warning("%s" % (res.content,)) except Exception as e: + d = defer.fail(e) logger.warning("Error retrieving key: %r" % (e,)) + return d # # key management @@ -231,7 +243,7 @@ class KeyManager(object): def send_key(self, ktype): """ - Send user's key of type C{ktype} to provider. + Send user's key of type ktype to provider. Public key bound to user's is sent to provider, which will sign it and replace any prior keys for the same address in its database. @@ -239,27 +251,33 @@ class KeyManager(object): :param ktype: The type of the key. :type ktype: subclass of EncryptionKey - :raise KeyNotFound: If the key was not found in local database. + :return: A Deferred which fires when the key is sent, or which fails + with KeyNotFound if the key was not found in local database. + :rtype: Deferred + + :raise UnsupportedKeyTypeError: if invalid key type """ - leap_assert( - ktype is OpenPGPKey, - 'For now we only know how to send OpenPGP public keys.') - # prepare the public key bound to address - pubkey = self.get_key( + self._assert_supported_key_type(ktype) + + def send(pubkey): + data = { + self.PUBKEY_KEY: pubkey.key_data + } + uri = "%s/%s/users/%s.json" % ( + self._api_uri, + self._api_version, + self._uid) + self._put(uri, data) + signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address) + + d = self.get_key( self._address, ktype, private=False, fetch_remote=False) - data = { - self.PUBKEY_KEY: pubkey.key_data - } - uri = "%s/%s/users/%s.json" % ( - self._api_uri, - self._api_version, - self._uid) - self._put(uri, data) - signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address) + d.addCallback(send) + return d def get_key(self, address, ktype, private=False, fetch_remote=True): """ - Return a key of type C{ktype} bound to C{address}. + Return a key of type ktype bound to address. First, search for the key in local storage. If it is not available, then try to fetch from nickserver. @@ -274,36 +292,47 @@ class KeyManager(object): from nickserver :type fetch_remote: bool - :return: A key of type C{ktype} bound to C{address}. - :rtype: EncryptionKey - :raise KeyNotFound: If the key was not found both locally and in - keyserver. + :return: A Deferred which fires with an EncryptionKey of type ktype + bound to address, or which fails with KeyNotFound if no key + was found neither locally or in keyserver. + :rtype: Deferred + + :raise UnsupportedKeyTypeError: if invalid key type """ + self._assert_supported_key_type(ktype) logger.debug("getting key for %s" % (address,)) leap_assert( ktype in self._wrapper_map, 'Unkown key type: %s.' % str(ktype)) - try: - signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address) - # return key if it exists in local database - key = self._wrapper_map[ktype].get_key(address, private=private) - signal(proto.KEYMANAGER_KEY_FOUND, address) + signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address) + def key_found(key): + signal(proto.KEYMANAGER_KEY_FOUND, address) return key - except KeyNotFound: + + def key_not_found(failure): + if not failure.check(KeyNotFound): + return failure + signal(proto.KEYMANAGER_KEY_NOT_FOUND, address) # we will only try to fetch a key from nickserver if fetch_remote # is True and the key is not private. if fetch_remote is False or private is True: - raise + return failure signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address) - self._fetch_keys_from_server(address) # might raise KeyNotFound - key = self._wrapper_map[ktype].get_key(address, private=False) - signal(proto.KEYMANAGER_KEY_FOUND, address) - - return key + d = self._fetch_keys_from_server(address) + d.addCallback( + lambda _: + self._wrapper_map[ktype].get_key(address, private=False)) + d.addCallback(key_found) + return d + + # return key if it exists in local database + d = self._wrapper_map[ktype].get_key(address, private=private) + d.addCallbacks(key_found, key_not_found) + return d def get_all_keys(self, private=False): """ @@ -312,33 +341,50 @@ class KeyManager(object): :param private: Include private keys :type private: bool - :return: A list with all keys in local db. - :rtype: list - """ - return map( - lambda doc: build_key_from_dict( - self._key_class_from_type(doc.content['type']), - doc.content), - self._soledad.get_from_index( - TAGS_PRIVATE_INDEX, - KEYMANAGER_KEY_TAG, - '1' if private else '0')) + :return: A Deferred which fires with a list of all keys in local db. + :rtype: Deferred + """ + def build_keys(docs): + return map( + lambda doc: build_key_from_dict( + self._key_class_from_type(doc.content['type']), + doc.content), + docs) + + # XXX: there is no check that the soledad indexes are ready, as it + # happens with EncryptionScheme. + # The usecases right now are not problematic. This could be solve + # adding a keytype to this funciont and moving the soledad request + # to the EncryptionScheme. + d = self._soledad.get_from_index( + TAGS_PRIVATE_INDEX, + KEYMANAGER_KEY_TAG, + '1' if private else '0') + d.addCallback(build_keys) + return d def gen_key(self, ktype): """ - Generate a key of type C{ktype} bound to the user's address. + Generate a key of type ktype bound to the user's address. :param ktype: The type of the key. :type ktype: subclass of EncryptionKey - :return: The generated key. - :rtype: EncryptionKey + :return: A Deferred which fires with the generated EncryptionKey. + :rtype: Deferred + + :raise UnsupportedKeyTypeError: if invalid key type """ - signal(proto.KEYMANAGER_STARTED_KEY_GENERATION, self._address) - key = self._wrapper_map[ktype].gen_key(self._address) - signal(proto.KEYMANAGER_FINISHED_KEY_GENERATION, self._address) + self._assert_supported_key_type(ktype) - return key + def signal_finished(key): + signal(proto.KEYMANAGER_FINISHED_KEY_GENERATION, self._address) + return key + + signal(proto.KEYMANAGER_STARTED_KEY_GENERATION, self._address) + d = self._wrapper_map[ktype].gen_key(self._address) + d.addCallback(signal_finished) + return d # # Setters/getters @@ -397,7 +443,8 @@ class KeyManager(object): def encrypt(self, data, address, ktype, passphrase=None, sign=None, cipher_algo='AES256', fetch_remote=True): """ - Encrypt C{data} for C{address} and sign with C{sign} address. + Encrypt data with the public key bound to address and sign with with + the private key bound to sign address. :param data: The data to be encrypted. :type data: str @@ -412,34 +459,39 @@ class KeyManager(object): :type sign: str :param cipher_algo: The cipher algorithm to use. :type cipher_algo: str - :param fetch_remote: If key not found in local storage try to fetch + :param fetch_remote: If key is not found in local storage try to fetch from nickserver :type fetch_remote: bool - :return: The encrypted data. - :rtype: str + :return: A Deferred which fires with the encrypted data as str, or + which fails with KeyNotFound if no keys were found neither + locally or in keyserver or fails with EncryptError if failed + encrypting for some reason. + :rtype: Deferred - :raise KeyNotFound: If any of the keys was not found both locally and - in keyserver. - :raise EncryptError: Raised if failed encrypting for some reason. + :raise UnsupportedKeyTypeError: if invalid key type """ - pubkey = self.get_key(address, ktype, private=False, - fetch_remote=fetch_remote) - privkey = None - if sign is not None: - privkey = self.get_key(sign, ktype, private=True) - return self._wrapper_map[ktype].encrypt( - data, pubkey, passphrase, privkey, cipher_algo=cipher_algo) + self._assert_supported_key_type(ktype) + + def encrypt(keys): + pubkey, signkey = keys + return self._wrapper_map[ktype].encrypt( + data, pubkey, passphrase, sign=signkey, + cipher_algo=cipher_algo) + + d = self._get_keys(ktype, address, sign, fetch_remote=fetch_remote) + d.addCallback(encrypt) + return d def decrypt(self, data, address, ktype, passphrase=None, verify=None, fetch_remote=True): """ - Decrypt C{data} using private key from @{address} and verify with - C{verify} address. + Decrypt data using private key from address and verify with public key + bound to verify address. :param data: The data to be decrypted. :type data: str - :param address: The address to who was encrypted. + :param address: The address to whom data was encrypted. :type address: str :param ktype: The type of the key. :type ktype: subclass of EncryptionKey @@ -452,26 +504,59 @@ class KeyManager(object): to fetch from nickserver :type fetch_remote: bool - :return: The decrypted data and the signing key if signature verifies - :rtype: (unicode, EncryptionKey) + :return: A Deferred which fires with the decrypted data as str and the + signing EncryptionKey if signature verifies, or which fails + with KeyNotFound if no keys were found neither locally or in + keyserver or fails with DecryptError if failed decrypting for + some reason. + :rtype: Deferred - :raise KeyNotFound: If any of the keys was not found both locally and - in keyserver. - :raise DecryptError: Raised if failed decrypting for some reason. + :raise UnsupportedKeyTypeError: if invalid key type """ - privkey = self.get_key(address, ktype, private=True) - pubkey = None - if verify is not None: - pubkey = self.get_key(verify, ktype, private=False, - fetch_remote=fetch_remote) - decrypted, signed = self._wrapper_map[ktype].decrypt( - data, privkey, passphrase, pubkey) - return (decrypted, pubkey if signed else None) + self._assert_supported_key_type(ktype) + + def decrypt(keys): + pubkey, privkey = keys + decrypted, signed = self._wrapper_map[ktype].decrypt( + data, privkey, passphrase=passphrase, verify=pubkey) + return (decrypted, pubkey if signed else None) + + d = self._get_keys(ktype, verify, address, fetch_remote) + d.addCallback(decrypt) + return d + + def _get_keys(self, ktype, public, private, fetch_remote=True): + """ + Get public and private keys of ktype. + + :param ktype: The type of the key. + :type ktype: subclass of EncryptionKey + :param public: The address of the public key. + :type public: str + :param private: The address of the private key. + :type private: str + :param fetch_remote: If key for verify not found in local storage try + to fetch from nickserver + :type fetch_remote: bool + + :return: A Deferred which fires with a tuple with public and private + EncryptionKeys, or which fails with KeyNotFound if no keys + were found neither locally or in keyserver. + :rtype: Deferred + """ + dpub = defer.succeed(None) + if public is not None: + dpub = self.get_key(public, ktype, private=False, + fetch_remote=fetch_remote) + dpriv = defer.succeed(None) + if private is not None: + dpriv = self.get_key(private, ktype, private=True) + return defer.gatherResults([dpub, dpriv]) def sign(self, data, address, ktype, digest_algo='SHA512', clearsign=False, detach=True, binary=False): """ - Sign C{data} with C{address}. + Sign data with private key bound to address. :param data: The data to be signed. :type data: str @@ -488,23 +573,30 @@ class KeyManager(object): :param binary: If True, do not ascii armour the output. :type binary: bool - :return: The signed data. - :rtype: str + :return: A Deferred which fires with the signed data as str or fails + with KeyNotFound if no key was found neither locally or in + keyserver or fails with SignFailed if there was any error + signing. + :rtype: Deferred - :raise KeyNotFound: If the key was not found both locally and - in keyserver. - :raise SignFailed: If there was any error signing. + :raise UnsupportedKeyTypeError: if invalid key type """ - privkey = self.get_key(address, ktype, private=True) - return self._wrapper_map[ktype].sign( - data, privkey, digest_algo=digest_algo, clearsign=clearsign, - detach=detach, binary=binary) + self._assert_supported_key_type(ktype) + + def sign(privkey): + return self._wrapper_map[ktype].sign( + data, privkey, digest_algo=digest_algo, clearsign=clearsign, + detach=detach, binary=binary) + + d = self.get_key(address, ktype, private=True) + d.addCallback(sign) + return d def verify(self, data, address, ktype, detached_sig=None, fetch_remote=True): """ - Verify signed C{data} with C{address}, eventually using - C{detached_sig}. + Verify signed data with private key bound to address, eventually using + detached_sig. :param data: The data to be verified. :type data: str @@ -519,71 +611,90 @@ class KeyManager(object): to fetch from nickserver :type fetch_remote: bool - :return: The signing key if signature verifies else None - :rtype: EncryptionKey + :return: A Deferred which fires with the signing EncryptionKey if + signature verifies else None, or which fails with KeyNotFound + if no key was found neither locally or in keyserver. + :rtype: Deferred - :raise KeyNotFound: If the key was not found both locally and - in keyserver. + :raise UnsupportedKeyTypeError: if invalid key type """ - pubkey = self.get_key(address, ktype, private=False, - fetch_remote=fetch_remote) - signed = self._wrapper_map[ktype].verify( - data, pubkey, detached_sig=detached_sig) - return pubkey if signed else None + self._assert_supported_key_type(ktype) + + def verify(pubkey): + signed = self._wrapper_map[ktype].verify( + data, pubkey, detached_sig=detached_sig) + return pubkey if signed else None + + d = self.get_key(address, ktype, private=False, + fetch_remote=fetch_remote) + d.addCallback(verify) + return d def delete_key(self, key): """ - Remove C{key} from storage. - - May raise: - openpgp.errors.KeyNotFound - openpgp.errors.KeyAttributesDiffer + Remove key from storage. :param key: The key to be removed. :type key: EncryptionKey + + :return: A Deferred which fires when the key is deleted, or which fails + KeyNotFound if the key was not found on local storage. + :rtype: Deferred + + :raise UnsupportedKeyTypeError: if invalid key type """ - try: - self._wrapper_map[type(key)].delete_key(key) - except IndexError as e: - leap_assert(False, "Unsupported key type. Error {0!r}".format(e)) + self._assert_supported_key_type(type(key)) + return self._wrapper_map[type(key)].delete_key(key) def put_key(self, key, address): """ - Put C{key} in local storage. + Put key bound to address in local storage. :param key: The key to be stored :type key: EncryptionKey :param address: address for which this key will be active :type address: str - :raises KeyAddressMismatch: if address doesn't match any uid on the key - :raises KeyNotValidUpdate: if a key with the same uid exists and the - new one is not a valid update for it + :return: A Deferred which fires when the key is in the storage, or + which fails with KeyAddressMismatch if address doesn't match + any uid on the key or fails with KeyNotValidUpdate if a key + with the same uid exists and the new one is not a valid update + for it. + :rtype: Deferred + + :raise UnsupportedKeyTypeError: if invalid key type """ - if address not in key.address: - raise KeyAddressMismatch("UID %s found, but expected %s" - % (str(key.address), address)) + self._assert_supported_key_type(type(key)) - try: - old_key = self._wrapper_map[type(key)].get_key(address, - private=key.private) - except KeyNotFound: - old_key = None - - if key.private or can_upgrade(key, old_key): - try: - self._wrapper_map[type(key)].put_key(key, address) - except IndexError as e: - leap_assert( - False, "Unsupported key type. Error {0!r}".format(e)) - else: - raise KeyNotValidUpgrade("Key %s can not be upgraded by new key %s" - % (old_key.key_id, key.key_id)) + if address not in key.address: + return defer.fail( + KeyAddressMismatch("UID %s found, but expected %s" + % (str(key.address), address))) + + def old_key_not_found(failure): + if failure.check(KeyNotFound): + return None + else: + return failure + + def check_upgrade(old_key): + if key.private or can_upgrade(key, old_key): + return self._wrapper_map[type(key)].put_key(key, address) + else: + raise KeyNotValidUpgrade( + "Key %s can not be upgraded by new key %s" + % (old_key.key_id, key.key_id)) + + d = self._wrapper_map[type(key)].get_key(address, + private=key.private) + d.addErrback(old_key_not_found) + d.addCallback(check_upgrade) + return d def put_raw_key(self, key, ktype, address, validation=ValidationLevel.Weak_Chain): """ - Put C{key} in local storage. + Put raw key bound to address in local storage. :param key: The ascii key to be stored :type key: str @@ -595,19 +706,24 @@ class KeyManager(object): (default: 'Weak_Chain') :type validation: ValidationLevel - :raises KeyAddressMismatch: if address doesn't match any uid on the key - :raises KeyNotValidUpdate: if a key with the same uid exists and the - new one is not a valid update for it + :return: A Deferred which fires when the key is in the storage, or + which fails with KeyAddressMismatch if address doesn't match + any uid on the key or fails with KeyNotValidUpdate if a key + with the same uid exists and the new one is not a valid update + for it. + :rtype: Deferred + + :raise UnsupportedKeyTypeError: if invalid key type """ + self._assert_supported_key_type(ktype) pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(key) - pubkey.validation = validation - self.put_key(pubkey, address) + return self.put_key(pubkey, address) def fetch_key(self, address, uri, ktype, validation=ValidationLevel.Weak_Chain): """ - Fetch a public key for C{address} from the network and put it in + Fetch a public key bound to address from the network and put it in local storage. :param address: The email address of the key. @@ -620,22 +736,40 @@ class KeyManager(object): (default: 'Weak_Chain') :type validation: ValidationLevel - :raises KeyNotFound: if not valid key on C{uri} - :raises KeyAddressMismatch: if address doesn't match any uid on the key - :raises KeyNotValidUpdate: if a key with the same uid exists and the - new one is not a valid update for it + :return: A Deferred which fires when the key is in the storage, or + which fails with KeyNotFound: if not valid key on uri or fails + with KeyAddressMismatch if address doesn't match any uid on + the key or fails with KeyNotValidUpdate if a key with the same + uid exists and the new one is not a valid update for it. + :rtype: Deferred + + :raise UnsupportedKeyTypeError: if invalid key type """ + self._assert_supported_key_type(ktype) + res = self._get(uri) if not res.ok: - raise KeyNotFound(uri) + return defer.fail(KeyNotFound(uri)) # XXX parse binary keys pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(res.content) if pubkey is None: - raise KeyNotFound(uri) + return defer.fail(KeyNotFound(uri)) pubkey.validation = validation - self.put_key(pubkey, address) + return self.put_key(pubkey, address) + + def _assert_supported_key_type(self, ktype): + """ + Check if ktype is one of the supported key types + + :param ktype: the type of the key. + :type ktype: subclass of EncryptionKey + + :raise UnsupportedKeyTypeError: if invalid key type + """ + if ktype not in self._wrapper_map: + raise UnsupportedKeyTypeError(str(ktype)) from ._version import get_versions __version__ = get_versions()['version'] diff --git a/keymanager/src/leap/keymanager/errors.py b/keymanager/src/leap/keymanager/errors.py index c068b27..4041837 100644 --- a/keymanager/src/leap/keymanager/errors.py +++ b/keymanager/src/leap/keymanager/errors.py @@ -95,3 +95,9 @@ class KeyNotValidUpgrade(Exception): """ Already existing key can not be upgraded with the new key """ + + +class UnsupportedKeyTypeError(Exception): + """ + Invalid key type + """ diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index 0e243ba..4e98de6 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -33,6 +33,7 @@ import time from abc import ABCMeta, abstractmethod from datetime import datetime from leap.common.check import leap_assert +from twisted.internet import defer from leap.keymanager.validation import ValidationLevel, toValidationLevel @@ -277,21 +278,61 @@ class EncryptionScheme(object): """ leap_assert(self._soledad is not None, "Cannot init indexes with null soledad") - # Ask the database for currently existing indexes. - db_indexes = dict(self._soledad.list_indexes()) - # Loop through the indexes we expect to find. - for name, expression in INDEXES.items(): - if name not in db_indexes: - # The index does not yet exist. - self._soledad.create_index(name, *expression) - continue - if expression == db_indexes[name]: - # The index exists and is up to date. - continue - # The index exists but the definition is not what expected, so we - # delete it and add the proper index expression. - self._soledad.delete_index(name) - self._soledad.create_index(name, *expression) + + def init_idexes(indexes): + deferreds = [] + db_indexes = dict(indexes) + # Loop through the indexes we expect to find. + for name, expression in INDEXES.items(): + if name not in db_indexes: + # The index does not yet exist. + d = self._soledad.create_index(name, *expression) + deferreds.append(d) + elif expression != db_indexes[name]: + # The index exists but the definition is not what expected, + # so we delete it and add the proper index expression. + d = self._soledad.delete_index(name) + d.addCallback( + lambda _: + self._soledad.create_index(name, *expression)) + deferreds.append(d) + return defer.gatherResults(deferreds, consumeErrors=True) + + self.deferred_indexes = self._soledad.list_indexes() + self.deferred_indexes.addCallback(init_idexes) + + def _wait_indexes(self, *methods): + """ + Methods that need to wait for the indexes to be ready. + + Heavily based on + http://blogs.fluidinfo.com/terry/2009/05/11/a-mixin-class-allowing-python-__init__-methods-to-work-with-twisted-deferreds/ + + :param methods: methods that need to wait for the indexes to be ready + :type methods: tuple(str) + """ + self.waiting = [] + self.stored = {} + + def restore(_): + for method in self.stored: + setattr(self, method, self.stored[method]) + for d in self.waiting: + d.callback(None) + + def makeWrapper(method): + def wrapper(*args, **kw): + d = defer.Deferred() + d.addCallback(lambda _: self.stored[method](*args, **kw)) + self.waiting.append(d) + return d + return wrapper + + for method in methods: + self.stored[method] = getattr(self, method) + setattr(self, method, makeWrapper(method)) + + self.deferred_indexes.addCallback(restore) @abstractmethod def get_key(self, address, private=False): @@ -303,9 +344,10 @@ class EncryptionScheme(object): :param private: Look for a private key instead of a public one? :type private: bool - :return: The key bound to C{address}. - :rtype: EncryptionKey - @raise KeyNotFound: If the key was not found on local storage. + :return: A Deferred which fires with the EncryptionKey bound to + address, or which fails with KeyNotFound if the key was not + found on local storage. + :rtype: Deferred """ pass @@ -318,6 +360,9 @@ class EncryptionScheme(object): :type key: EncryptionKey :param address: address for which this key will be active. :type address: str + + :return: A Deferred which fires when the key is in the storage. + :rtype: Deferred """ pass @@ -341,6 +386,11 @@ class EncryptionScheme(object): :param key: The key to be removed. :type key: EncryptionKey + + :return: A Deferred which fires when the key is deleted, or which + fails with KeyNotFound if the key was not found on local + storage. + :rtype: Deferred """ pass diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 1d1de98..f81fb0a 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -28,6 +28,7 @@ import io from datetime import datetime from gnupg import GPG from gnupg.gnupg import GPGUtilities +from twisted.internet import defer from leap.common.check import leap_assert, leap_assert_type, leap_check from leap.keymanager import errors @@ -243,6 +244,7 @@ class OpenPGPScheme(EncryptionScheme): :type gpgbinary: C{str} """ EncryptionScheme.__init__(self, soledad) + self._wait_indexes("get_key", "put_key") self._gpgbinary = gpgbinary # @@ -255,59 +257,67 @@ class OpenPGPScheme(EncryptionScheme): :param address: The address bound to the key. :type address: str - :return: The key bound to C{address}. - :rtype: OpenPGPKey - @raise KeyAlreadyExists: If key already exists in local database. + + :return: A Deferred which fires with the key bound to address, or fails + with KeyAlreadyExists if key already exists in local database. + :rtype: Deferred """ # make sure the key does not already exist leap_assert(is_address(address), 'Not an user address: %s' % address) - try: - self.get_key(address) - raise errors.KeyAlreadyExists(address) - except errors.KeyNotFound: - logger.debug('Key for %s not found' % (address,)) - - with self._temporary_gpgwrapper() as gpg: - # TODO: inspect result, or use decorator - params = gpg.gen_key_input( - key_type='RSA', - key_length=4096, - name_real=address, - name_email=address, - name_comment='') - logger.info("About to generate keys... This might take SOME time.") - gpg.gen_key(params) - logger.info("Keys for %s have been successfully " - "generated." % (address,)) - pubkeys = gpg.list_keys() - - # assert for new key characteristics - - # XXX This exception is not properly catched by the soledad - # bootstrapping, so if we do not finish generating the keys - # we end with a blocked thread -- kali - leap_assert( - len(pubkeys) is 1, # a unitary keyring! - 'Keyring has wrong number of keys: %d.' % len(pubkeys)) - key = gpg.list_keys(secret=True).pop() - leap_assert( - len(key['uids']) is 1, # with just one uid! - 'Wrong number of uids for key: %d.' % len(key['uids'])) - uid_match = False - for uid in key['uids']: - if re.match('.*<%s>$' % address, uid) is not None: - uid_match = True - return - leap_assert(uid_match, 'Key not correctly bound to address.') - # insert both public and private keys in storage - for secret in [True, False]: - key = gpg.list_keys(secret=secret).pop() - openpgp_key = _build_key_from_gpg( - key, gpg.export_keys(key['fingerprint'], secret=secret)) - self.put_key(openpgp_key, address) + def _gen_key(_): + with self._temporary_gpgwrapper() as gpg: + # TODO: inspect result, or use decorator + params = gpg.gen_key_input( + key_type='RSA', + key_length=4096, + name_real=address, + name_email=address, + name_comment='') + logger.info("About to generate keys... " + "This might take SOME time.") + gpg.gen_key(params) + logger.info("Keys for %s have been successfully " + "generated." % (address,)) + pubkeys = gpg.list_keys() + + # assert for new key characteristics + + # XXX This exception is not properly catched by the soledad + # bootstrapping, so if we do not finish generating the keys + # we end with a blocked thread -- kali + + leap_assert( + len(pubkeys) is 1, # a unitary keyring! + 'Keyring has wrong number of keys: %d.' % len(pubkeys)) + key = gpg.list_keys(secret=True).pop() + leap_assert( + len(key['uids']) is 1, # with just one uid! + 'Wrong number of uids for key: %d.' % len(key['uids'])) + uid_match = False + for uid in key['uids']: + if re.match('.*<%s>$' % address, uid) is not None: + uid_match = True + return + leap_assert(uid_match, 'Key not correctly bound to address.') + # insert both public and private keys in storage + deferreds = [] + for secret in [True, False]: + key = gpg.list_keys(secret=secret).pop() + openpgp_key = _build_key_from_gpg( + key, + gpg.export_keys(key['fingerprint'], secret=secret)) + d = self.put_key(openpgp_key, address) + deferreds.append(d) + return defer.gatherResults(deferreds) + + def key_already_exists(_): + raise errors.KeyAlreadyExists(address) - return self.get_key(address, private=True) + d = self.get_key(address) + d.addCallbacks(key_already_exists, _gen_key) + d.addCallback(lambda _: self.get_key(address, private=True)) + return d def get_key(self, address, private=False): """ @@ -318,19 +328,24 @@ class OpenPGPScheme(EncryptionScheme): :param private: Look for a private key instead of a public one? :type private: bool - :return: The key bound to C{address}. - :rtype: OpenPGPKey - @raise KeyNotFound: If the key was not found on local storage. + :return: A Deferred which fires with the OpenPGPKey bound to address, + or which fails with KeyNotFound if the key was not found on + local storage. + :rtype: Deferred """ address = _parse_address(address) - doc = self._get_key_doc(address, private) - if doc is None: - raise errors.KeyNotFound(address) - leap_assert( - address in doc.content[KEY_ADDRESS_KEY], - 'Wrong address in key data.') - return build_key_from_dict(OpenPGPKey, doc.content) + def build_key(doc): + if doc is None: + raise errors.KeyNotFound(address) + leap_assert( + address in doc.content[KEY_ADDRESS_KEY], + 'Wrong address in key data.') + return build_key_from_dict(OpenPGPKey, doc.content) + + d = self._get_key_doc(address, private) + d.addCallback(build_key) + return d def parse_ascii_key(self, key_data): """ @@ -388,6 +403,9 @@ class OpenPGPScheme(EncryptionScheme): :type key_data: str or unicode :param address: address for which this key will be active :type address: str + + :return: A Deferred which fires when the OpenPGPKey is in the storage. + :rtype: Deferred """ leap_assert_type(key_data, (str, unicode)) @@ -395,12 +413,17 @@ class OpenPGPScheme(EncryptionScheme): try: openpgp_pubkey, openpgp_privkey = self.parse_ascii_key(key_data) except (errors.KeyAddressMismatch, errors.KeyFingerprintMismatch) as e: - leap_assert(False, repr(e)) + return defer.fail(e) + def put_key(_, key): + return self.put_key(key, address) + + d = defer.succeed(None) if openpgp_pubkey is not None: - self.put_key(openpgp_pubkey, address) + d.addCallback(put_key, openpgp_pubkey) if openpgp_privkey is not None: - self.put_key(openpgp_privkey, address) + d.addCallback(put_key, openpgp_privkey) + return d def put_key(self, key, address): """ @@ -410,42 +433,59 @@ class OpenPGPScheme(EncryptionScheme): :type key: OpenPGPKey :param address: address for which this key will be active. :type address: str + + :return: A Deferred which fires when the key is in the storage. + :rtype: Deferred """ - self._put_key_doc(key) - self._put_active_doc(key, address) + d = self._put_key_doc(key) + d.addCallback(lambda _: self._put_active_doc(key, address)) + return d def _put_key_doc(self, key): """ Put key document in soledad :type key: OpenPGPKey - """ - docs = self._soledad.get_from_index( + :rtype: Deferred + """ + def check_and_put(docs, key): + if len(docs) == 1: + doc = docs.pop() + if key.fingerprint == doc.content[KEY_FINGERPRINT_KEY]: + # in case of an update of the key merge them with gnupg + with self._temporary_gpgwrapper() as gpg: + gpg.import_keys(doc.content[KEY_DATA_KEY]) + gpg.import_keys(key.key_data) + gpgkey = gpg.list_keys(secret=key.private).pop() + key = _build_key_from_gpg( + gpgkey, + gpg.export_keys(gpgkey['fingerprint'], + secret=key.private)) + doc.set_json(key.get_json()) + d = self._soledad.put_doc(doc) + else: + logger.critical( + "Can't put a key whith the same key_id and different " + "fingerprint: %s, %s" + % (key.fingerprint, doc.content[KEY_FINGERPRINT_KEY])) + d = defer.fail( + errors.KeyFingerprintMismatch(key.fingerprint)) + elif len(docs) > 1: + logger.critical( + "There is more than one key with the same key_id %s" + % (key.key_id,)) + d = defer.fail(errors.KeyAttributesDiffer(key.key_id)) + else: + d = self._soledad.create_doc_from_json(key.get_json()) + return d + + d = self._soledad.get_from_index( TYPE_ID_PRIVATE_INDEX, self.KEY_TYPE, key.key_id, '1' if key.private else '0') - if len(docs) != 0: - doc = docs.pop() - if key.fingerprint == doc.content[KEY_FINGERPRINT_KEY]: - # in case of an update of the key merge them with gnupg - with self._temporary_gpgwrapper() as gpg: - gpg.import_keys(doc.content[KEY_DATA_KEY]) - gpg.import_keys(key.key_data) - gpgkey = gpg.list_keys(secret=key.private).pop() - key = _build_key_from_gpg( - gpgkey, - gpg.export_keys(gpgkey['fingerprint'], - secret=key.private)) - doc.set_json(key.get_json()) - self._soledad.put_doc(doc) - else: - logger.critical( - "Can't put a key whith the same key_id and different " - "fingerprint: %s, %s" - % (key.fingerprint, doc.content[KEY_FINGERPRINT_KEY])) - else: - self._soledad.create_doc_from_json(key.get_json()) + d.addCallback(check_and_put, key) + return d def _put_active_doc(self, key, address): """ @@ -453,24 +493,37 @@ class OpenPGPScheme(EncryptionScheme): :type key: OpenPGPKey :type addresses: str + :rtype: Deferred """ - docs = self._soledad.get_from_index( + def check_and_put(docs): + if len(docs) == 1: + doc = docs.pop() + doc.set_json(key.get_active_json(address)) + d = self._soledad.put_doc(doc) + else: + if len(docs) > 1: + logger.error("There is more than one active key document " + "for the address %s" % (address,)) + deferreds = [] + for doc in docs: + delete = self._soledad.delete_doc(doc) + deferreds.append(delete) + d = defer.gatherResults(deferreds, consumeErrors=True) + else: + d = defer.succeed(None) + + d.addCallback( + lambda _: self._soledad.create_doc_from_json( + key.get_active_json(address))) + return d + + d = self._soledad.get_from_index( TYPE_ADDRESS_PRIVATE_INDEX, self.ACTIVE_TYPE, address, '1' if key.private else '0') - if len(docs) == 1: - doc = docs.pop() - doc.set_json(key.get_active_json(address)) - self._soledad.put_doc(doc) - else: - if len(docs) > 1: - logger.error("There is more than one active key document " - "for the address %s" % (address,)) - for doc in docs: - self._soledad.delete_doc(doc) - self._soledad.create_doc_from_json( - key.get_active_json(address)) + d.addCallback(check_and_put) + return d def _get_key_doc(self, address, private=False): """ @@ -482,69 +535,94 @@ class OpenPGPScheme(EncryptionScheme): :type address: str :param private: Whether to look for a private key. :type private: bool - :return: The document with the key or None if it does not exist. - :rtype: leap.soledad.document.SoledadDocument + + :return: A Deferred which fires with the SoledadDocument with the key + or None if it does not exist. + :rtype: Deferred """ - activedoc = self._soledad.get_from_index( + def get_key_from_active_doc(activedoc): + if len(activedoc) is 0: + return None + leap_assert( + len(activedoc) is 1, + 'Found more than one key for address %s!' % (address,)) + + key_id = activedoc[0].content[KEY_ID_KEY] + d = self._soledad.get_from_index( + TYPE_ID_PRIVATE_INDEX, + self.KEY_TYPE, + key_id, + '1' if private else '0') + d.addCallback(get_doc, key_id) + return d + + def get_doc(doclist, key_id): + leap_assert( + len(doclist) is 1, + 'There is %d keys for id %s!' % (len(doclist), key_id)) + return doclist.pop() + + d = self._soledad.get_from_index( TYPE_ADDRESS_PRIVATE_INDEX, self.ACTIVE_TYPE, address, '1' if private else '0') - if len(activedoc) is 0: - return None - leap_assert( - len(activedoc) is 1, - 'Found more than one key for address %s!' % (address,)) - - key_id = activedoc[0].content[KEY_ID_KEY] - doclist = self._soledad.get_from_index( - TYPE_ID_PRIVATE_INDEX, - self.KEY_TYPE, - key_id, - '1' if private else '0') - leap_assert( - len(doclist) is 1, - 'There is %d keys for id %s!' % (len(doclist), key_id)) - return doclist.pop() + d.addCallback(get_key_from_active_doc) + return d def delete_key(self, key): """ Remove C{key} from storage. - May raise: - errors.KeyNotFound - :param key: The key to be removed. :type key: EncryptionKey + + :return: A Deferred which fires when the key is deleted, or which + fails with KeyNotFound if the key was not found on local + storage. + :rtype: Deferred """ leap_assert_type(key, OpenPGPKey) - activedocs = self._soledad.get_from_index( - TYPE_ID_PRIVATE_INDEX, - self.ACTIVE_TYPE, - key.key_id, - '1' if key.private else '0') - for doc in activedocs: - self._soledad.delete_doc(doc) - docs = self._soledad.get_from_index( + def delete_docs(activedocs): + deferreds = [] + for doc in activedocs: + d = self._soledad.delete_doc(doc) + deferreds.append(d) + return defer.gatherResults(deferreds) + + def get_key_docs(_): + return self._soledad.get_from_index( + TYPE_ID_PRIVATE_INDEX, + self.KEY_TYPE, + key.key_id, + '1' if key.private else '0') + + def delete_key(docs): + if len(docs) == 0: + raise errors.KeyNotFound(key) + if len(docs) > 1: + logger.critical("There is more than one key for key_id %s" + % key.key_id) + + doc = None + for d in docs: + if d.content['fingerprint'] == key.fingerprint: + doc = d + break + if doc is None: + raise errors.KeyNotFound(key) + return self._soledad.delete_doc(doc) + + d = self._soledad.get_from_index( TYPE_ID_PRIVATE_INDEX, - self.KEY_TYPE, + self.ACTIVE_TYPE, key.key_id, '1' if key.private else '0') - if len(docs) == 0: - raise errors.KeyNotFound(key) - if len(docs) > 1: - logger.critical("There is more than one key for key_id %s" - % key.key_id) - - doc = None - for d in docs: - if d.content['fingerprint'] == key.fingerprint: - doc = d - break - if doc is None: - raise errors.KeyNotFound(key) - self._soledad.delete_doc(doc) + d.addCallback(delete_docs) + d.addCallback(get_key_docs) + d.addCallback(delete_key) + return d # # Data encryption, decryption, signing and verifying diff --git a/keymanager/src/leap/keymanager/tests/__init__.py b/keymanager/src/leap/keymanager/tests/__init__.py index 1ea33b5..05b4487 100644 --- a/keymanager/src/leap/keymanager/tests/__init__.py +++ b/keymanager/src/leap/keymanager/tests/__init__.py @@ -18,37 +18,25 @@ Base classes for the Key Manager tests. """ -from mock import Mock +from twisted.internet.defer import gatherResults +from twisted.trial import unittest from leap.common.testing.basetest import BaseLeapTest from leap.soledad.client import Soledad from leap.keymanager import KeyManager +from leap.keymanager.openpgp import OpenPGPKey ADDRESS = 'leap@leap.se' +ADDRESS_2 = 'anotheruser@leap.se' # XXX discover the gpg binary path GPG_BINARY_PATH = '/usr/bin/gpg' -class KeyManagerWithSoledadTestCase(BaseLeapTest): +class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest): def setUp(self): - # mock key fetching and storing so Soledad doesn't fail when trying to - # reach the server. - Soledad._get_secrets_from_shared_db = Mock(return_value=None) - Soledad._put_secrets_in_shared_db = Mock(return_value=None) - - class MockSharedDB(object): - - get_doc = Mock(return_value=None) - put_doc = Mock() - lock = Mock(return_value=('atoken', 300)) - unlock = Mock(return_value=True) - - def __call__(self): - return self - - Soledad._shared_db = MockSharedDB() + self.setUpEnv() self._soledad = Soledad( u"leap@leap.se", @@ -58,14 +46,32 @@ class KeyManagerWithSoledadTestCase(BaseLeapTest): server_url='', cert_file=None, auth_token=None, + syncable=False ) def tearDown(self): km = self._key_manager() - for key in km.get_all_keys(): - km._wrapper_map[key.__class__].delete_key(key) - for key in km.get_all_keys(private=True): - km._wrapper_map[key.__class__].delete_key(key) + + def delete_keys(keys): + deferreds = [] + for key in keys: + d = km._wrapper_map[key.__class__].delete_key(key) + deferreds.append(d) + return gatherResults(deferreds) + + def get_and_delete_keys(_): + deferreds = [] + for private in [True, False]: + d = km.get_all_keys(private=private) + d.addCallback(delete_keys) + deferreds.append(d) + return gatherResults(deferreds) + + # wait for the indexes to be ready for the tear down + d = km._wrapper_map[OpenPGPKey].deferred_indexes + d.addCallback(get_and_delete_keys) + d.addCallback(lambda _: self.tearDownEnv()) + return d def _key_manager(self, user=ADDRESS, url='', token=None): return KeyManager(user, url, self._soledad, token=token, @@ -234,3 +240,62 @@ RZXoH+FTg9UAW87eqU610npOkT6cRaBxaMK/mDtGNdc= =JTFu -----END PGP PRIVATE KEY BLOCK----- """ + +# key 7FEE575A: public key "anotheruser " +PUBLIC_KEY_2 = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mI0EUYwJXgEEAMbTKHuPJ5/Gk34l9Z06f+0WCXTDXdte1UBoDtZ1erAbudgC4MOR +gquKqoj3Hhw0/ILqJ88GcOJmKK/bEoIAuKaqlzDF7UAYpOsPZZYmtRfPC2pTCnXq +Z1vdeqLwTbUspqXflkCkFtfhGKMq5rH8GV5a3tXZkRWZhdNwhVXZagC3ABEBAAG0 +IWFub3RoZXJ1c2VyIDxhbm90aGVydXNlckBsZWFwLnNlPoi4BBMBAgAiBQJRjAle +AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRB/nfpof+5XWotuA/4tLN4E +gUr7IfLy2HkHAxzw7A4rqfMN92DIM9mZrDGaWRrOn3aVF7VU1UG7MDkHfPvp/cFw +ezoCw4s4IoHVc/pVlOkcHSyt4/Rfh248tYEJmFCJXGHpkK83VIKYJAithNccJ6Q4 +JE/o06Mtf4uh/cA1HUL4a4ceqUhtpLJULLeKo7iNBFGMCV4BBADsyQI7GR0wSAxz +VayLjuPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQt +Z/hwcLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63 +yuRe94WenT1RJd6xU1aaUff4rKizuQARAQABiJ8EGAECAAkFAlGMCV4CGwwACgkQ +f536aH/uV1rPZQQAqCzRysOlu8ez7PuiBD4SebgRqWlxa1TF1ujzfLmuPivROZ2X +Kw5aQstxgGSjoB7tac49s0huh4X8XK+BtJBfU84JS8Jc2satlfwoyZ35LH6sDZck +I+RS/3we6zpMfHs3vvp9xgca6ZupQxivGtxlJs294TpJorx+mFFqbV17AzQ= +=Thdu +-----END PGP PUBLIC KEY BLOCK----- +""" + +PRIVATE_KEY_2 = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +lQHYBFGMCV4BBADG0yh7jyefxpN+JfWdOn/tFgl0w13bXtVAaA7WdXqwG7nYAuDD +kYKriqqI9x4cNPyC6ifPBnDiZiiv2xKCALimqpcwxe1AGKTrD2WWJrUXzwtqUwp1 +6mdb3Xqi8E21LKal35ZApBbX4RijKuax/BleWt7V2ZEVmYXTcIVV2WoAtwARAQAB +AAP7BLuSAx7tOohnimEs74ks8l/L6dOcsFQZj2bqs4AoY3jFe7bV0tHr4llypb/8 +H3/DYvpf6DWnCjyUS1tTnXSW8JXtx01BUKaAufSmMNg9blKV6GGHlT/Whe9uVyks +7XHk/+9mebVMNJ/kNlqq2k+uWqJohzC8WWLRK+d1tBeqDsECANZmzltPaqUsGV5X +C3zszE3tUBgptV/mKnBtopKi+VH+t7K6fudGcG+bAcZDUoH/QVde52mIIjjIdLje +uajJuHUCAO1mqh+vPoGv4eBLV7iBo3XrunyGXiys4a39eomhxTy3YktQanjjx+ty +GltAGCs5PbWGO6/IRjjvd46wh53kzvsCAO0J97gsWhzLuFnkxFAJSPk7RRlyl7lI +1XS/x0Og6j9XHCyY1OYkfBm0to3UlCfkgirzCYlTYObCofzdKFIPDmSqHbQhYW5v +dGhlcnVzZXIgPGFub3RoZXJ1c2VyQGxlYXAuc2U+iLgEEwECACIFAlGMCV4CGwMG +CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEH+d+mh/7ldai24D/i0s3gSBSvsh +8vLYeQcDHPDsDiup8w33YMgz2ZmsMZpZGs6fdpUXtVTVQbswOQd8++n9wXB7OgLD +izgigdVz+lWU6RwdLK3j9F+Hbjy1gQmYUIlcYemQrzdUgpgkCK2E1xwnpDgkT+jT +oy1/i6H9wDUdQvhrhx6pSG2kslQst4qjnQHYBFGMCV4BBADsyQI7GR0wSAxzVayL +juPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQtZ/hw +cLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63yuRe +94WenT1RJd6xU1aaUff4rKizuQARAQABAAP9EyElqJ3dq3EErXwwT4mMnbd1SrVC +rUJrNWQZL59mm5oigS00uIyR0SvusOr+UzTtd8ysRuwHy5d/LAZsbjQStaOMBILx +77TJveOel0a1QK0YSMF2ywZMCKvquvjli4hAtWYz/EwfuzQN3t23jc5ny+GqmqD2 +3FUxLJosFUfLNmECAO9KhVmJi+L9dswIs+2Dkjd1eiRQzNOEVffvYkGYZyKxNiXF +UA5kvyZcB4iAN9sWCybE4WHZ9jd4myGB0MPDGxkCAP1RsXJbbuD6zS7BXe5gwunO +2q4q7ptdSl/sJYQuTe1KNP5d/uGsvlcFfsYjpsopasPjFBIncc/2QThMKlhoEaEB +/0mVAxpT6SrEvUbJ18z7kna24SgMPr3OnPMxPGfvNLJY/Xv/A17YfoqjmByCvsKE +JCDjopXtmbcrZyoEZbEht9mko4ifBBgBAgAJBQJRjAleAhsMAAoJEH+d+mh/7lda +z2UEAKgs0crDpbvHs+z7ogQ+Enm4EalpcWtUxdbo83y5rj4r0TmdlysOWkLLcYBk +o6Ae7WnOPbNIboeF/FyvgbSQX1POCUvCXNrGrZX8KMmd+Sx+rA2XJCPkUv98Hus6 +THx7N776fcYHGumbqUMYrxrcZSbNveE6SaK8fphRam1dewM0 +=a5gs +-----END PGP PRIVATE KEY BLOCK----- +""" diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index ee4462a..b8ef88a 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -23,12 +23,12 @@ Tests for the Key Manager. from datetime import datetime from mock import Mock -from leap.common.testing.basetest import BaseLeapTest +from twisted.internet.defer import inlineCallbacks +from twisted.trial import unittest + from leap.keymanager import ( - openpgp, KeyNotFound, KeyAddressMismatch, - errors, ) from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.keys import ( @@ -42,23 +42,16 @@ from leap.keymanager.validation import ( from leap.keymanager.tests import ( KeyManagerWithSoledadTestCase, ADDRESS, + ADDRESS_2, KEY_FINGERPRINT, PUBLIC_KEY, + PUBLIC_KEY_2, PRIVATE_KEY, - GPG_BINARY_PATH + PRIVATE_KEY_2, ) -ADDRESS_2 = 'anotheruser@leap.se' - - -class KeyManagerUtilTestCase(BaseLeapTest): - - def setUp(self): - pass - - def tearDown(self): - pass +class KeyManagerUtilTestCase(unittest.TestCase): def test_is_address(self): self.assertTrue( @@ -128,227 +121,43 @@ class KeyManagerUtilTestCase(BaseLeapTest): 'Wrong data in key.') -class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): - - def _test_openpgp_gen_key(self): - pgp = openpgp.OpenPGPScheme(self._soledad) - self.assertRaises(KeyNotFound, pgp.get_key, 'user@leap.se') - key = pgp.gen_key('user@leap.se') - self.assertIsInstance(key, openpgp.OpenPGPKey) - self.assertEqual( - ['user@leap.se'], key.address, 'Wrong address bound to key.') - self.assertEqual( - 4096, key.length, 'Wrong key length.') - - def test_openpgp_put_delete_key(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) - key = pgp.get_key(ADDRESS, private=False) - pgp.delete_key(key) - self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - - def test_openpgp_put_ascii_key(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) - key = pgp.get_key(ADDRESS, private=False) - self.assertIsInstance(key, openpgp.OpenPGPKey) - self.assertTrue( - ADDRESS in key.address, 'Wrong address bound to key.') - self.assertEqual( - 4096, key.length, 'Wrong key length.') - pgp.delete_key(key) - self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - - def test_get_public_key(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) - self.assertRaises( - KeyNotFound, pgp.get_key, ADDRESS, private=True) - key = pgp.get_key(ADDRESS, private=False) - self.assertTrue(ADDRESS in key.address) - self.assertFalse(key.private) - self.assertEqual(KEY_FINGERPRINT, key.fingerprint) - pgp.delete_key(key) - self.assertRaises(KeyNotFound, pgp.get_key, ADDRESS) - - def test_openpgp_encrypt_decrypt(self): - # encrypt - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) - pubkey = pgp.get_key(ADDRESS, private=False) - data = 'data' - cyphertext = pgp.encrypt(data, pubkey) - # assert - self.assertTrue(cyphertext is not None) - self.assertTrue(cyphertext != '') - self.assertTrue(cyphertext != data) - self.assertTrue(pgp.is_encrypted(cyphertext)) - self.assertTrue(pgp.is_encrypted(cyphertext)) - # decrypt - self.assertRaises( - KeyNotFound, pgp.get_key, ADDRESS, private=True) - pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) - privkey = pgp.get_key(ADDRESS, private=True) - decrypted, _ = pgp.decrypt(cyphertext, privkey) - self.assertEqual(decrypted, data) - pgp.delete_key(pubkey) - pgp.delete_key(privkey) - self.assertRaises( - KeyNotFound, pgp.get_key, ADDRESS, private=False) - self.assertRaises( - KeyNotFound, pgp.get_key, ADDRESS, private=True) - - def test_verify_with_private_raises(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) - data = 'data' - privkey = pgp.get_key(ADDRESS, private=True) - signed = pgp.sign(data, privkey) - self.assertRaises( - AssertionError, - pgp.verify, signed, privkey) - - def test_sign_with_public_raises(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) - data = 'data' - self.assertRaises( - AssertionError, - pgp.sign, data, ADDRESS, OpenPGPKey) - - def test_verify_with_wrong_key_raises(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) - data = 'data' - privkey = pgp.get_key(ADDRESS, private=True) - signed = pgp.sign(data, privkey) - pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) - wrongkey = pgp.get_key(ADDRESS_2) - self.assertFalse(pgp.verify(signed, wrongkey)) - - def test_encrypt_sign_with_public_raises(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) - data = 'data' - privkey = pgp.get_key(ADDRESS, private=True) - pubkey = pgp.get_key(ADDRESS, private=False) - self.assertRaises( - AssertionError, - pgp.encrypt, data, privkey, sign=pubkey) - - def test_decrypt_verify_with_private_raises(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) - data = 'data' - privkey = pgp.get_key(ADDRESS, private=True) - pubkey = pgp.get_key(ADDRESS, private=False) - encrypted_and_signed = pgp.encrypt( - data, pubkey, sign=privkey) - self.assertRaises( - AssertionError, - pgp.decrypt, - encrypted_and_signed, privkey, verify=privkey) - - def test_decrypt_verify_with_wrong_key(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) - data = 'data' - privkey = pgp.get_key(ADDRESS, private=True) - pubkey = pgp.get_key(ADDRESS, private=False) - encrypted_and_signed = pgp.encrypt(data, pubkey, sign=privkey) - pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) - wrongkey = pgp.get_key(ADDRESS_2) - decrypted, validsign = pgp.decrypt(encrypted_and_signed, privkey, - verify=wrongkey) - self.assertEqual(decrypted, data) - self.assertFalse(validsign) - - def test_sign_verify(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) - data = 'data' - privkey = pgp.get_key(ADDRESS, private=True) - signed = pgp.sign(data, privkey, detach=False) - pubkey = pgp.get_key(ADDRESS, private=False) - self.assertTrue(pgp.verify(signed, pubkey)) - - def test_encrypt_sign_decrypt_verify(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) - pubkey = pgp.get_key(ADDRESS, private=False) - privkey = pgp.get_key(ADDRESS, private=True) - pgp.put_ascii_key(PRIVATE_KEY_2, ADDRESS_2) - pubkey2 = pgp.get_key(ADDRESS_2, private=False) - privkey2 = pgp.get_key(ADDRESS_2, private=True) - data = 'data' - encrypted_and_signed = pgp.encrypt( - data, pubkey2, sign=privkey) - res, validsign = pgp.decrypt( - encrypted_and_signed, privkey2, verify=pubkey) - self.assertEqual(data, res) - self.assertTrue(validsign) - - def test_sign_verify_detached_sig(self): - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) - pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) - data = 'data' - privkey = pgp.get_key(ADDRESS, private=True) - signature = pgp.sign(data, privkey, detach=True) - pubkey = pgp.get_key(ADDRESS, private=False) - validsign = pgp.verify(data, pubkey, detached_sig=signature) - self.assertTrue(validsign) - - class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): + @inlineCallbacks def test_get_all_keys_in_db(self): km = self._key_manager() - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) # get public keys - keys = km.get_all_keys(False) + keys = yield km.get_all_keys(False) self.assertEqual(len(keys), 1, 'Wrong number of keys') self.assertTrue(ADDRESS in keys[0].address) self.assertFalse(keys[0].private) # get private keys - keys = km.get_all_keys(True) + keys = yield km.get_all_keys(True) self.assertEqual(len(keys), 1, 'Wrong number of keys') self.assertTrue(ADDRESS in keys[0].address) self.assertTrue(keys[0].private) + @inlineCallbacks def test_get_public_key(self): km = self._key_manager() - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) # get the key - key = km.get_key(ADDRESS, OpenPGPKey, private=False, - fetch_remote=False) + key = yield km.get_key(ADDRESS, OpenPGPKey, private=False, + fetch_remote=False) self.assertTrue(key is not None) self.assertTrue(ADDRESS in key.address) self.assertEqual( key.fingerprint.lower(), KEY_FINGERPRINT.lower()) self.assertFalse(key.private) + @inlineCallbacks def test_get_private_key(self): km = self._key_manager() - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) # get the key - key = km.get_key(ADDRESS, OpenPGPKey, private=True, - fetch_remote=False) + key = yield km.get_key(ADDRESS, OpenPGPKey, private=True, + fetch_remote=False) self.assertTrue(key is not None) self.assertTrue(ADDRESS in key.address) self.assertEqual( @@ -357,17 +166,17 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): def test_send_key_raises_key_not_found(self): km = self._key_manager() - self.assertRaises( - KeyNotFound, - km.send_key, OpenPGPKey) + d = km.send_key(OpenPGPKey) + return self.assertFailure(d, KeyNotFound) + @inlineCallbacks def test_send_key(self): """ Test that request is well formed when sending keys to server. """ token = "mytoken" km = self._key_manager(token=token) - km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY, ADDRESS) km._fetcher.put = Mock() # the following data will be used on the send km.ca_cert_path = 'capath' @@ -375,10 +184,11 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km.uid = 'myuid' km.api_uri = 'apiuri' km.api_version = 'apiver' - km.send_key(OpenPGPKey) + yield km.send_key(OpenPGPKey) # setup expected args + pubkey = yield km.get_key(km._address, OpenPGPKey) data = { - km.PUBKEY_KEY: km.get_key(km._address, OpenPGPKey).key_data, + km.PUBKEY_KEY: pubkey.key_data, } url = '%s/%s/users/%s.json' % ('apiuri', 'apiver', 'myuid') km._fetcher.put.assert_called_once_with( @@ -386,7 +196,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): headers={'Authorization': 'Token token=%s' % token}, ) - def test__fetch_keys_from_server(self): + def test_fetch_keys_from_server(self): """ Test that the request is well formed when fetching keys from server. """ @@ -406,15 +216,19 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km._fetcher.get = Mock( return_value=Response()) km.ca_cert_path = 'cacertpath' - # do the fetch - km._fetch_keys_from_server(ADDRESS_2) - # and verify the call - km._fetcher.get.assert_called_once_with( - 'http://nickserver.domain', - data={'address': ADDRESS_2}, - verify='cacertpath', - ) + def verify_the_call(_): + km._fetcher.get.assert_called_once_with( + 'http://nickserver.domain', + data={'address': ADDRESS_2}, + verify='cacertpath', + ) + + d = km._fetch_keys_from_server(ADDRESS_2) + d.addCallback(verify_the_call) + return d + + @inlineCallbacks def test_get_key_fetches_from_server(self): """ Test that getting a key successfuly fetches from server. @@ -435,26 +249,26 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km._fetcher.get = Mock(return_value=Response()) km.ca_cert_path = 'cacertpath' # try to key get without fetching from server - self.assertRaises( - KeyNotFound, km.get_key, ADDRESS, OpenPGPKey, - fetch_remote=False - ) + d = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + yield self.assertFailure(d, KeyNotFound) # try to get key fetching from server. - key = km.get_key(ADDRESS, OpenPGPKey) + key = yield km.get_key(ADDRESS, OpenPGPKey) self.assertIsInstance(key, OpenPGPKey) self.assertTrue(ADDRESS in key.address) + @inlineCallbacks def test_put_key_ascii(self): """ Test that putting ascii key works """ km = self._key_manager(url='http://nickserver.domain') - km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) - key = km.get_key(ADDRESS, OpenPGPKey) + yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) + key = yield km.get_key(ADDRESS, OpenPGPKey) self.assertIsInstance(key, OpenPGPKey) self.assertTrue(ADDRESS in key.address) + @inlineCallbacks def test_fetch_uri_ascii_key(self): """ Test that fetch key downloads the ascii key and gets included in @@ -469,8 +283,8 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km._fetcher.get = Mock(return_value=Response()) km.ca_cert_path = 'cacertpath' - km.fetch_key(ADDRESS, "http://site.domain/key", OpenPGPKey) - key = km.get_key(ADDRESS, OpenPGPKey) + yield km.fetch_key(ADDRESS, "http://site.domain/key", OpenPGPKey) + key = yield km.get_key(ADDRESS, OpenPGPKey) self.assertEqual(KEY_FINGERPRINT, key.fingerprint) def test_fetch_uri_empty_key(self): @@ -485,8 +299,8 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km._fetcher.get = Mock(return_value=Response()) km.ca_cert_path = 'cacertpath' - self.assertRaises(KeyNotFound, km.fetch_key, - ADDRESS, "http://site.domain/key", OpenPGPKey) + d = km.fetch_key(ADDRESS, "http://site.domain/key", OpenPGPKey) + return self.assertFailure(d, KeyNotFound) def test_fetch_uri_address_differ(self): """ @@ -501,120 +315,66 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km._fetcher.get = Mock(return_value=Response()) km.ca_cert_path = 'cacertpath' - self.assertRaises(KeyAddressMismatch, km.fetch_key, - ADDRESS_2, "http://site.domain/key", OpenPGPKey) + d = km.fetch_key(ADDRESS_2, "http://site.domain/key", OpenPGPKey) + return self.assertFailure(d, KeyAddressMismatch) class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): RAW_DATA = 'data' + @inlineCallbacks def test_keymanager_openpgp_encrypt_decrypt(self): km = self._key_manager() # put raw private key - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY_2, ADDRESS_2) + yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_ascii_key( + PRIVATE_KEY_2, ADDRESS_2) # encrypt - encdata = km.encrypt(self.RAW_DATA, ADDRESS, OpenPGPKey, - sign=ADDRESS_2, fetch_remote=False) + encdata = yield km.encrypt(self.RAW_DATA, ADDRESS, OpenPGPKey, + sign=ADDRESS_2, fetch_remote=False) self.assertNotEqual(self.RAW_DATA, encdata) # decrypt - rawdata, signingkey = km.decrypt(encdata, ADDRESS, OpenPGPKey, - verify=ADDRESS_2, fetch_remote=False) + rawdata, signingkey = yield km.decrypt( + encdata, ADDRESS, OpenPGPKey, verify=ADDRESS_2, fetch_remote=False) self.assertEqual(self.RAW_DATA, rawdata) - key = km.get_key(ADDRESS_2, OpenPGPKey, private=False, - fetch_remote=False) + key = yield km.get_key(ADDRESS_2, OpenPGPKey, private=False, + fetch_remote=False) self.assertEqual(signingkey.fingerprint, key.fingerprint) + @inlineCallbacks def test_keymanager_openpgp_encrypt_decrypt_wrong_sign(self): km = self._key_manager() # put raw keys - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY_2, ADDRESS_2) + yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + yield km._wrapper_map[OpenPGPKey].put_ascii_key( + PRIVATE_KEY_2, ADDRESS_2) # encrypt - encdata = km.encrypt(self.RAW_DATA, ADDRESS, OpenPGPKey, - sign=ADDRESS_2, fetch_remote=False) + encdata = yield km.encrypt(self.RAW_DATA, ADDRESS, OpenPGPKey, + sign=ADDRESS_2, fetch_remote=False) self.assertNotEqual(self.RAW_DATA, encdata) # verify - rawdata, signingkey = km.decrypt(encdata, ADDRESS, OpenPGPKey, - verify=ADDRESS, fetch_remote=False) + rawdata, signingkey = yield km.decrypt( + encdata, ADDRESS, OpenPGPKey, verify=ADDRESS, fetch_remote=False) self.assertEqual(self.RAW_DATA, rawdata) self.assertTrue(signingkey is None) + @inlineCallbacks def test_keymanager_openpgp_sign_verify(self): km = self._key_manager() # put raw private keys - km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) - signdata = km.sign(self.RAW_DATA, ADDRESS, OpenPGPKey, detach=False) + yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + signdata = yield km.sign(self.RAW_DATA, ADDRESS, OpenPGPKey, + detach=False) self.assertNotEqual(self.RAW_DATA, signdata) # verify - signingkey = km.verify(signdata, ADDRESS, OpenPGPKey, + signingkey = yield km.verify(signdata, ADDRESS, OpenPGPKey, + fetch_remote=False) + key = yield km.get_key(ADDRESS, OpenPGPKey, private=False, fetch_remote=False) - key = km.get_key(ADDRESS, OpenPGPKey, private=False, - fetch_remote=False) self.assertEqual(signingkey.fingerprint, key.fingerprint) -# Key material for testing - -# key 7FEE575A: public key "anotheruser " -PUBLIC_KEY_2 = """ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -mI0EUYwJXgEEAMbTKHuPJ5/Gk34l9Z06f+0WCXTDXdte1UBoDtZ1erAbudgC4MOR -gquKqoj3Hhw0/ILqJ88GcOJmKK/bEoIAuKaqlzDF7UAYpOsPZZYmtRfPC2pTCnXq -Z1vdeqLwTbUspqXflkCkFtfhGKMq5rH8GV5a3tXZkRWZhdNwhVXZagC3ABEBAAG0 -IWFub3RoZXJ1c2VyIDxhbm90aGVydXNlckBsZWFwLnNlPoi4BBMBAgAiBQJRjAle -AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRB/nfpof+5XWotuA/4tLN4E -gUr7IfLy2HkHAxzw7A4rqfMN92DIM9mZrDGaWRrOn3aVF7VU1UG7MDkHfPvp/cFw -ezoCw4s4IoHVc/pVlOkcHSyt4/Rfh248tYEJmFCJXGHpkK83VIKYJAithNccJ6Q4 -JE/o06Mtf4uh/cA1HUL4a4ceqUhtpLJULLeKo7iNBFGMCV4BBADsyQI7GR0wSAxz -VayLjuPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQt -Z/hwcLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63 -yuRe94WenT1RJd6xU1aaUff4rKizuQARAQABiJ8EGAECAAkFAlGMCV4CGwwACgkQ -f536aH/uV1rPZQQAqCzRysOlu8ez7PuiBD4SebgRqWlxa1TF1ujzfLmuPivROZ2X -Kw5aQstxgGSjoB7tac49s0huh4X8XK+BtJBfU84JS8Jc2satlfwoyZ35LH6sDZck -I+RS/3we6zpMfHs3vvp9xgca6ZupQxivGtxlJs294TpJorx+mFFqbV17AzQ= -=Thdu ------END PGP PUBLIC KEY BLOCK----- -""" - -PRIVATE_KEY_2 = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -lQHYBFGMCV4BBADG0yh7jyefxpN+JfWdOn/tFgl0w13bXtVAaA7WdXqwG7nYAuDD -kYKriqqI9x4cNPyC6ifPBnDiZiiv2xKCALimqpcwxe1AGKTrD2WWJrUXzwtqUwp1 -6mdb3Xqi8E21LKal35ZApBbX4RijKuax/BleWt7V2ZEVmYXTcIVV2WoAtwARAQAB -AAP7BLuSAx7tOohnimEs74ks8l/L6dOcsFQZj2bqs4AoY3jFe7bV0tHr4llypb/8 -H3/DYvpf6DWnCjyUS1tTnXSW8JXtx01BUKaAufSmMNg9blKV6GGHlT/Whe9uVyks -7XHk/+9mebVMNJ/kNlqq2k+uWqJohzC8WWLRK+d1tBeqDsECANZmzltPaqUsGV5X -C3zszE3tUBgptV/mKnBtopKi+VH+t7K6fudGcG+bAcZDUoH/QVde52mIIjjIdLje -uajJuHUCAO1mqh+vPoGv4eBLV7iBo3XrunyGXiys4a39eomhxTy3YktQanjjx+ty -GltAGCs5PbWGO6/IRjjvd46wh53kzvsCAO0J97gsWhzLuFnkxFAJSPk7RRlyl7lI -1XS/x0Og6j9XHCyY1OYkfBm0to3UlCfkgirzCYlTYObCofzdKFIPDmSqHbQhYW5v -dGhlcnVzZXIgPGFub3RoZXJ1c2VyQGxlYXAuc2U+iLgEEwECACIFAlGMCV4CGwMG -CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEH+d+mh/7ldai24D/i0s3gSBSvsh -8vLYeQcDHPDsDiup8w33YMgz2ZmsMZpZGs6fdpUXtVTVQbswOQd8++n9wXB7OgLD -izgigdVz+lWU6RwdLK3j9F+Hbjy1gQmYUIlcYemQrzdUgpgkCK2E1xwnpDgkT+jT -oy1/i6H9wDUdQvhrhx6pSG2kslQst4qjnQHYBFGMCV4BBADsyQI7GR0wSAxzVayL -juPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQtZ/hw -cLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63yuRe -94WenT1RJd6xU1aaUff4rKizuQARAQABAAP9EyElqJ3dq3EErXwwT4mMnbd1SrVC -rUJrNWQZL59mm5oigS00uIyR0SvusOr+UzTtd8ysRuwHy5d/LAZsbjQStaOMBILx -77TJveOel0a1QK0YSMF2ywZMCKvquvjli4hAtWYz/EwfuzQN3t23jc5ny+GqmqD2 -3FUxLJosFUfLNmECAO9KhVmJi+L9dswIs+2Dkjd1eiRQzNOEVffvYkGYZyKxNiXF -UA5kvyZcB4iAN9sWCybE4WHZ9jd4myGB0MPDGxkCAP1RsXJbbuD6zS7BXe5gwunO -2q4q7ptdSl/sJYQuTe1KNP5d/uGsvlcFfsYjpsopasPjFBIncc/2QThMKlhoEaEB -/0mVAxpT6SrEvUbJ18z7kna24SgMPr3OnPMxPGfvNLJY/Xv/A17YfoqjmByCvsKE -JCDjopXtmbcrZyoEZbEht9mko4ifBBgBAgAJBQJRjAleAhsMAAoJEH+d+mh/7lda -z2UEAKgs0crDpbvHs+z7ogQ+Enm4EalpcWtUxdbo83y5rj4r0TmdlysOWkLLcYBk -o6Ae7WnOPbNIboeF/FyvgbSQX1POCUvCXNrGrZX8KMmd+Sx+rA2XJCPkUv98Hus6 -THx7N776fcYHGumbqUMYrxrcZSbNveE6SaK8fphRam1dewM0 -=a5gs ------END PGP PRIVATE KEY BLOCK----- -""" import unittest if __name__ == "__main__": unittest.main() diff --git a/keymanager/src/leap/keymanager/tests/test_openpgp.py b/keymanager/src/leap/keymanager/tests/test_openpgp.py new file mode 100644 index 0000000..01cf341 --- /dev/null +++ b/keymanager/src/leap/keymanager/tests/test_openpgp.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# test_keymanager.py +# Copyright (C) 2014 LEAP +# +# 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 . + + +""" +Tests for the OpenPGP support on Key Manager. +""" + + +from twisted.internet.defer import inlineCallbacks + +from leap.keymanager import ( + KeyNotFound, + openpgp, +) +from leap.keymanager.openpgp import OpenPGPKey +from leap.keymanager.tests import ( + KeyManagerWithSoledadTestCase, + ADDRESS, + ADDRESS_2, + KEY_FINGERPRINT, + PUBLIC_KEY, + PUBLIC_KEY_2, + PRIVATE_KEY, + PRIVATE_KEY_2, + GPG_BINARY_PATH +) + + +class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): + + @inlineCallbacks + def _test_openpgp_gen_key(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield self._assert_key_not_found(pgp, 'user@leap.se') + key = yield pgp.gen_key('user@leap.se') + self.assertIsInstance(key, openpgp.OpenPGPKey) + self.assertEqual( + ['user@leap.se'], key.address, 'Wrong address bound to key.') + self.assertEqual( + 4096, key.length, 'Wrong key length.') + + @inlineCallbacks + def test_openpgp_put_delete_key(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield self._assert_key_not_found(pgp, ADDRESS) + yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + key = yield pgp.get_key(ADDRESS, private=False) + yield pgp.delete_key(key) + yield self._assert_key_not_found(pgp, ADDRESS) + + @inlineCallbacks + def test_openpgp_put_ascii_key(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield self._assert_key_not_found(pgp, ADDRESS) + yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + key = yield pgp.get_key(ADDRESS, private=False) + self.assertIsInstance(key, openpgp.OpenPGPKey) + self.assertTrue( + ADDRESS in key.address, 'Wrong address bound to key.') + self.assertEqual( + 4096, key.length, 'Wrong key length.') + yield pgp.delete_key(key) + yield self._assert_key_not_found(pgp, ADDRESS) + + @inlineCallbacks + def test_get_public_key(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield self._assert_key_not_found(pgp, ADDRESS) + yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + yield self._assert_key_not_found(pgp, ADDRESS, private=True) + key = yield pgp.get_key(ADDRESS, private=False) + self.assertTrue(ADDRESS in key.address) + self.assertFalse(key.private) + self.assertEqual(KEY_FINGERPRINT, key.fingerprint) + yield pgp.delete_key(key) + yield self._assert_key_not_found(pgp, ADDRESS) + + @inlineCallbacks + def test_openpgp_encrypt_decrypt(self): + data = 'data' + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + + # encrypt + yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + pubkey = yield pgp.get_key(ADDRESS, private=False) + cyphertext = pgp.encrypt(data, pubkey) + + self.assertTrue(cyphertext is not None) + self.assertTrue(cyphertext != '') + self.assertTrue(cyphertext != data) + self.assertTrue(pgp.is_encrypted(cyphertext)) + self.assertTrue(pgp.is_encrypted(cyphertext)) + + # decrypt + yield self._assert_key_not_found(pgp, ADDRESS, private=True) + yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + privkey = yield pgp.get_key(ADDRESS, private=True) + decrypted, _ = pgp.decrypt(cyphertext, privkey) + self.assertEqual(decrypted, data) + + yield pgp.delete_key(pubkey) + yield pgp.delete_key(privkey) + yield self._assert_key_not_found(pgp, ADDRESS, private=False) + yield self._assert_key_not_found(pgp, ADDRESS, private=True) + + @inlineCallbacks + def test_verify_with_private_raises(self): + data = 'data' + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + privkey = yield pgp.get_key(ADDRESS, private=True) + signed = pgp.sign(data, privkey) + self.assertRaises( + AssertionError, + pgp.verify, signed, privkey) + + @inlineCallbacks + def test_sign_with_public_raises(self): + data = 'data' + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + self.assertRaises( + AssertionError, + pgp.sign, data, ADDRESS, OpenPGPKey) + + @inlineCallbacks + def test_verify_with_wrong_key_raises(self): + data = 'data' + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + privkey = yield pgp.get_key(ADDRESS, private=True) + signed = pgp.sign(data, privkey) + yield pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) + wrongkey = yield pgp.get_key(ADDRESS_2) + self.assertFalse(pgp.verify(signed, wrongkey)) + + @inlineCallbacks + def test_encrypt_sign_with_public_raises(self): + data = 'data' + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + privkey = yield pgp.get_key(ADDRESS, private=True) + pubkey = yield pgp.get_key(ADDRESS, private=False) + self.assertRaises( + AssertionError, + pgp.encrypt, data, privkey, sign=pubkey) + + @inlineCallbacks + def test_decrypt_verify_with_private_raises(self): + data = 'data' + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + privkey = yield pgp.get_key(ADDRESS, private=True) + pubkey = yield pgp.get_key(ADDRESS, private=False) + encrypted_and_signed = pgp.encrypt( + data, pubkey, sign=privkey) + self.assertRaises( + AssertionError, + pgp.decrypt, + encrypted_and_signed, privkey, verify=privkey) + + @inlineCallbacks + def test_decrypt_verify_with_wrong_key(self): + data = 'data' + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + privkey = yield pgp.get_key(ADDRESS, private=True) + pubkey = yield pgp.get_key(ADDRESS, private=False) + encrypted_and_signed = pgp.encrypt(data, pubkey, sign=privkey) + yield pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) + wrongkey = yield pgp.get_key(ADDRESS_2) + decrypted, validsign = pgp.decrypt(encrypted_and_signed, privkey, + verify=wrongkey) + self.assertEqual(decrypted, data) + self.assertFalse(validsign) + + @inlineCallbacks + def test_sign_verify(self): + data = 'data' + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + privkey = yield pgp.get_key(ADDRESS, private=True) + signed = pgp.sign(data, privkey, detach=False) + pubkey = yield pgp.get_key(ADDRESS, private=False) + validsign = pgp.verify(signed, pubkey) + self.assertTrue(validsign) + + @inlineCallbacks + def test_encrypt_sign_decrypt_verify(self): + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + + yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + pubkey = yield pgp.get_key(ADDRESS, private=False) + privkey = yield pgp.get_key(ADDRESS, private=True) + + yield pgp.put_ascii_key(PRIVATE_KEY_2, ADDRESS_2) + pubkey2 = yield pgp.get_key(ADDRESS_2, private=False) + privkey2 = yield pgp.get_key(ADDRESS_2, private=True) + + data = 'data' + encrypted_and_signed = pgp.encrypt( + data, pubkey2, sign=privkey) + res, validsign = pgp.decrypt( + encrypted_and_signed, privkey2, verify=pubkey) + self.assertEqual(data, res) + self.assertTrue(validsign) + + @inlineCallbacks + def test_sign_verify_detached_sig(self): + data = 'data' + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=GPG_BINARY_PATH) + yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) + privkey = yield pgp.get_key(ADDRESS, private=True) + signature = yield pgp.sign(data, privkey, detach=True) + pubkey = yield pgp.get_key(ADDRESS, private=False) + validsign = pgp.verify(data, pubkey, detached_sig=signature) + self.assertTrue(validsign) + + def _assert_key_not_found(self, pgp, address, private=False): + d = pgp.get_key(address, private=private) + return self.assertFailure(d, KeyNotFound) diff --git a/keymanager/src/leap/keymanager/tests/test_validation.py b/keymanager/src/leap/keymanager/tests/test_validation.py index 400d36e..83a02e0 100644 --- a/keymanager/src/leap/keymanager/tests/test_validation.py +++ b/keymanager/src/leap/keymanager/tests/test_validation.py @@ -19,6 +19,7 @@ Tests for the Validation Levels """ from datetime import datetime +from twisted.internet.defer import inlineCallbacks from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.errors import ( @@ -35,51 +36,57 @@ from leap.keymanager.validation import ValidationLevel class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): + @inlineCallbacks def test_none_old_key(self): km = self._key_manager() - km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) - key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) + key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.fingerprint, KEY_FINGERPRINT) + @inlineCallbacks def test_cant_upgrade(self): km = self._key_manager() - km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Provider_Trust) - self.assertRaises(KeyNotValidUpgrade, km.put_raw_key, UNRELATED_KEY, - OpenPGPKey, ADDRESS) + yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS, + validation=ValidationLevel.Provider_Trust) + d = km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS) + yield self.assertFailure(d, KeyNotValidUpgrade) + @inlineCallbacks def test_fingerprint_level(self): km = self._key_manager() - km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) - km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Fingerprint) - key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) + yield km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS, + validation=ValidationLevel.Fingerprint) + key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT) + @inlineCallbacks def test_expired_key(self): km = self._key_manager() - km.put_raw_key(EXPIRED_KEY, OpenPGPKey, ADDRESS) - km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS) - key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + yield km.put_raw_key(EXPIRED_KEY, OpenPGPKey, ADDRESS) + yield km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS) + key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT) + @inlineCallbacks def test_expired_fail_lower_level(self): km = self._key_manager() - km.put_raw_key(EXPIRED_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Third_Party_Endorsement) - self.assertRaises( - KeyNotValidUpgrade, - km.put_raw_key, + yield km.put_raw_key( + EXPIRED_KEY, OpenPGPKey, ADDRESS, + validation=ValidationLevel.Third_Party_Endorsement) + d = km.put_raw_key( UNRELATED_KEY, OpenPGPKey, ADDRESS, validation=ValidationLevel.Provider_Trust) + yield self.assertFailure(d, KeyNotValidUpgrade) + @inlineCallbacks def test_roll_back(self): km = self._key_manager() - km.put_raw_key(EXPIRED_KEY_UPDATED, OpenPGPKey, ADDRESS) - km.put_raw_key(EXPIRED_KEY, OpenPGPKey, ADDRESS) - key = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + yield km.put_raw_key(EXPIRED_KEY_UPDATED, OpenPGPKey, ADDRESS) + yield km.put_raw_key(EXPIRED_KEY, OpenPGPKey, ADDRESS) + key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.expiry_date, EXPIRED_KEY_NEW_EXPIRY_DATE) -- cgit v1.2.3 From 12684222d47a670b4e7790fa63348c5d2d966b6a Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 5 Dec 2014 12:28:37 -0600 Subject: Fix key generation --- keymanager/src/leap/keymanager/openpgp.py | 8 ++------ keymanager/src/leap/keymanager/tests/test_openpgp.py | 3 +++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index f81fb0a..c95b381 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -282,11 +282,6 @@ class OpenPGPScheme(EncryptionScheme): pubkeys = gpg.list_keys() # assert for new key characteristics - - # XXX This exception is not properly catched by the soledad - # bootstrapping, so if we do not finish generating the keys - # we end with a blocked thread -- kali - leap_assert( len(pubkeys) is 1, # a unitary keyring! 'Keyring has wrong number of keys: %d.' % len(pubkeys)) @@ -298,8 +293,9 @@ class OpenPGPScheme(EncryptionScheme): for uid in key['uids']: if re.match('.*<%s>$' % address, uid) is not None: uid_match = True - return + break leap_assert(uid_match, 'Key not correctly bound to address.') + # insert both public and private keys in storage deferreds = [] for secret in [True, False]: diff --git a/keymanager/src/leap/keymanager/tests/test_openpgp.py b/keymanager/src/leap/keymanager/tests/test_openpgp.py index 01cf341..e6f56e2 100644 --- a/keymanager/src/leap/keymanager/tests/test_openpgp.py +++ b/keymanager/src/leap/keymanager/tests/test_openpgp.py @@ -43,6 +43,9 @@ from leap.keymanager.tests import ( class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): + # set the trial timeout to 20min, needed by the key generation test + timeout = 1200 + @inlineCallbacks def _test_openpgp_gen_key(self): pgp = openpgp.OpenPGPScheme( -- cgit v1.2.3 From 1e3fc37172d474488f300e180e69c4adb7b396c6 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Sat, 13 Dec 2014 10:15:53 -0600 Subject: Find the gpg path instead of hard code it --- keymanager/src/leap/keymanager/tests/__init__.py | 15 ++++++++--- .../src/leap/keymanager/tests/test_openpgp.py | 29 +++++++++++----------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/keymanager/src/leap/keymanager/tests/__init__.py b/keymanager/src/leap/keymanager/tests/__init__.py index 05b4487..7128d20 100644 --- a/keymanager/src/leap/keymanager/tests/__init__.py +++ b/keymanager/src/leap/keymanager/tests/__init__.py @@ -18,6 +18,9 @@ Base classes for the Key Manager tests. """ +import distutils.spawn +import os.path + from twisted.internet.defer import gatherResults from twisted.trial import unittest @@ -29,14 +32,13 @@ from leap.keymanager.openpgp import OpenPGPKey ADDRESS = 'leap@leap.se' ADDRESS_2 = 'anotheruser@leap.se' -# XXX discover the gpg binary path -GPG_BINARY_PATH = '/usr/bin/gpg' class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest): def setUp(self): self.setUpEnv() + self.gpg_binary_path = self._find_gpg() self._soledad = Soledad( u"leap@leap.se", @@ -75,7 +77,14 @@ class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest): def _key_manager(self, user=ADDRESS, url='', token=None): return KeyManager(user, url, self._soledad, token=token, - gpgbinary=GPG_BINARY_PATH) + gpgbinary=self.gpg_binary_path) + + def _find_gpg(self): + gpg_path = distutils.spawn.find_executable('gpg') + if gpg_path is not None: + return os.path.realpath(gpg_path) + else: + return "/usr/bin/gpg" # key 24D18DDF: public key "Leap Test Key " diff --git a/keymanager/src/leap/keymanager/tests/test_openpgp.py b/keymanager/src/leap/keymanager/tests/test_openpgp.py index e6f56e2..5f85c74 100644 --- a/keymanager/src/leap/keymanager/tests/test_openpgp.py +++ b/keymanager/src/leap/keymanager/tests/test_openpgp.py @@ -37,7 +37,6 @@ from leap.keymanager.tests import ( PUBLIC_KEY_2, PRIVATE_KEY, PRIVATE_KEY_2, - GPG_BINARY_PATH ) @@ -49,7 +48,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): @inlineCallbacks def _test_openpgp_gen_key(self): pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield self._assert_key_not_found(pgp, 'user@leap.se') key = yield pgp.gen_key('user@leap.se') self.assertIsInstance(key, openpgp.OpenPGPKey) @@ -61,7 +60,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): @inlineCallbacks def test_openpgp_put_delete_key(self): pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield self._assert_key_not_found(pgp, ADDRESS) yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) key = yield pgp.get_key(ADDRESS, private=False) @@ -71,7 +70,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): @inlineCallbacks def test_openpgp_put_ascii_key(self): pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield self._assert_key_not_found(pgp, ADDRESS) yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) key = yield pgp.get_key(ADDRESS, private=False) @@ -86,7 +85,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): @inlineCallbacks def test_get_public_key(self): pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield self._assert_key_not_found(pgp, ADDRESS) yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) yield self._assert_key_not_found(pgp, ADDRESS, private=True) @@ -101,7 +100,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_openpgp_encrypt_decrypt(self): data = 'data' pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) # encrypt yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) @@ -130,7 +129,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_verify_with_private_raises(self): data = 'data' pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) signed = pgp.sign(data, privkey) @@ -142,7 +141,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_sign_with_public_raises(self): data = 'data' pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) self.assertRaises( AssertionError, @@ -152,7 +151,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_verify_with_wrong_key_raises(self): data = 'data' pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) signed = pgp.sign(data, privkey) @@ -164,7 +163,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_encrypt_sign_with_public_raises(self): data = 'data' pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) pubkey = yield pgp.get_key(ADDRESS, private=False) @@ -176,7 +175,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_decrypt_verify_with_private_raises(self): data = 'data' pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) pubkey = yield pgp.get_key(ADDRESS, private=False) @@ -191,7 +190,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_decrypt_verify_with_wrong_key(self): data = 'data' pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) pubkey = yield pgp.get_key(ADDRESS, private=False) @@ -207,7 +206,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_sign_verify(self): data = 'data' pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) signed = pgp.sign(data, privkey, detach=False) @@ -218,7 +217,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): @inlineCallbacks def test_encrypt_sign_decrypt_verify(self): pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) pubkey = yield pgp.get_key(ADDRESS, private=False) @@ -240,7 +239,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def test_sign_verify_detached_sig(self): data = 'data' pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=GPG_BINARY_PATH) + self._soledad, gpgbinary=self.gpg_binary_path) yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) signature = yield pgp.sign(data, privkey, detach=True) -- cgit v1.2.3 From 7acfc0802eeda852c09dac549b26dc455554cc04 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Sun, 14 Dec 2014 22:13:57 -0600 Subject: Return the right error on signature verification --- keymanager/src/leap/keymanager/__init__.py | 79 +++++++++++----------- keymanager/src/leap/keymanager/errors.py | 7 ++ .../src/leap/keymanager/tests/test_keymanager.py | 3 +- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 7ff437e..5324d42 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -54,7 +54,8 @@ from leap.keymanager.errors import ( KeyNotFound, KeyAddressMismatch, KeyNotValidUpgrade, - UnsupportedKeyTypeError + UnsupportedKeyTypeError, + InvalidSignature ) from leap.keymanager.validation import ValidationLevel, can_upgrade @@ -479,7 +480,12 @@ class KeyManager(object): data, pubkey, passphrase, sign=signkey, cipher_algo=cipher_algo) - d = self._get_keys(ktype, address, sign, fetch_remote=fetch_remote) + dpub = self.get_key(address, ktype, private=False, + fetch_remote=fetch_remote) + dpriv = defer.succeed(None) + if sign is not None: + dpriv = self.get_key(sign, ktype, private=True) + d = defer.gatherResults([dpub, dpriv]) d.addCallback(encrypt) return d @@ -504,11 +510,12 @@ class KeyManager(object): to fetch from nickserver :type fetch_remote: bool - :return: A Deferred which fires with the decrypted data as str and the - signing EncryptionKey if signature verifies, or which fails - with KeyNotFound if no keys were found neither locally or in - keyserver or fails with DecryptError if failed decrypting for - some reason. + :return: A Deferred which fires with: + * (decripted str, signing key) if validation works + * (decripted str, KeyNotFound) if signing key not found + * (decripted str, InvalidSignature) if signature is invalid + * KeyNotFound failure if private key not found + * DecryptError failure if decription failed :rtype: Deferred :raise UnsupportedKeyTypeError: if invalid key type @@ -519,39 +526,25 @@ class KeyManager(object): pubkey, privkey = keys decrypted, signed = self._wrapper_map[ktype].decrypt( data, privkey, passphrase=passphrase, verify=pubkey) - return (decrypted, pubkey if signed else None) - - d = self._get_keys(ktype, verify, address, fetch_remote) - d.addCallback(decrypt) - return d - - def _get_keys(self, ktype, public, private, fetch_remote=True): - """ - Get public and private keys of ktype. - - :param ktype: The type of the key. - :type ktype: subclass of EncryptionKey - :param public: The address of the public key. - :type public: str - :param private: The address of the private key. - :type private: str - :param fetch_remote: If key for verify not found in local storage try - to fetch from nickserver - :type fetch_remote: bool + if pubkey is None: + signature = KeyNotFound(verify) + elif signed: + signature = pubkey + else: + signature = InvalidSignature( + 'Failed to verify signature with key %s' % + (pubkey.key_id,)) + return (decrypted, signature) - :return: A Deferred which fires with a tuple with public and private - EncryptionKeys, or which fails with KeyNotFound if no keys - were found neither locally or in keyserver. - :rtype: Deferred - """ + dpriv = self.get_key(address, ktype, private=True) dpub = defer.succeed(None) - if public is not None: - dpub = self.get_key(public, ktype, private=False, + if verify is not None: + dpub = self.get_key(verify, ktype, private=False, fetch_remote=fetch_remote) - dpriv = defer.succeed(None) - if private is not None: - dpriv = self.get_key(private, ktype, private=True) - return defer.gatherResults([dpub, dpriv]) + dpub.addErrback(lambda f: None if f.check(KeyNotFound) else f) + d = defer.gatherResults([dpub, dpriv]) + d.addCallback(decrypt) + return d def sign(self, data, address, ktype, digest_algo='SHA512', clearsign=False, detach=True, binary=False): @@ -612,8 +605,9 @@ class KeyManager(object): :type fetch_remote: bool :return: A Deferred which fires with the signing EncryptionKey if - signature verifies else None, or which fails with KeyNotFound - if no key was found neither locally or in keyserver. + signature verifies, or which fails with InvalidSignature if + signature don't verifies or fails with KeyNotFound if no key + was found neither locally or in keyserver. :rtype: Deferred :raise UnsupportedKeyTypeError: if invalid key type @@ -623,7 +617,12 @@ class KeyManager(object): def verify(pubkey): signed = self._wrapper_map[ktype].verify( data, pubkey, detached_sig=detached_sig) - return pubkey if signed else None + if signed: + return pubkey + else: + raise InvalidSignature( + 'Failed to verify signature with key %s' % + (pubkey.key_id,)) d = self.get_key(address, ktype, private=False, fetch_remote=fetch_remote) diff --git a/keymanager/src/leap/keymanager/errors.py b/keymanager/src/leap/keymanager/errors.py index 4041837..8a9fb3c 100644 --- a/keymanager/src/leap/keymanager/errors.py +++ b/keymanager/src/leap/keymanager/errors.py @@ -51,6 +51,13 @@ class NoPasswordGiven(Exception): pass +class InvalidSignature(Exception): + """ + Raised when signature could not be verified. + """ + pass + + class EncryptError(Exception): """ Raised upon failures of encryption. diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index b8ef88a..dca89e8 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -29,6 +29,7 @@ from twisted.trial import unittest from leap.keymanager import ( KeyNotFound, KeyAddressMismatch, + errors ) from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.keys import ( @@ -357,7 +358,7 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): rawdata, signingkey = yield km.decrypt( encdata, ADDRESS, OpenPGPKey, verify=ADDRESS, fetch_remote=False) self.assertEqual(self.RAW_DATA, rawdata) - self.assertTrue(signingkey is None) + self.assertTrue(isinstance(signingkey, errors.InvalidSignature)) @inlineCallbacks def test_keymanager_openpgp_sign_verify(self): -- cgit v1.2.3 From 69d9e05711f138d06b63189eb0fb74b305b1578a Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 7 Jan 2015 16:45:37 -0200 Subject: Use trial as test runner. --- keymanager/pkg/requirements-testing.pip | 1 + keymanager/setup.cfg | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 keymanager/setup.cfg diff --git a/keymanager/pkg/requirements-testing.pip b/keymanager/pkg/requirements-testing.pip index f41fdba..14a4820 100644 --- a/keymanager/pkg/requirements-testing.pip +++ b/keymanager/pkg/requirements-testing.pip @@ -1,2 +1,3 @@ mock leap.soledad.client +setuptools-trial diff --git a/keymanager/setup.cfg b/keymanager/setup.cfg new file mode 100644 index 0000000..c71bffa --- /dev/null +++ b/keymanager/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test = trial -- cgit v1.2.3 From fb74dafe063683441108a198fc09d85b4d68c276 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 29 Dec 2014 00:27:27 -0600 Subject: Return a valid error from gatherResults --- keymanager/src/leap/keymanager/__init__.py | 11 +++++++---- keymanager/src/leap/keymanager/tests/test_keymanager.py | 8 ++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 5324d42..c4050fa 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -485,8 +485,8 @@ class KeyManager(object): dpriv = defer.succeed(None) if sign is not None: dpriv = self.get_key(sign, ktype, private=True) - d = defer.gatherResults([dpub, dpriv]) - d.addCallback(encrypt) + d = defer.gatherResults([dpub, dpriv], consumeErrors=True) + d.addCallbacks(encrypt, self._extract_first_error) return d def decrypt(self, data, address, ktype, passphrase=None, verify=None, @@ -542,10 +542,13 @@ class KeyManager(object): dpub = self.get_key(verify, ktype, private=False, fetch_remote=fetch_remote) dpub.addErrback(lambda f: None if f.check(KeyNotFound) else f) - d = defer.gatherResults([dpub, dpriv]) - d.addCallback(decrypt) + d = defer.gatherResults([dpub, dpriv], consumeErrors=True) + d.addCallbacks(decrypt, self._extract_first_error) return d + def _extract_first_error(self, failure): + return failure.value.subFailure + def sign(self, data, address, ktype, digest_algo='SHA512', clearsign=False, detach=True, binary=False): """ diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index dca89e8..86832ba 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -375,6 +375,14 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): fetch_remote=False) self.assertEqual(signingkey.fingerprint, key.fingerprint) + def test_keymanager_encrypt_key_not_found(self): + km = self._key_manager() + d = km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) + d.addCallback( + lambda _: km.encrypt(self.RAW_DATA, ADDRESS_2, OpenPGPKey, + sign=ADDRESS, fetch_remote=False)) + return self.assertFailure(d, KeyNotFound) + import unittest if __name__ == "__main__": -- cgit v1.2.3 From 679a1b90b11c9b7e06fde72e72314c757af7767e Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 19 Dec 2014 07:38:42 -0600 Subject: Fix key upgrade on no expiration date and higher validation level --- keymanager/src/leap/keymanager/validation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index 245013e..87de2af 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -82,7 +82,8 @@ def can_upgrade(new_key, old_key): return True # No expiration date and higher validation level - elif new_key.validation >= old_key.validation: + if (old_key.expiry_date is None and + new_key.validation > old_key.validation): return True return False -- cgit v1.2.3 From 61ad8463efa940347a5b50f8d25a056c68dbda0a Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 19 Dec 2014 07:40:05 -0600 Subject: On key update merge metadata correctly --- keymanager/src/leap/keymanager/openpgp.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index c95b381..0adfc52 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -40,8 +40,6 @@ from leap.keymanager.keys import ( TYPE_ID_PRIVATE_INDEX, TYPE_ADDRESS_PRIVATE_INDEX, KEY_ADDRESS_KEY, - KEY_FINGERPRINT_KEY, - KEY_DATA_KEY, KEY_ID_KEY, KEYMANAGER_ACTIVE_TYPE, ) @@ -447,23 +445,30 @@ class OpenPGPScheme(EncryptionScheme): def check_and_put(docs, key): if len(docs) == 1: doc = docs.pop() - if key.fingerprint == doc.content[KEY_FINGERPRINT_KEY]: + oldkey = build_key_from_dict(OpenPGPKey, doc.content) + if key.fingerprint == oldkey.fingerprint: # in case of an update of the key merge them with gnupg with self._temporary_gpgwrapper() as gpg: - gpg.import_keys(doc.content[KEY_DATA_KEY]) + gpg.import_keys(oldkey.key_data) gpg.import_keys(key.key_data) gpgkey = gpg.list_keys(secret=key.private).pop() - key = _build_key_from_gpg( + mergedkey = _build_key_from_gpg( gpgkey, gpg.export_keys(gpgkey['fingerprint'], secret=key.private)) - doc.set_json(key.get_json()) + mergedkey.validation = max( + [key.validation, oldkey.validation]) + mergedkey.last_audited_at = oldkey.last_audited_at + mergedkey.refreshed_at = key.refreshed_at + mergedkey.encr_used = key.encr_used or oldkey.encr_used + mergedkey.sign_used = key.sign_used or oldkey.sign_used + doc.set_json(mergedkey.get_json()) d = self._soledad.put_doc(doc) else: logger.critical( "Can't put a key whith the same key_id and different " "fingerprint: %s, %s" - % (key.fingerprint, doc.content[KEY_FINGERPRINT_KEY])) + % (key.fingerprint, oldkey.fingerprint)) d = defer.fail( errors.KeyFingerprintMismatch(key.fingerprint)) elif len(docs) > 1: -- cgit v1.2.3 From 0a301839f1c486e1ea3faa914dbbe33d75790b22 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 19 Dec 2014 08:15:43 -0600 Subject: Upgrade keys if not successfully used and strict high validation level --- keymanager/changes/feature-6211_succesfully_used | 1 + keymanager/src/leap/keymanager/__init__.py | 23 +++- .../src/leap/keymanager/tests/test_validation.py | 118 ++++++++++++++++++--- keymanager/src/leap/keymanager/validation.py | 6 +- 4 files changed, 127 insertions(+), 21 deletions(-) create mode 100644 keymanager/changes/feature-6211_succesfully_used diff --git a/keymanager/changes/feature-6211_succesfully_used b/keymanager/changes/feature-6211_succesfully_used new file mode 100644 index 0000000..74f0bba --- /dev/null +++ b/keymanager/changes/feature-6211_succesfully_used @@ -0,0 +1 @@ +- Upgrade keys if not successfully used and strict high validation level (closes: #6211) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index c4050fa..fdbc206 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -476,9 +476,13 @@ class KeyManager(object): def encrypt(keys): pubkey, signkey = keys - return self._wrapper_map[ktype].encrypt( + encrypted = self._wrapper_map[ktype].encrypt( data, pubkey, passphrase, sign=signkey, cipher_algo=cipher_algo) + pubkey.encr_used = True + d = self._wrapper_map[ktype].put_key(pubkey, address) + d.addCallback(lambda _: encrypted) + return d dpub = self.get_key(address, ktype, private=False, fetch_remote=fetch_remote) @@ -529,7 +533,10 @@ class KeyManager(object): if pubkey is None: signature = KeyNotFound(verify) elif signed: - signature = pubkey + pubkey.sign_used = True + d = self._wrapper_map[ktype].put_key(pubkey, address) + d.addCallback(lambda _: (decrypted, pubkey)) + return d else: signature = InvalidSignature( 'Failed to verify signature with key %s' % @@ -621,7 +628,10 @@ class KeyManager(object): signed = self._wrapper_map[ktype].verify( data, pubkey, detached_sig=detached_sig) if signed: - return pubkey + pubkey.sign_used = True + d = self._wrapper_map[ktype].put_key(pubkey, address) + d.addCallback(lambda _: pubkey) + return d else: raise InvalidSignature( 'Failed to verify signature with key %s' % @@ -718,9 +728,12 @@ class KeyManager(object): :raise UnsupportedKeyTypeError: if invalid key type """ self._assert_supported_key_type(ktype) - pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(key) + pubkey, privkey = self._wrapper_map[ktype].parse_ascii_key(key) pubkey.validation = validation - return self.put_key(pubkey, address) + d = self.put_key(pubkey, address) + if privkey is not None: + d.addCallback(lambda _: self.put_key(privkey, address)) + return d def fetch_key(self, address, uri, ktype, validation=ValidationLevel.Weak_Chain): diff --git a/keymanager/src/leap/keymanager/tests/test_validation.py b/keymanager/src/leap/keymanager/tests/test_validation.py index 83a02e0..a8b35ca 100644 --- a/keymanager/src/leap/keymanager/tests/test_validation.py +++ b/keymanager/src/leap/keymanager/tests/test_validation.py @@ -89,6 +89,34 @@ class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.expiry_date, EXPIRED_KEY_NEW_EXPIRY_DATE) + @inlineCallbacks + def test_not_used(self): + km = self._key_manager() + yield km.put_raw_key(UNEXPIRED_KEY, OpenPGPKey, ADDRESS, + validation=ValidationLevel.Provider_Trust) + yield km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS, + validation=ValidationLevel.Provider_Endorsement) + key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT) + + @inlineCallbacks + def test_used(self): + TEXT = "some text" + + km = self._key_manager() + yield km.put_raw_key(UNEXPIRED_KEY, OpenPGPKey, ADDRESS) + yield km.encrypt(TEXT, ADDRESS, OpenPGPKey) + + km2 = self._key_manager() + yield km2.put_raw_key(UNEXPIRED_PRIVATE, OpenPGPKey, ADDRESS) + signature = yield km2.sign(TEXT, ADDRESS, OpenPGPKey) + + yield km.verify(TEXT, ADDRESS, OpenPGPKey, detached_sig=signature) + d = km.put_raw_key( + UNRELATED_KEY, OpenPGPKey, ADDRESS, + validation=ValidationLevel.Provider_Endorsement) + yield self.assertFailure(d, KeyNotValidUpgrade) + # Key material for testing @@ -162,7 +190,7 @@ Osuse7+NkyUHgMXMVW7cz+nU7iO+ht2rkBtv+Z5LGlzgHTeFjKci -----END PGP PUBLIC KEY BLOCK----- """ # updated expiration date -EXPIRED_KEY_NEW_EXPIRY_DATE = datetime.fromtimestamp(2045319180) +EXPIRED_KEY_NEW_EXPIRY_DATE = datetime.fromtimestamp(2049717872) EXPIRED_KEY_UPDATED = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) @@ -174,27 +202,87 @@ acDh9vNtPxDCg5RdI0bfdIEBGgHTfsda3kWGvo1wH5SgrTRq0+EcTI7aJgkMmM/A IhnpACE52NvGdG9eB3x7xyQFsQqK8F0XvEev2UJH4SR7vb+Z7FNTJKCy6likYbSV wGGFuowFSESnzXuUI6PcjyuO6FUbMgeM5euFABEBAAG0HExlYXAgVGVzdCBLZXkg PGxlYXBAbGVhcC5zZT6JAT4EEwECACgCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B -AheABQJUURIXBQld/ZovAAoJEG8V8AShiFp8xUcIALcAHZbaxvyhHRGOrwDddbH0 -fFDK0AqKTsIT7y4D/HLFCP5zG3Ck7qGPZdkHXZfzq8rIb+zUjW3oJIVI1IucHxG2 -T5kppa8RFCBAFlRWYf6R3isX3YL0d3QSragjoxRNPcHNU8ALHcvfSonFHBoi4fH4 -4rvgksAiT68SsdPaoXDlabx5T15evu/7T5e/DGMQVPMxiaSuSQhbOKuMk2wcFdmL -tBYHLZPa54hHPNhEDyxLgtKKph0gObk9ojKfH9kPvLveIcpS5CqTJfN/kqBz7CJW -wEeAi2iG3H1OEB25aCUdTxXSRNlGqEgcWPaWxtc1RzlARu7LB64OUZuRy4puiAG5 +AheABQJUlDCSBQleQLiTAAoJEG8V8AShiFp8t3QH/1eqkVIScXmqaCVeno3VSKiH +HqnxiHcEgtpNRfUlP6tLD4H6QPEpvoUI9S/8HSYi3nbDGXEX8ycKlnwxjdIqWSOW +xj91/7uQAo+dP9QaVJ6xgaAiqzN1x3JzX3Js1wTodmNV0TfmGjxwnC5up/xK7/pd +KuDP3woDsRlwy8Lgj67mkn49xfAFHo6hI6SD36UBDAC/ELq6kZaba4Kk0fEVHCEz +HX0B09ZIY9fmf305cEB3dNh6SMQgKtH0wKozaqI2UM2B+cs3z08bC+YuUUh7UJTH +yr+hI7vF4/WEeJB3fuhP3xsumLhV8P47DaJ7oivmtsDEbAJFKqvigEqNES73Xpy5 AQ0EG+t93QEIAKqRq/2sBDW4g3FU+11LhixT+GosrfVvnitz3S9k2tBXok/wYpI1 XeA+kTHiF0LaqoaciDRvkA9DvhDbSrNM1yeuYRyZiHlTmoPZ/Fkl60oA2cyLd1L5 sXbuipY3TEiakugdSU4rzgi0hFycm6Go6yq2G6eC6UALvD9CTMdZHw40TadG9xpm 4thYPuJ1kPH8/bkbTi9sLHoApYgL+7ssje8w4epr0qD4IGxeKwJPf/tbTRpnd8w3 leldixHHKAutNt49p0pkXlORAHRpUmp+KMZhFvCvIPwe9o5mYtMR7sDRxjY61ZEQ -KLyKoh5wsJsaPXBjdG7cf6G/cBcwvnQVUHcAEQEAAYkBJQQYAQIADwUCG+t93QIb -DAUJAAFRgAAKCRBvFfAEoYhafOPgB/9z4YCyT/N0262HtegHykhsyykuqEeNb1LV -D9INcP+RbCX/0IjFgP4DTMPP7qqF1OBwR276maALT321Gqxc5HN5YrwxGdmoyBLm -unaQJJlD+7B1C+jnO6r4m44obvJ/NMERxVyzkXap3J2VgRIO1wNLI9I0sH6Kj5/j -Mgy06OwXDcqIc+jB4sIJ3Tnm8LZ3phJzNEm9mI8Ak0oJ7IEcMndR6DzmRt1rJQcq -K/D7hOG02zvyRhxF27U1qR1MxeU/gNnOx8q4dnVyWB+EiV1sFl4iTOyYHEsoyd7W -Osuse7+NkyUHgMXMVW7cz+nU7iO+ht2rkBtv+Z5LGlzgHTeFjKci -=79Ll +KLyKoh5wsJsaPXBjdG7cf6G/cBcwvnQVUHcAEQEAAYkBJQQYAQIADwIbDAUCVJQw +3QUJXkC4/QAKCRBvFfAEoYhafEtiB/9hMfSFNMxtlIJDJArG4JwR7sBOatYUT858 +qZnTgGETZN8wXpeEpXWKdDdmCX9aeE9jsDNgSQ5WWpqU21bGMXh1IGjAzmqTqq3/ +ik1vALuaVfr6OqjTzrJVQujT61CGed26xpP3Zh8hLKyKa+dXnX/VpgZS42wZLPx2 +wcODfANmTfE2AhMap/RyDy21q4nau+z2hMEOKdtF8dpP+pEvzoN5ZexYP1hfT+Av +oFPyVB5YtEMfxTEshDKRPjbdgNmw4faKXd5Cbelo4YxxpO16FHb6gzIdjOX15vQ+ +KwcVXzg9xk4D3cr1mnTCops/iv6TXvcw4Wbo70rrKXwkjl8LKjOP +=sHoe -----END PGP PUBLIC KEY BLOCK----- """ +UNEXPIRED_KEY = EXPIRED_KEY_UPDATED +UNEXPIRED_PRIVATE = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +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 +=dZE8 +-----END PGP PRIVATE KEY BLOCK----- +""" import unittest diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index 87de2af..b3aff3e 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -60,7 +60,6 @@ def can_upgrade(new_key, old_key): :type old_key: EncryptionKey :rtype: bool """ - # XXX not succesfully used and strict high validation level (#6211) # XXX implement key signature checking (#6120) # First contact @@ -86,4 +85,9 @@ def can_upgrade(new_key, old_key): new_key.validation > old_key.validation): return True + # Not successfully used and strict high validation level + if (not (old_key.sign_used and old_key.encr_used) and + new_key.validation > old_key.validation): + return True + return False -- cgit v1.2.3 From cf8b6825d15ec970a6e54aac408d007605f3bb57 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 19 Dec 2014 22:37:40 -0600 Subject: upgrade key when signed by old key --- keymanager/changes/feature-6240_signed_key | 1 + keymanager/pkg/requirements.pip | 2 +- keymanager/src/leap/keymanager/__init__.py | 2 +- keymanager/src/leap/keymanager/openpgp.py | 99 +++++++++++++--------- .../src/leap/keymanager/tests/test_validation.py | 55 ++++++++++++ keymanager/src/leap/keymanager/validation.py | 6 +- 6 files changed, 123 insertions(+), 42 deletions(-) create mode 100644 keymanager/changes/feature-6240_signed_key diff --git a/keymanager/changes/feature-6240_signed_key b/keymanager/changes/feature-6240_signed_key new file mode 100644 index 0000000..7236950 --- /dev/null +++ b/keymanager/changes/feature-6240_signed_key @@ -0,0 +1 @@ +- upgrade key when signed by old key (Closes #6240) diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index f3cdae9..207483c 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -3,5 +3,5 @@ simplejson requests # if we bump the gnupg version, bump also the sanity check # in keymanager.__init__ -gnupg>=1.2.3 +gnupg>=1.4.0 enum diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index fdbc206..a1a59f5 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -25,7 +25,7 @@ try: assert(GPGUtilities) # pyflakes happy from gnupg import __version__ as _gnupg_version from pkg_resources import parse_version - assert(parse_version(_gnupg_version) >= parse_version('1.2.3')) + assert(parse_version(_gnupg_version) >= parse_version('1.4.0')) except (ImportError, AssertionError): print "*******" diff --git a/keymanager/src/leap/keymanager/openpgp.py b/keymanager/src/leap/keymanager/openpgp.py index 0adfc52..794a0ec 100644 --- a/keymanager/src/leap/keymanager/openpgp.py +++ b/keymanager/src/leap/keymanager/openpgp.py @@ -163,39 +163,6 @@ class TempGPGWrapper(object): shutil.rmtree(self._gpg.homedir) -def _build_key_from_gpg(key, key_data): - """ - Build an OpenPGPKey based on C{key} from local gpg storage. - - ASCII armored GPG key data has to be queried independently in this - wrapper, so we receive it in C{key_data}. - - :param key: Key obtained from GPG storage. - :type key: dict - :param key_data: Key data obtained from GPG storage. - :type key_data: str - :return: An instance of the key. - :rtype: OpenPGPKey - """ - expiry_date = None - if key['expires']: - expiry_date = datetime.fromtimestamp(int(key['expires'])) - address = [] - for uid in key['uids']: - address.append(_parse_address(uid)) - - return OpenPGPKey( - address, - key_id=key['keyid'], - fingerprint=key['fingerprint'], - key_data=key_data, - private=True if key['type'] == 'sec' else False, - length=int(key['length']), - expiry_date=expiry_date, - refreshed_at=datetime.now(), - ) - - def _parse_address(address): """ Remove name, '<', '>' and the identity suffix after the '+' until the '@' @@ -221,6 +188,26 @@ class OpenPGPKey(EncryptionKey): Base class for OpenPGP keys. """ + def __init__(self, address, gpgbinary=None, **kwargs): + self._gpgbinary = gpgbinary + super(OpenPGPKey, self).__init__(address, **kwargs) + + @property + def signatures(self): + """ + Get the key signatures + + :return: the key IDs that have signed the key + :rtype: list(str) + """ + with TempGPGWrapper(keys=[self], gpgbinary=self._gpgbinary) as gpg: + res = gpg.list_sigs(self.key_id) + for uid, sigs in res.sigs.iteritems(): + if _parse_address(uid) in self.address: + return sigs + + return [] + class OpenPGPScheme(EncryptionScheme): """ @@ -298,7 +285,7 @@ class OpenPGPScheme(EncryptionScheme): deferreds = [] for secret in [True, False]: key = gpg.list_keys(secret=secret).pop() - openpgp_key = _build_key_from_gpg( + openpgp_key = self._build_key_from_gpg( key, gpg.export_keys(key['fingerprint'], secret=secret)) d = self.put_key(openpgp_key, address) @@ -335,7 +322,9 @@ class OpenPGPScheme(EncryptionScheme): leap_assert( address in doc.content[KEY_ADDRESS_KEY], 'Wrong address in key data.') - return build_key_from_dict(OpenPGPKey, doc.content) + key = build_key_from_dict(OpenPGPKey, doc.content) + key._gpgbinary = self._gpgbinary + return key d = self._get_key_doc(address, private) d.addCallback(build_key) @@ -375,7 +364,7 @@ class OpenPGPScheme(EncryptionScheme): openpgp_privkey = None if privkey is not None: # build private key - openpgp_privkey = _build_key_from_gpg( + openpgp_privkey = self._build_key_from_gpg( privkey, gpg.export_keys(privkey['fingerprint'], secret=True)) leap_check(pubkey['fingerprint'] == privkey['fingerprint'], @@ -383,7 +372,7 @@ class OpenPGPScheme(EncryptionScheme): errors.KeyFingerprintMismatch) # build public key - openpgp_pubkey = _build_key_from_gpg( + openpgp_pubkey = self._build_key_from_gpg( pubkey, gpg.export_keys(pubkey['fingerprint'], secret=False)) @@ -452,7 +441,7 @@ class OpenPGPScheme(EncryptionScheme): gpg.import_keys(oldkey.key_data) gpg.import_keys(key.key_data) gpgkey = gpg.list_keys(secret=key.private).pop() - mergedkey = _build_key_from_gpg( + mergedkey = self._build_key_from_gpg( gpgkey, gpg.export_keys(gpgkey['fingerprint'], secret=key.private)) @@ -571,6 +560,40 @@ class OpenPGPScheme(EncryptionScheme): d.addCallback(get_key_from_active_doc) return d + def _build_key_from_gpg(self, key, key_data): + """ + Build an OpenPGPKey for C{address} based on C{key} from + local gpg storage. + + ASCII armored GPG key data has to be queried independently in this + wrapper, so we receive it in C{key_data}. + + :param key: Key obtained from GPG storage. + :type key: dict + :param key_data: Key data obtained from GPG storage. + :type key_data: str + :return: An instance of the key. + :rtype: OpenPGPKey + """ + expiry_date = None + if key['expires']: + expiry_date = datetime.fromtimestamp(int(key['expires'])) + address = [] + for uid in key['uids']: + address.append(_parse_address(uid)) + + return OpenPGPKey( + address, + gpgbinary=self._gpgbinary, + key_id=key['keyid'], + fingerprint=key['fingerprint'], + key_data=key_data, + private=True if key['type'] == 'sec' else False, + length=int(key['length']), + expiry_date=expiry_date, + refreshed_at=datetime.now(), + ) + def delete_key(self, key): """ Remove C{key} from storage. diff --git a/keymanager/src/leap/keymanager/tests/test_validation.py b/keymanager/src/leap/keymanager/tests/test_validation.py index a8b35ca..15e7d27 100644 --- a/keymanager/src/leap/keymanager/tests/test_validation.py +++ b/keymanager/src/leap/keymanager/tests/test_validation.py @@ -117,6 +117,14 @@ class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): validation=ValidationLevel.Provider_Endorsement) yield self.assertFailure(d, KeyNotValidUpgrade) + @inlineCallbacks + def test_signed_key(self): + km = self._key_manager() + yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) + yield km.put_raw_key(SIGNED_KEY, OpenPGPKey, ADDRESS) + key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + self.assertEqual(key.fingerprint, SIGNED_FINGERPRINT) + # Key material for testing @@ -284,6 +292,53 @@ HEsoyd7WOsuse7+NkyUHgMXMVW7cz+nU7iO+ht2rkBtv+Z5LGlzgHTeFjKci -----END PGP PRIVATE KEY BLOCK----- """ +# key CA1AD31E: public key "Leap Test Key " +# signed by E36E738D69173C13D709E44F2F455E2824D18DDF +SIGNED_FINGERPRINT = "6704CF3362087DA23E3D2DF8ED81DFD1CA1AD31E" +SIGNED_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +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 +=xprj +-----END PGP PUBLIC KEY BLOCK----- +""" + import unittest if __name__ == "__main__": diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index b3aff3e..c6fe478 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -60,8 +60,6 @@ def can_upgrade(new_key, old_key): :type old_key: EncryptionKey :rtype: bool """ - # XXX implement key signature checking (#6120) - # First contact if old_key is None: return True @@ -90,4 +88,8 @@ def can_upgrade(new_key, old_key): new_key.validation > old_key.validation): return True + # New key signed by the old key + if old_key.key_id in new_key.signatures: + return True + return False -- cgit v1.2.3 From abf2bde0d74e9f6dbafa7fc088a094d7684f8d5a Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 5 Jan 2015 08:54:24 -0600 Subject: Port validation levels to enum34 --- keymanager/pkg/requirements.pip | 2 +- keymanager/src/leap/keymanager/keys.py | 2 +- .../src/leap/keymanager/tests/test_keymanager.py | 2 +- keymanager/src/leap/keymanager/validation.py | 20 ++++++++++---------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index 207483c..11c4229 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -4,4 +4,4 @@ requests # if we bump the gnupg version, bump also the sanity check # in keymanager.__init__ gnupg>=1.4.0 -enum +enum34 diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index 4e98de6..562c0a9 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -213,7 +213,7 @@ class EncryptionKey(object): KEY_EXPIRY_DATE_KEY: expiry_date, KEY_LAST_AUDITED_AT_KEY: last_audited_at, KEY_REFRESHED_AT_KEY: refreshed_at, - KEY_VALIDATION_KEY: str(self.validation), + KEY_VALIDATION_KEY: self.validation.name, KEY_ENCR_USED_KEY: self.encr_used, KEY_SIGN_USED_KEY: self.sign_used, KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG], diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 86832ba..93bc42c 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -79,7 +79,7 @@ class KeyManagerUtilTestCase(unittest.TestCase): 'expiry_date': 0, 'last_audited_at': 0, 'refreshed_at': 1311239602, - 'validation': str(ValidationLevel.Weak_Chain), + 'validation': ValidationLevel.Weak_Chain.name, 'encr_used': False, 'sign_used': True, } diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index c6fe478..c81d533 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -24,17 +24,17 @@ See: from datetime import datetime -from enum import Enum +from enum import IntEnum -ValidationLevel = Enum( - "Weak_Chain", - "Provider_Trust", - "Provider_Endorsement", - "Third_Party_Endorsement", - "Third_Party_Consensus", - "Historically_Auditing", - "Known_Key", +ValidationLevel = IntEnum("ValidationLevel", + "Weak_Chain " + "Provider_Trust " + "Provider_Endorsement " + "Third_Party_Endorsement " + "Third_Party_Consensus " + "Historically_Auditing " + "Known_Key " "Fingerprint") @@ -49,7 +49,7 @@ def toValidationLevel(value): :raises ValueError: if C{value} is not a validation level """ for level in ValidationLevel: - if value == str(level): + if value == level.name: return level raise ValueError("Not valid validation level: %s" % (value,)) -- cgit v1.2.3 From 74a2c68557fd86752bc52e1bda9f38e0460f0349 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Thu, 19 Feb 2015 10:21:56 -0600 Subject: Fetch keys should return KeyNotFound for unknown errors --- keymanager/changes/bug-4752_error_fetching_keys | 1 + keymanager/src/leap/keymanager/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 keymanager/changes/bug-4752_error_fetching_keys diff --git a/keymanager/changes/bug-4752_error_fetching_keys b/keymanager/changes/bug-4752_error_fetching_keys new file mode 100644 index 0000000..04bf1af --- /dev/null +++ b/keymanager/changes/bug-4752_error_fetching_keys @@ -0,0 +1 @@ +- Return always KeyNotFound failure if fetch keys fails on an unknown error diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index a1a59f5..3ef6350 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -230,11 +230,11 @@ class KeyManager(object): if e.response.status_code == 404: d = defer.fail(KeyNotFound(address)) else: - d = defer.fail(e) + d = defer.fail(KeyNotFound(e.message)) logger.warning("HTTP error retrieving key: %r" % (e,)) logger.warning("%s" % (res.content,)) except Exception as e: - d = defer.fail(e) + d = defer.fail(KeyNotFound(e.message)) logger.warning("Error retrieving key: %r" % (e,)) return d -- cgit v1.2.3 From f025de95fbb23ee222721c0f4de14f511c6d9aee Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 30 Mar 2015 11:25:21 +0200 Subject: [feat] set fetched keys as Weak Chain if they are not from the same domain Nicknym server is authoritative for its own domain, but for others it might retrieve keys from key servers. On keys from the same domain we set the validation level to 'Provider Trust'. For other domains in the email address we set it to 'Weak Chain' as we don't have info about its source. Resolves: #6815 Related: #6718 Releases: 0.4.0 --- .../changes/feature-6815_nicknym_validation_level | 1 + keymanager/src/leap/keymanager/__init__.py | 40 ++++++++- .../src/leap/keymanager/tests/test_keymanager.py | 96 ++++++++++++++++------ 3 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 keymanager/changes/feature-6815_nicknym_validation_level diff --git a/keymanager/changes/feature-6815_nicknym_validation_level b/keymanager/changes/feature-6815_nicknym_validation_level new file mode 100644 index 0000000..c82f7d4 --- /dev/null +++ b/keymanager/changes/feature-6815_nicknym_validation_level @@ -0,0 +1 @@ +- fetched keys from other domain than its provider are set as 'Weak Chain' validation level (Closes: #6815) diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 3ef6350..f7b1974 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -44,6 +44,7 @@ import logging import requests from twisted.internet import defer +from urlparse import urlparse from leap.common.check import leap_assert from leap.common.events import signal @@ -219,13 +220,21 @@ class KeyManager(object): res = self._get(self._nickserver_uri, {'address': address}) res.raise_for_status() server_keys = res.json() + # insert keys in local database if self.OPENPGP_KEY in server_keys: + # nicknym server is authoritative for its own domain, + # for other domains the key might come from key servers. + validation_level = ValidationLevel.Weak_Chain + _, domain = _split_email(address) + if (domain == _get_domain(self._nickserver_uri)): + validation_level = ValidationLevel.Provider_Trust + d = self.put_raw_key( server_keys['openpgp'], OpenPGPKey, address=address, - validation=ValidationLevel.Provider_Trust) + validation=validation_level) except requests.exceptions.HTTPError as e: if e.response.status_code == 404: d = defer.fail(KeyNotFound(address)) @@ -786,6 +795,35 @@ class KeyManager(object): if ktype not in self._wrapper_map: raise UnsupportedKeyTypeError(str(ktype)) + +def _split_email(address): + """ + Split username and domain from an email address + + :param address: an email address + :type address: str + + :return: username and domain from the email address + :rtype: (str, str) + """ + if address.count("@") != 1: + return None + return address.split("@") + + +def _get_domain(url): + """ + Get the domain from an url + + :param url: an url + :type url: str + + :return: the domain part of the url + :rtype: str + """ + return urlparse(url).hostname + + from ._version import get_versions __version__ = get_versions()['version'] del get_versions diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 93bc42c..55f892e 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -52,6 +52,9 @@ from leap.keymanager.tests import ( ) +NICKSERVER_URI = "http://leap.se/" + + class KeyManagerUtilTestCase(unittest.TestCase): def test_is_address(self): @@ -201,31 +204,16 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): """ Test that the request is well formed when fetching keys from server. """ - km = self._key_manager(url='http://nickserver.domain') - - class Response(object): - status_code = 200 - headers = {'content-type': 'application/json'} - - def json(self): - return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2} - - def raise_for_status(self): - pass - - # mock the fetcher so it returns the key for ADDRESS_2 - km._fetcher.get = Mock( - return_value=Response()) - km.ca_cert_path = 'cacertpath' + km = self._key_manager(url=NICKSERVER_URI) def verify_the_call(_): km._fetcher.get.assert_called_once_with( - 'http://nickserver.domain', + NICKSERVER_URI, data={'address': ADDRESS_2}, verify='cacertpath', ) - d = km._fetch_keys_from_server(ADDRESS_2) + d = self._fetch_key(km, ADDRESS_2, PUBLIC_KEY_2) d.addCallback(verify_the_call) return d @@ -234,14 +222,35 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): """ Test that getting a key successfuly fetches from server. """ - km = self._key_manager(url='http://nickserver.domain') + km = self._key_manager(url=NICKSERVER_URI) + + key = yield self._fetch_key(km, ADDRESS, PUBLIC_KEY) + self.assertIsInstance(key, OpenPGPKey) + self.assertTrue(ADDRESS in key.address) + self.assertEqual(key.validation, ValidationLevel.Provider_Trust) + + @inlineCallbacks + def test_get_key_fetches_other_domain(self): + """ + Test that getting a key successfuly fetches from server. + """ + km = self._key_manager(url=NICKSERVER_URI) + + key = yield self._fetch_key(km, ADDRESS_OTHER, PUBLIC_KEY_OTHER) + self.assertIsInstance(key, OpenPGPKey) + self.assertTrue(ADDRESS_OTHER in key.address) + self.assertEqual(key.validation, ValidationLevel.Weak_Chain) + def _fetch_key(self, km, address, key): + """ + :returns: a Deferred that will fire with the OpenPGPKey + """ class Response(object): status_code = 200 headers = {'content-type': 'application/json'} def json(self): - return {'address': ADDRESS, 'openpgp': PUBLIC_KEY} + return {'address': address, 'openpgp': key} def raise_for_status(self): pass @@ -250,19 +259,18 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): km._fetcher.get = Mock(return_value=Response()) km.ca_cert_path = 'cacertpath' # try to key get without fetching from server - d = km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) - yield self.assertFailure(d, KeyNotFound) + d_fail = km.get_key(address, OpenPGPKey, fetch_remote=False) + d = self.assertFailure(d_fail, KeyNotFound) # try to get key fetching from server. - key = yield km.get_key(ADDRESS, OpenPGPKey) - self.assertIsInstance(key, OpenPGPKey) - self.assertTrue(ADDRESS in key.address) + d.addCallback(lambda _: km.get_key(address, OpenPGPKey)) + return d @inlineCallbacks def test_put_key_ascii(self): """ Test that putting ascii key works """ - km = self._key_manager(url='http://nickserver.domain') + km = self._key_manager(url=NICKSERVER_URI) yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) key = yield km.get_key(ADDRESS, OpenPGPKey) @@ -387,3 +395,39 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): import unittest if __name__ == "__main__": unittest.main() + +# key 0F91B402: someone@somedomain.org +# 9420 EC7B 6DCB 867F 5592 E6D1 7504 C974 0F91 B402 +ADDRESS_OTHER = "someone@somedomain.org" +PUBLIC_KEY_OTHER = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mQENBFUZFLwBCADRzTstykRAV3aWysLAV4O3DXdpXhV3Cww8Pfc6m1bVxAT2ifcL +kLWEaIkOB48SYIHbYzqOi1/h5abJf+5n4uhaIks+FsjsXYo1XOiYpVCNf7+xLnUM +jkmglKT5sASr61QDcFMqWfGTJ8iUTNVCJZ2k14QJ4Vss/ntnV9uB7Ef7wU7RZvxr +wINH/0LfKPsGE9l2qNpKUAAmg2bHn9YdsHj1sqlW7eZpwvefYrQej4KBaL2oq3vt +QQOdXGFqWYMe3cX+bQ1DAMG3ttTF6EGkY97BK7A18I/RJiLujWCEAkMzFr5SK9KU +AOMj6MpjfTOE+GfUKsu7/gGt42eMBFsIOvsZABEBAAG0IFNvbWVvbmUgPHNvbWVv +bmVAc29tZWRvbWFpbi5vcmc+iQE4BBMBAgAiBQJVGRS8AhsDBgsJCAcDAgYVCAIJ +CgsEFgIDAQIeAQIXgAAKCRB1BMl0D5G0AlFsCAC33LhxBRwO64T6DgTb4/39aLpi +9T3yAmXBAHC7Q+4f37IBX5fJBRKu4Lvfp6KherOl/I/Jj34yv8pm0j+kXeWktfxZ +cW+mv2vjBHQVopiUSyMVh7caFSq9sKm+oQdo6oIl9DHSARegbkCn2+0b4VxgJpyj +TZBMyUMD2AayivQU4QHOM3KCozhLNNDbpKy7LH0MSAUDmRaJsPk1zK15lQocK/7R +Z5yF4rdrdzDWrVucZJc09yntSqTGECue3W2GBCaBlb/O1c9xei4MTb4nSHS5Gp/7 +hcjrvIrgPpehndk8ZRREN/Y8uk1W5fbWzx+5z8g31RCGWBQw4NAnG10NZ3oEuQEN +BFUZFLwBCADocYZmLu1iXIE6gKqniR6Z8UDC5XnqgK+BEJwi1abe9zWhjgKeW9Vv +u1i194wuCUiNkP/bMvwMBZLTslDzqxl32ETk9FvB3kWy80S8MDjQJ15IN4I622fq +MEWwtQ0WrRay9VV6M8H2mIf71/1d5T9ysWK4XRyv+N7eRhfg7T2uhrpNyKdCZzjq +2wlgpVkMY7gtxTqJseM+qS5UNiReGxtoOXFLzzmagFgbqK88eMeZJZt8yKf81xhP +SWLTxaVaeBEAlajvEkxZJrrDQuc+maTwtMxmNUe815wJnpcRF8VD91GUpSLAN6EC +1QuJUl6Lc2o2tcHeo6CGsDZ96o0J8pFhABEBAAGJAR8EGAECAAkFAlUZFLwCGwwA +CgkQdQTJdA+RtAKcdwgApzHPhwwaZ9TBjgOytke/hPE0ht/EJ5nRiIda2PucoPh6 +DwnaI8nvmGXUfC4qFy6LM8/fJHof1BqLnMbx8MCLurnm5z30q8RhLE3YWM11zuMy +6wkHGmi/6S1G4okC+Uu8AA4K//HBo8bLcqGVWRnFAmCqy6VMAofsQvmM7vHbRj56 +U919Bki/7I6kcxPEzO73Umh3o82VP/Hz3JMigRNBRfG3jPrX04RLJj3Ib5lhQIDw +XrO8VHz9foOpY+rJnWj+6QAozxorzZYShu6H0GR1nIuqWMwli1nrx6BeIJAVz5cg +QzEd9yAN+81fkIBaa6Y8LCBxV03JCc2J4eCUKXd1gg== +=gDzy +-----END PGP PUBLIC KEY BLOCK----- +""" -- cgit v1.2.3 From 13babd9fe81dd7e3af7266fb99306c7726cf84f5 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Tue, 7 Apr 2015 12:56:20 +0200 Subject: [doc] added the right link the validation levels documentation The mailing list was linked, but now there is a proper documentation page. - Releases: 0.4.0 --- keymanager/src/leap/keymanager/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index c81d533..dfe6432 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -19,7 +19,7 @@ Validation levels implementation for key managment. See: - https://lists.riseup.net/www/arc/leap-discuss/2014-09/msg00000.html + https://leap.se/en/docs/design/transitional-key-validation """ -- cgit v1.2.3 From 298d73867104e4e81102392ffb0171fd07b8ab33 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 6 May 2015 18:04:03 -0300 Subject: [feat] adapt to new events api on common - Related: #6359 --- keymanager/changes/VERSION_COMPAT | 2 +- .../changes/feature_adapt-to-new-events-on-common | 1 + keymanager/src/leap/keymanager/__init__.py | 17 ++++++++--------- 3 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 keymanager/changes/feature_adapt-to-new-events-on-common diff --git a/keymanager/changes/VERSION_COMPAT b/keymanager/changes/VERSION_COMPAT index ac65675..0bb331a 100644 --- a/keymanager/changes/VERSION_COMPAT +++ b/keymanager/changes/VERSION_COMPAT @@ -8,4 +8,4 @@ # # BEGIN DEPENDENCY LIST ------------------------- # leap.foo.bar>=x.y.z -leap.common>=0.3.10 +leap.common>=0.4 diff --git a/keymanager/changes/feature_adapt-to-new-events-on-common b/keymanager/changes/feature_adapt-to-new-events-on-common new file mode 100644 index 0000000..e57e777 --- /dev/null +++ b/keymanager/changes/feature_adapt-to-new-events-on-common @@ -0,0 +1 @@ +- Adapt to new events api on leap.common. Related to #5359. diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index f7b1974..47f479b 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -47,8 +47,7 @@ from twisted.internet import defer from urlparse import urlparse from leap.common.check import leap_assert -from leap.common.events import signal -from leap.common.events import events_pb2 as proto +from leap.common.events import emit, catalog from leap.common.decorators import memoized_method from leap.keymanager.errors import ( @@ -278,7 +277,7 @@ class KeyManager(object): self._api_version, self._uid) self._put(uri, data) - signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address) + emit(catalog.KEYMANAGER_DONE_UPLOADING_KEYS, self._address) d = self.get_key( self._address, ktype, private=False, fetch_remote=False) @@ -314,24 +313,24 @@ class KeyManager(object): leap_assert( ktype in self._wrapper_map, 'Unkown key type: %s.' % str(ktype)) - signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address) + emit(catalog.KEYMANAGER_LOOKING_FOR_KEY, address) def key_found(key): - signal(proto.KEYMANAGER_KEY_FOUND, address) + emit(catalog.KEYMANAGER_KEY_FOUND, address) return key def key_not_found(failure): if not failure.check(KeyNotFound): return failure - signal(proto.KEYMANAGER_KEY_NOT_FOUND, address) + emit(catalog.KEYMANAGER_KEY_NOT_FOUND, address) # we will only try to fetch a key from nickserver if fetch_remote # is True and the key is not private. if fetch_remote is False or private is True: return failure - signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address) + emit(catalog.KEYMANAGER_LOOKING_FOR_KEY, address) d = self._fetch_keys_from_server(address) d.addCallback( lambda _: @@ -388,10 +387,10 @@ class KeyManager(object): self._assert_supported_key_type(ktype) def signal_finished(key): - signal(proto.KEYMANAGER_FINISHED_KEY_GENERATION, self._address) + emit(catalog.KEYMANAGER_FINISHED_KEY_GENERATION, self._address) return key - signal(proto.KEYMANAGER_STARTED_KEY_GENERATION, self._address) + emit(catalog.KEYMANAGER_STARTED_KEY_GENERATION, self._address) d = self._wrapper_map[ktype].gen_key(self._address) d.addCallback(signal_finished) return d -- cgit v1.2.3 From ef84af7221f7a692d6cad01a5eb839206d421ef0 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 8 Jun 2015 15:20:34 -0300 Subject: [pkg] fold in changes --- keymanager/CHANGELOG | 20 ++++++++++++++++++++ keymanager/changes/bug-4752_error_fetching_keys | 1 - keymanager/changes/bug_6022_fix-call-to-verify-file | 1 - keymanager/changes/feature-5932_fetch_keys_uri | 1 - keymanager/changes/feature-6211_succesfully_used | 1 - keymanager/changes/feature-6212_multi_uid_support | 1 - keymanager/changes/feature-6240_signed_key | 1 - keymanager/changes/feature-6299_new_doc | 2 -- keymanager/changes/feature-6346_use_addresses | 1 - keymanager/changes/feature-6366_expose_signing_key | 1 - keymanager/changes/feature-6368_async_api | 1 - .../changes/feature-6815_nicknym_validation_level | 1 - keymanager/changes/feature-cleanup_api | 1 - keymanager/changes/feature-put_key_ascii | 1 - .../changes/feature_adapt-to-new-events-on-common | 1 - 15 files changed, 20 insertions(+), 15 deletions(-) delete mode 100644 keymanager/changes/bug-4752_error_fetching_keys delete mode 100644 keymanager/changes/bug_6022_fix-call-to-verify-file delete mode 100644 keymanager/changes/feature-5932_fetch_keys_uri delete mode 100644 keymanager/changes/feature-6211_succesfully_used delete mode 100644 keymanager/changes/feature-6212_multi_uid_support delete mode 100644 keymanager/changes/feature-6240_signed_key delete mode 100644 keymanager/changes/feature-6299_new_doc delete mode 100644 keymanager/changes/feature-6346_use_addresses delete mode 100644 keymanager/changes/feature-6366_expose_signing_key delete mode 100644 keymanager/changes/feature-6368_async_api delete mode 100644 keymanager/changes/feature-6815_nicknym_validation_level delete mode 100644 keymanager/changes/feature-cleanup_api delete mode 100644 keymanager/changes/feature-put_key_ascii delete mode 100644 keymanager/changes/feature_adapt-to-new-events-on-common diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index fc8333b..381e4e7 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,23 @@ +0.4.0 Jun 8, 2015: + o Adapt to new events api on leap.common. Related to #5359. + o Add 'fetch_key' method to fetch keys from a URI. Closes #5932. + o Clean up API. + o Fix call to python-gnupg's verify_file() method. Closes #6022. + o KeyManager.put_key now accepts also ascii keys. + o Multi uid support. Closes #6212. + o Port keymanager to the new soledad async API. Closes #6368. + o Return always KeyNotFound failure if fetch keys fails on an unknown error. + o Upgrade keys if not successfully used and strict high validation level. + Closes #6211. + o Use addresses instead of keys for encrypt, decrypt, sign & verify. + Closes #6346. + o Expose info about the signing key. Closes #6366. + o Fetched keys from other domain than its provider are set as 'Weak Chain' + validation level. Closes #6815. + o Keep old key after upgrade. Closes #6262. + o New soledad doc struct for encryption-keys. Closes #6299. + o Upgrade key when signed by old key. Closes #6240. + 0.3.8 Apr 4, 2014: o Properly raise KeyNotFound exception when looking for keys on nickserver. Fixes #5415. diff --git a/keymanager/changes/bug-4752_error_fetching_keys b/keymanager/changes/bug-4752_error_fetching_keys deleted file mode 100644 index 04bf1af..0000000 --- a/keymanager/changes/bug-4752_error_fetching_keys +++ /dev/null @@ -1 +0,0 @@ -- Return always KeyNotFound failure if fetch keys fails on an unknown error diff --git a/keymanager/changes/bug_6022_fix-call-to-verify-file b/keymanager/changes/bug_6022_fix-call-to-verify-file deleted file mode 100644 index 121e25a..0000000 --- a/keymanager/changes/bug_6022_fix-call-to-verify-file +++ /dev/null @@ -1 +0,0 @@ - o Fix call to python-gnupg's verify_file() method (#6022). diff --git a/keymanager/changes/feature-5932_fetch_keys_uri b/keymanager/changes/feature-5932_fetch_keys_uri deleted file mode 100644 index 6f628a6..0000000 --- a/keymanager/changes/feature-5932_fetch_keys_uri +++ /dev/null @@ -1 +0,0 @@ -- Add 'fetch_key' method to fetch keys from a URI (closes #5932) diff --git a/keymanager/changes/feature-6211_succesfully_used b/keymanager/changes/feature-6211_succesfully_used deleted file mode 100644 index 74f0bba..0000000 --- a/keymanager/changes/feature-6211_succesfully_used +++ /dev/null @@ -1 +0,0 @@ -- Upgrade keys if not successfully used and strict high validation level (closes: #6211) diff --git a/keymanager/changes/feature-6212_multi_uid_support b/keymanager/changes/feature-6212_multi_uid_support deleted file mode 100644 index 495b8c4..0000000 --- a/keymanager/changes/feature-6212_multi_uid_support +++ /dev/null @@ -1 +0,0 @@ -- Multi uid support (closes #6212) diff --git a/keymanager/changes/feature-6240_signed_key b/keymanager/changes/feature-6240_signed_key deleted file mode 100644 index 7236950..0000000 --- a/keymanager/changes/feature-6240_signed_key +++ /dev/null @@ -1 +0,0 @@ -- upgrade key when signed by old key (Closes #6240) diff --git a/keymanager/changes/feature-6299_new_doc b/keymanager/changes/feature-6299_new_doc deleted file mode 100644 index c893338..0000000 --- a/keymanager/changes/feature-6299_new_doc +++ /dev/null @@ -1,2 +0,0 @@ -- new soledad doc struct for encryption-keys (closes #6299) -- keep old key after upgrade (closes #6262) diff --git a/keymanager/changes/feature-6346_use_addresses b/keymanager/changes/feature-6346_use_addresses deleted file mode 100644 index 2f32c98..0000000 --- a/keymanager/changes/feature-6346_use_addresses +++ /dev/null @@ -1 +0,0 @@ -- Use addresses instead of keys for encrypt, decrypt, sign & verify (closes #6346) diff --git a/keymanager/changes/feature-6366_expose_signing_key b/keymanager/changes/feature-6366_expose_signing_key deleted file mode 100644 index d594a15..0000000 --- a/keymanager/changes/feature-6366_expose_signing_key +++ /dev/null @@ -1 +0,0 @@ -- expose info about the signing key (closes #6366) diff --git a/keymanager/changes/feature-6368_async_api b/keymanager/changes/feature-6368_async_api deleted file mode 100644 index b584daa..0000000 --- a/keymanager/changes/feature-6368_async_api +++ /dev/null @@ -1 +0,0 @@ -- Port keymanager to the new soledad async API (Closes 6368) diff --git a/keymanager/changes/feature-6815_nicknym_validation_level b/keymanager/changes/feature-6815_nicknym_validation_level deleted file mode 100644 index c82f7d4..0000000 --- a/keymanager/changes/feature-6815_nicknym_validation_level +++ /dev/null @@ -1 +0,0 @@ -- fetched keys from other domain than its provider are set as 'Weak Chain' validation level (Closes: #6815) diff --git a/keymanager/changes/feature-cleanup_api b/keymanager/changes/feature-cleanup_api deleted file mode 100644 index cc7d5d6..0000000 --- a/keymanager/changes/feature-cleanup_api +++ /dev/null @@ -1 +0,0 @@ -- Clean up API diff --git a/keymanager/changes/feature-put_key_ascii b/keymanager/changes/feature-put_key_ascii deleted file mode 100644 index 78f2907..0000000 --- a/keymanager/changes/feature-put_key_ascii +++ /dev/null @@ -1 +0,0 @@ -- KeyManager.put_key now accepts also ascii keys diff --git a/keymanager/changes/feature_adapt-to-new-events-on-common b/keymanager/changes/feature_adapt-to-new-events-on-common deleted file mode 100644 index e57e777..0000000 --- a/keymanager/changes/feature_adapt-to-new-events-on-common +++ /dev/null @@ -1 +0,0 @@ -- Adapt to new events api on leap.common. Related to #5359. -- cgit v1.2.3 From a14c9614ad95ec4f917890f4a8b1dd5398d33164 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Mon, 8 Jun 2015 15:21:00 -0300 Subject: [pkg] bump dependencies --- keymanager/changes/VERSION_COMPAT | 1 - keymanager/pkg/requirements.pip | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/keymanager/changes/VERSION_COMPAT b/keymanager/changes/VERSION_COMPAT index 0bb331a..cc00ecf 100644 --- a/keymanager/changes/VERSION_COMPAT +++ b/keymanager/changes/VERSION_COMPAT @@ -8,4 +8,3 @@ # # BEGIN DEPENDENCY LIST ------------------------- # leap.foo.bar>=x.y.z -leap.common>=0.4 diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index 11c4229..c81c1a1 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -1,4 +1,4 @@ -leap.common>=0.3.7 +leap.common>=0.4.0 simplejson requests # if we bump the gnupg version, bump also the sanity check -- cgit v1.2.3 From 7698fba53ff8d5d311d53d784a83f94a78c68035 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Fri, 26 Jun 2015 18:12:22 +0200 Subject: [bug] remove the dependency on enum34 * Resolves: #7188 --- keymanager/changes/bug-7188_rm_enum34 | 1 + keymanager/pkg/requirements.pip | 1 - keymanager/src/leap/keymanager/__init__.py | 14 ++--- keymanager/src/leap/keymanager/keys.py | 10 +-- .../src/leap/keymanager/tests/test_keymanager.py | 13 ++-- .../src/leap/keymanager/tests/test_validation.py | 18 +++--- keymanager/src/leap/keymanager/validation.py | 73 +++++++++++++++------- 7 files changed, 78 insertions(+), 52 deletions(-) create mode 100644 keymanager/changes/bug-7188_rm_enum34 diff --git a/keymanager/changes/bug-7188_rm_enum34 b/keymanager/changes/bug-7188_rm_enum34 new file mode 100644 index 0000000..4788df1 --- /dev/null +++ b/keymanager/changes/bug-7188_rm_enum34 @@ -0,0 +1 @@ +- remove the dependency on enum34 (Closes: #7188) diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index c81c1a1..1aef332 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -4,4 +4,3 @@ requests # if we bump the gnupg version, bump also the sanity check # in keymanager.__init__ gnupg>=1.4.0 -enum34 diff --git a/keymanager/src/leap/keymanager/__init__.py b/keymanager/src/leap/keymanager/__init__.py index 47f479b..c2d7409 100644 --- a/keymanager/src/leap/keymanager/__init__.py +++ b/keymanager/src/leap/keymanager/__init__.py @@ -57,7 +57,7 @@ from leap.keymanager.errors import ( UnsupportedKeyTypeError, InvalidSignature ) -from leap.keymanager.validation import ValidationLevel, can_upgrade +from leap.keymanager.validation import ValidationLevels, can_upgrade from leap.keymanager.keys import ( build_key_from_dict, @@ -224,10 +224,10 @@ class KeyManager(object): if self.OPENPGP_KEY in server_keys: # nicknym server is authoritative for its own domain, # for other domains the key might come from key servers. - validation_level = ValidationLevel.Weak_Chain + validation_level = ValidationLevels.Weak_Chain _, domain = _split_email(address) if (domain == _get_domain(self._nickserver_uri)): - validation_level = ValidationLevel.Provider_Trust + validation_level = ValidationLevels.Provider_Trust d = self.put_raw_key( server_keys['openpgp'], @@ -712,7 +712,7 @@ class KeyManager(object): return d def put_raw_key(self, key, ktype, address, - validation=ValidationLevel.Weak_Chain): + validation=ValidationLevels.Weak_Chain): """ Put raw key bound to address in local storage. @@ -724,7 +724,7 @@ class KeyManager(object): :type address: str :param validation: validation level for this key (default: 'Weak_Chain') - :type validation: ValidationLevel + :type validation: ValidationLevels :return: A Deferred which fires when the key is in the storage, or which fails with KeyAddressMismatch if address doesn't match @@ -744,7 +744,7 @@ class KeyManager(object): return d def fetch_key(self, address, uri, ktype, - validation=ValidationLevel.Weak_Chain): + validation=ValidationLevels.Weak_Chain): """ Fetch a public key bound to address from the network and put it in local storage. @@ -757,7 +757,7 @@ class KeyManager(object): :type ktype: subclass of EncryptionKey :param validation: validation level for this key (default: 'Weak_Chain') - :type validation: ValidationLevel + :type validation: ValidationLevels :return: A Deferred which fires when the key is in the storage, or which fails with KeyNotFound: if not valid key on uri or fails diff --git a/keymanager/src/leap/keymanager/keys.py b/keymanager/src/leap/keymanager/keys.py index 562c0a9..91559c2 100644 --- a/keymanager/src/leap/keymanager/keys.py +++ b/keymanager/src/leap/keymanager/keys.py @@ -35,7 +35,7 @@ from datetime import datetime from leap.common.check import leap_assert from twisted.internet import defer -from leap.keymanager.validation import ValidationLevel, toValidationLevel +from leap.keymanager.validation import ValidationLevels logger = logging.getLogger(__name__) @@ -120,11 +120,11 @@ def build_key_from_dict(kClass, kdict): :rtype: C{kClass} """ try: - validation = toValidationLevel(kdict[KEY_VALIDATION_KEY]) + validation = ValidationLevels.get(kdict[KEY_VALIDATION_KEY]) except ValueError: logger.error("Not valid validation level (%s) for key %s", (kdict[KEY_VALIDATION_KEY], kdict[KEY_ID_KEY])) - validation = ValidationLevel.Weak_Chain + validation = ValidationLevels.Weak_Chain expiry_date = _to_datetime(kdict[KEY_EXPIRY_DATE_KEY]) last_audited_at = _to_datetime(kdict[KEY_LAST_AUDITED_AT_KEY]) @@ -176,7 +176,7 @@ class EncryptionKey(object): def __init__(self, address, key_id="", fingerprint="", key_data="", private=False, length=0, expiry_date=None, - validation=ValidationLevel.Weak_Chain, last_audited_at=None, + validation=ValidationLevels.Weak_Chain, last_audited_at=None, refreshed_at=None, encr_used=False, sign_used=False): self.address = address self.key_id = key_id @@ -213,7 +213,7 @@ class EncryptionKey(object): KEY_EXPIRY_DATE_KEY: expiry_date, KEY_LAST_AUDITED_AT_KEY: last_audited_at, KEY_REFRESHED_AT_KEY: refreshed_at, - KEY_VALIDATION_KEY: self.validation.name, + KEY_VALIDATION_KEY: str(self.validation), KEY_ENCR_USED_KEY: self.encr_used, KEY_SIGN_USED_KEY: self.sign_used, KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG], diff --git a/keymanager/src/leap/keymanager/tests/test_keymanager.py b/keymanager/src/leap/keymanager/tests/test_keymanager.py index 55f892e..08d3750 100644 --- a/keymanager/src/leap/keymanager/tests/test_keymanager.py +++ b/keymanager/src/leap/keymanager/tests/test_keymanager.py @@ -36,10 +36,7 @@ from leap.keymanager.keys import ( is_address, build_key_from_dict, ) -from leap.keymanager.validation import ( - ValidationLevel, - toValidationLevel -) +from leap.keymanager.validation import ValidationLevels from leap.keymanager.tests import ( KeyManagerWithSoledadTestCase, ADDRESS, @@ -82,7 +79,7 @@ class KeyManagerUtilTestCase(unittest.TestCase): 'expiry_date': 0, 'last_audited_at': 0, 'refreshed_at': 1311239602, - 'validation': ValidationLevel.Weak_Chain.name, + 'validation': str(ValidationLevels.Weak_Chain), 'encr_used': False, 'sign_used': True, } @@ -115,7 +112,7 @@ class KeyManagerUtilTestCase(unittest.TestCase): datetime.fromtimestamp(kdict['refreshed_at']), key.refreshed_at, 'Wrong data in key.') self.assertEqual( - toValidationLevel(kdict['validation']), key.validation, + ValidationLevels.get(kdict['validation']), key.validation, 'Wrong data in key.') self.assertEqual( kdict['encr_used'], key.encr_used, @@ -227,7 +224,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): key = yield self._fetch_key(km, ADDRESS, PUBLIC_KEY) self.assertIsInstance(key, OpenPGPKey) self.assertTrue(ADDRESS in key.address) - self.assertEqual(key.validation, ValidationLevel.Provider_Trust) + self.assertEqual(key.validation, ValidationLevels.Provider_Trust) @inlineCallbacks def test_get_key_fetches_other_domain(self): @@ -239,7 +236,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): key = yield self._fetch_key(km, ADDRESS_OTHER, PUBLIC_KEY_OTHER) self.assertIsInstance(key, OpenPGPKey) self.assertTrue(ADDRESS_OTHER in key.address) - self.assertEqual(key.validation, ValidationLevel.Weak_Chain) + self.assertEqual(key.validation, ValidationLevels.Weak_Chain) def _fetch_key(self, km, address, key): """ diff --git a/keymanager/src/leap/keymanager/tests/test_validation.py b/keymanager/src/leap/keymanager/tests/test_validation.py index 15e7d27..0c1d155 100644 --- a/keymanager/src/leap/keymanager/tests/test_validation.py +++ b/keymanager/src/leap/keymanager/tests/test_validation.py @@ -31,10 +31,10 @@ from leap.keymanager.tests import ( PUBLIC_KEY, KEY_FINGERPRINT ) -from leap.keymanager.validation import ValidationLevel +from leap.keymanager.validation import ValidationLevels -class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): +class ValidationLevelsTestCase(KeyManagerWithSoledadTestCase): @inlineCallbacks def test_none_old_key(self): @@ -47,7 +47,7 @@ class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): def test_cant_upgrade(self): km = self._key_manager() yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Provider_Trust) + validation=ValidationLevels.Provider_Trust) d = km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS) yield self.assertFailure(d, KeyNotValidUpgrade) @@ -56,7 +56,7 @@ class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): km = self._key_manager() yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) yield km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Fingerprint) + validation=ValidationLevels.Fingerprint) key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT) @@ -73,12 +73,12 @@ class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): km = self._key_manager() yield km.put_raw_key( EXPIRED_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Third_Party_Endorsement) + validation=ValidationLevels.Third_Party_Endorsement) d = km.put_raw_key( UNRELATED_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Provider_Trust) + validation=ValidationLevels.Provider_Trust) yield self.assertFailure(d, KeyNotValidUpgrade) @inlineCallbacks @@ -93,9 +93,9 @@ class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): def test_not_used(self): km = self._key_manager() yield km.put_raw_key(UNEXPIRED_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Provider_Trust) + validation=ValidationLevels.Provider_Trust) yield km.put_raw_key(UNRELATED_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Provider_Endorsement) + validation=ValidationLevels.Provider_Endorsement) key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT) @@ -114,7 +114,7 @@ class ValidationLevelTestCase(KeyManagerWithSoledadTestCase): yield km.verify(TEXT, ADDRESS, OpenPGPKey, detached_sig=signature) d = km.put_raw_key( UNRELATED_KEY, OpenPGPKey, ADDRESS, - validation=ValidationLevel.Provider_Endorsement) + validation=ValidationLevels.Provider_Endorsement) yield self.assertFailure(d, KeyNotValidUpgrade) @inlineCallbacks diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index dfe6432..3bb4032 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -24,34 +24,63 @@ See: from datetime import datetime -from enum import IntEnum -ValidationLevel = IntEnum("ValidationLevel", - "Weak_Chain " - "Provider_Trust " - "Provider_Endorsement " - "Third_Party_Endorsement " - "Third_Party_Consensus " - "Historically_Auditing " - "Known_Key " - "Fingerprint") +class ValidationLevel(object): + """ + A validation level + + Meant to be used to compare levels or get it's string representation. + """ + def __init__(self, name, value): + self.name = name + self.value = value + + def __cmp__(self, other): + return cmp(self.value, other.value) + + def __str__(self): + return self.name + + def __repr__(self): + return "" % (self.name, self.value) -def toValidationLevel(value): +class _ValidationLevels(object): """ - Convert a string representation of a validation level into - C{ValidationLevel} + Handler class to manage validation levels. It should have only one global + instance 'ValidationLevels'. - :param value: validation level - :type value: str - :rtype: ValidationLevel - :raises ValueError: if C{value} is not a validation level + The levels are attributes of the instance and can be used like: + ValidationLevels.Weak_Chain + ValidationLevels.get("Weak_Chain") """ - for level in ValidationLevel: - if value == level.name: - return level - raise ValueError("Not valid validation level: %s" % (value,)) + _level_names = ("Weak_Chain", + "Provider_Trust", + "Provider_Endorsement", + "Third_Party_Endorsement", + "Third_Party_Consensus", + "Historically_Auditing", + "Known_Key", + "Fingerprint") + + def __init__(self): + for name in self._level_names: + setattr(self, name, + ValidationLevel(name, self._level_names.index(name))) + + def get(self, name): + """ + Get the ValidationLevel of a name + + :param name: name of the level + :type name: str + :rtype: ValidationLevel + """ + return getattr(self, name) + + +ValidationLevels = _ValidationLevels() def can_upgrade(new_key, old_key): @@ -69,7 +98,7 @@ def can_upgrade(new_key, old_key): return True # Manually verified fingerprint - if new_key.validation == ValidationLevel.Fingerprint: + if new_key.validation == ValidationLevels.Fingerprint: return True # Expired key and higher validation level -- cgit v1.2.3 From 45669db6a8bd40c2ef115355c5d503e7211930ad Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 29 Jun 2015 12:05:51 -0400 Subject: [style] spelling typo --- keymanager/src/leap/keymanager/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymanager/src/leap/keymanager/validation.py b/keymanager/src/leap/keymanager/validation.py index 3bb4032..734cfce 100644 --- a/keymanager/src/leap/keymanager/validation.py +++ b/keymanager/src/leap/keymanager/validation.py @@ -30,7 +30,7 @@ class ValidationLevel(object): """ A validation level - Meant to be used to compare levels or get it's string representation. + Meant to be used to compare levels or get its string representation. """ def __init__(self, name, value): self.name = name -- cgit v1.2.3 From b954f6bfe19a11b579735cc642d61d250ee378df Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Fri, 10 Jul 2015 14:44:19 -0300 Subject: [pkg] fold in changes --- keymanager/CHANGELOG | 3 +++ keymanager/changes/bug-7188_rm_enum34 | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 keymanager/changes/bug-7188_rm_enum34 diff --git a/keymanager/CHANGELOG b/keymanager/CHANGELOG index 381e4e7..6b25f47 100644 --- a/keymanager/CHANGELOG +++ b/keymanager/CHANGELOG @@ -1,3 +1,6 @@ +0.4.1 Jul 10, 2015: + o Remove the dependency on enum34. Closes: #7188. + 0.4.0 Jun 8, 2015: o Adapt to new events api on leap.common. Related to #5359. o Add 'fetch_key' method to fetch keys from a URI. Closes #5932. diff --git a/keymanager/changes/bug-7188_rm_enum34 b/keymanager/changes/bug-7188_rm_enum34 deleted file mode 100644 index 4788df1..0000000 --- a/keymanager/changes/bug-7188_rm_enum34 +++ /dev/null @@ -1 +0,0 @@ -- remove the dependency on enum34 (Closes: #7188) -- cgit v1.2.3 From d0535f4e613dddef4037f62bb42a6b8250489871 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 22 Jul 2015 10:31:59 -0400 Subject: [pkg] separate leap requirements this is part of a process to make the setup of the development mode less troublesome. from now on, setting up a virtualenv in pure development mode will be as easy as telling pip to just install the external dependencies:: pip install -r pkg/requirements.pip and traversing all the leap repos for the needed leap dependencies doing:: python setup.py develop - Related: #7288 --- keymanager/pkg/requirements-leap.pip | 1 + keymanager/pkg/requirements.pip | 5 ++--- keymanager/pkg/utils.py | 11 +++++++---- keymanager/setup.py | 6 +++++- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 keymanager/pkg/requirements-leap.pip diff --git a/keymanager/pkg/requirements-leap.pip b/keymanager/pkg/requirements-leap.pip new file mode 100644 index 0000000..b311859 --- /dev/null +++ b/keymanager/pkg/requirements-leap.pip @@ -0,0 +1 @@ +leap.common>=0.4.0 diff --git a/keymanager/pkg/requirements.pip b/keymanager/pkg/requirements.pip index 1aef332..c4cb09a 100644 --- a/keymanager/pkg/requirements.pip +++ b/keymanager/pkg/requirements.pip @@ -1,6 +1,5 @@ -leap.common>=0.4.0 -simplejson -requests # if we bump the gnupg version, bump also the sanity check # in keymanager.__init__ gnupg>=1.4.0 +simplejson +requests diff --git a/keymanager/pkg/utils.py b/keymanager/pkg/utils.py index deace14..dd3deac 100644 --- a/keymanager/pkg/utils.py +++ b/keymanager/pkg/utils.py @@ -27,7 +27,7 @@ import sys def get_reqs_from_files(reqfiles): """ Returns the contents of the top requirement file listed as a - string list with the lines + string list with the lines. @param reqfiles: requirement files to parse @type reqfiles: list of str @@ -43,6 +43,9 @@ def parse_requirements(reqfiles=['requirements.txt', """ Parses the requirement files provided. + The passed reqfiles list is a list of possible locations to try, the + function will return the contents of the first path found. + Checks the value of LEAP_VENV_SKIP_PYSIDE to see if it should return PySide as a dep or not. Don't set, or set to 0 if you want to install it through pip. @@ -58,9 +61,9 @@ def parse_requirements(reqfiles=['requirements.txt', if re.match(r'\s*-e\s+', line): pass # do not try to do anything with externals on vcs - #requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', - #line)) - # http://foo.bar/baz/foobar/zipball/master#egg=foobar + # requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', + # line)) + # http://foo.bar/baz/foobar/zipball/master#egg=foobar elif re.match(r'\s*https?:', line): requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', line)) diff --git a/keymanager/setup.py b/keymanager/setup.py index 778909d..f554e09 100644 --- a/keymanager/setup.py +++ b/keymanager/setup.py @@ -107,6 +107,10 @@ cmdclass["freeze_debianver"] = freeze_debianver # XXX add ref to docs +requirements = ( + utils.parse_requirements() + + utils.parse_requirements(reqfiles=["pkg/requirements-leap.pip"])) + setup( name='leap.keymanager', version=VERSION, @@ -129,7 +133,7 @@ setup( packages=find_packages('src', exclude=['leap.keymanager.tests']), package_dir={'': 'src'}, test_suite='leap.keymanager.tests', - install_requires=utils.parse_requirements(), + install_requires=requirements, tests_require=utils.parse_requirements( reqfiles=['pkg/requirements-testing.pip']), ) -- cgit v1.2.3 From 305d1b4eb65d664eabceadf2201241526172c3cb Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 22 Jul 2015 10:34:21 -0400 Subject: [style] add the magnificient leap commit template --- keymanager/docs/leap-commit-template | 7 +++++ keymanager/docs/leap-commit-template.README | 47 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 keymanager/docs/leap-commit-template create mode 100644 keymanager/docs/leap-commit-template.README diff --git a/keymanager/docs/leap-commit-template b/keymanager/docs/leap-commit-template new file mode 100644 index 0000000..8a5c7cd --- /dev/null +++ b/keymanager/docs/leap-commit-template @@ -0,0 +1,7 @@ +[bug|feat|docs|style|refactor|test|pkg|i18n] ... +... + +- Resolves: #XYZ +- Related: #XYZ +- Documentation: #XYZ +- Releases: XYZ diff --git a/keymanager/docs/leap-commit-template.README b/keymanager/docs/leap-commit-template.README new file mode 100644 index 0000000..ce8809e --- /dev/null +++ b/keymanager/docs/leap-commit-template.README @@ -0,0 +1,47 @@ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +HOW TO USE THIS TEMPLATE: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run `git config commit.template docs/leap-commit-template` or +edit the .git/config for this project and add +`template = docs/leap-commit-template` +under the [commit] block + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +COMMIT TEMPLATE FORMAT EXPLAINED +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +[type] + + +