From: Sergey Matveev Date: Sun, 1 Oct 2017 18:12:02 +0000 (+0300) Subject: Initial revision X-Git-Tag: 1.0~5 X-Git-Url: http://www.git.cypherpunks.ru/?p=pyderasn.git;a=commitdiff_plain;h=664822cb0fc85360a5f77d93ed54714c42557c74;ds=sidebyside Initial revision --- 664822cb0fc85360a5f77d93ed54714c42557c74 diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..a119c02 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +[run] +branch = True + +[report] +show_missing = True +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + if __name__ == .__main__.: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3155f36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.pyc +.coverage +.coverage.* +.hypothesis +__pycache__ +dist diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..f047789 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +* Sergey Matveev diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..9a2708d --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + 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 +. diff --git a/COPYING.LESSER b/COPYING.LESSER new file mode 100644 index 0000000..fc8a5de --- /dev/null +++ b/COPYING.LESSER @@ -0,0 +1,165 @@ + GNU LESSER 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. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser 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 +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/INSTALL b/INSTALL new file mode 120000 index 0000000..4846ad4 --- /dev/null +++ b/INSTALL @@ -0,0 +1 @@ +doc/install.rst \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2c591b2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,12 @@ +include .coveragerc +include AUTHORS +include COPYING* +include INSTALL +include NEWS +include nose.cfg +include pip-requirements* +include PUBKEY.asc +include pyderasn.pyi +include README +include THANKS +include VERSION diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/NEWS @@ -0,0 +1 @@ +TODO diff --git a/PUBKEY.asc b/PUBKEY.asc new file mode 100644 index 0000000..efe5113 --- /dev/null +++ b/PUBKEY.asc @@ -0,0 +1,21 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFnCv/YBCAC8TD+EhE5qDTwHj25OKer4baCpvIMgGYprAmwiUkVq0Wq38on/ +xszHcKcLShApVQXfx32bx+laXdokv1BAPJygrbAo3ocwuo/tBDNlqQ1Tm9vdAhz9 +8S6B7UTLsyO4WkhC9h6wHPMEG4VuSxyHwF+lg5wcbDIHZRn029UctYSjdBXYL/YL +kPfywzLW6sB7FdDWv2Eb7SosTGz9T4kgco3a8cqUbtroawRmXw+AdyPDsxKf2fMP +xpBHhPQvWCnd294keJJ7EiqM11u9f0yRfSotVJU6UHETTgtdPwtP19RLfEqFUWUc +ZYTq2da1cAzBuseczZXbLk01njg0io2YPB0BABEBAAG0K1B5REVSQVNOIHJlbGVh +c2VzIDxweWRlcmFzbkBjeXBoZXJwdW5rcy5ydT6JAVcEEwEIAEEWIQQu1shGMFEC +31tOA4MEqTPRuiAyegUCWcK/9gIbAwwLCgkNCAwHCwMEAQIHFQoJCAsDAgUWAgED +AAIeAQIXgAAKCRAEqTPRuiAyelKFCACAM/hmkkjGDcZn9zRma717CrRr84LrbdOF +EfS+cWwcLpqeI3YmGPBW6hP94MnZuVcUJIVIhZ1C1/DHP32u3xDW0uj2VXwBLCQt +k9regkGOYVMW0l+MKY4Z81KgJSfX+kOq8RfLW0sq1bf91a/id8u/IEsyPHN34XLI +kptAFf6b0Wl6VU7nJiie25XI8DaYX98q7tYoD5yOSxzcCJ4IRAbAKg1B5RNTTZuG +y8RmHsszF3sJ5wLuGk1vpSh1jgq61RUquQYJa1iE2B8fxpL6Qr+T8IR2Jan4TFIn +vzGeBXtCD2yUIeJgSeF/3VoEq8lxJ+rwHwcsIqHF7QdqJCc7S0wviHUEEBEIAB0W +IQTPYOiaWSMeduJjZCKuGoEJ5JhX7wUCWcLAIAAKCRCuGoEJ5JhX7+lbAP9+WNA4 +Uk0pNH5BAASabuT+zllnHZ5SqZoKWbs7bzWfogD+NWmjTfSJCr7GSZ4Suy3Vw4nn +hUu3L6dceWUU+hAEOBw= +=Qodb +-----END PGP PUBLIC KEY BLOCK----- diff --git a/README b/README new file mode 100644 index 0000000..9bc2bf4 --- /dev/null +++ b/README @@ -0,0 +1,24 @@ +PyDERASN -- ASN.1 DER library for Python + +* Basic ASN.1 data types (X.208): BOOLEAN, INTEGER, BIT STRING, OCTET + STRING, NULL, OBJECT IDENTIFIER, ENUMERATED, all strings, UTCTime, + GeneralizedTime, CHOICE, ANY, SEQUENCE (OF), SET (OF) +* Size constraints checking +* Working with sequences as high level data objects with ability to + (un)marshall them +* Python 2.7/3.5 compatibility +* __slots__ friendliness +* Ability to know exact decoded objects offset and lengths in the binary +* Pretty printer and command-line decoder, that could conveniently + replace utilities like either dumpasn1 or openssl asn1parse + +pyderasn is free software: see the file COPYING.LESSER for copying conditions. + +PyDERASN home page is: http://pyderasn.cypherpunks.ru/ + +Please send questions, bug reports and patches to +https://lists.cypherpunks.ru/mailman/listinfo/pyderasn-devel +mailing list. Announcements also go to this mailing list. + +Development Git source code repository currently is located here: +https://git.cypherpunks.ru/cgit.cgi/pyderasn.git/ diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..d3827e7 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0 diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..e35d885 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +_build diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..2edea2b --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,2 @@ +html: + python -msphinx . _build/html diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..fc5892e --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,16 @@ +extensions = ["sphinx.ext.autodoc"] +templates_path = ["_templates"] +source_suffix = ".rst" +master_doc = "index" +project = "pyderasn" +copyright = "2017, Sergey Matveev" +author = "Sergey Matveev" +version = "1.0" +release = "1.0" +language = None +exclude_patterns = ["_build"] +pygments_style = "sphinx" +todo_include_todos = False +html_theme = "classic" +html_static_path = ["_static"] +html_sidebars = {} diff --git a/doc/download.rst b/doc/download.rst new file mode 100644 index 0000000..d4e91de --- /dev/null +++ b/doc/download.rst @@ -0,0 +1,41 @@ +.. _download: + +Download +======== + +.. list-table:: + :widths: 10 10 20 60 + :header-rows: 1 + + * - Package + - Size + - Tarball + - SHA256 checksum + * - ``attrs`` 17.2.0 + - 59 KiB + - `link `__ + `sign `__ + - ``612F3F53 90F2D0C7 FCA6A32A B5B1E750 5BC56C00 1D68B28F 56B7446D 6970DC0A`` + * - ``coverage`` 4.4.1 + - 287 KiB + - `link `__ + `sign `__ + - ``DF312773 C59A0CB5 1EB793F5 BA14A1D5 54D467D6 C46375F1 8E066DAA B8A86271`` + * - ``enum34`` 1.1.6 + - 31 KiB + - `link `__ + `sign `__ + - ``CC26B270 E58910E6 B54ACEE9 EC36C388 4C9BE18B 7A55FA46 305D4BA9 18D00177`` + * - ``hypothesis`` 3.30.4 + - 102 KiB + - `link `__ + `sign `__ + - ``A6281672 88FDCC15 EA806C45 9EBEF827 8D2A8BAD 01DB7C61 BD45D14A 905F53D6`` + * - ``six`` 1.11.0 + - 25 KiB + - `link `__ + `sign `__ + - ``890AC076 5EF9AEFA 5079CEBA ADE9C680 DBFB0E84 E7CFA1F9 9B9B43A8 5FA80126`` + +Development Git source code repository is located here: +https://git.cypherpunks.ru/cgit.cgi/pyderasn.git/. diff --git a/doc/examples.rst b/doc/examples.rst new file mode 100644 index 0000000..16a1d54 --- /dev/null +++ b/doc/examples.rst @@ -0,0 +1,417 @@ +Examples +======== + +.. contents:: + +Schema definition +----------------- + +Let's try to parse X.509 certificate. We have to define our structures +based on ASN.1 schema descriptions. + +.. list-table:: + :header-rows: 1 + + * - ASN.1 specification + - pyderasn's code + * - :: + + Certificate ::= SEQUENCE { + tbsCertificate TBSCertificate, + signatureAlgorithm AlgorithmIdentifier, + signatureValue BIT STRING } + - :: + + class Certificate(Sequence): + schema = ( + ("tbsCertificate", TBSCertificate()), + ("signatureAlgorithm", AlgorithmIdentifier()), + ("signatureValue", BitString()), + ) + * - :: + + AlgorithmIdentifier ::= SEQUENCE { + algorithm OBJECT IDENTIFIER, + parameters ANY DEFINED BY algorithm OPTIONAL } + - :: + + class AlgorithmIdentifier(Sequence): + schema = ( + ("algorithm", ObjectIdentifier()), + ("parameters", Any(optional=True)), + ) + * - :: + + TBSCertificate ::= SEQUENCE { + version [0] EXPLICIT Version DEFAULT v1, + serialNumber CertificateSerialNumber, + signature AlgorithmIdentifier, + issuer Name, + validity Validity, + subject Name, + subjectPublicKeyInfo SubjectPublicKeyInfo, + issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, + subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, + extensions [3] EXPLICIT Extensions OPTIONAL } + - :: + + class TBSCertificate(Sequence): + schema = ( + ("version", Version(expl=tag_ctxc(0), default="v1")), + ("serialNumber", CertificateSerialNumber()), + ("signature", AlgorithmIdentifier()), + ("issuer", Name()), + ("validity", Validity()), + ("subject", Name()), + ("subjectPublicKeyInfo", SubjectPublicKeyInfo()), + ("issuerUniqueID", UniqueIdentifier(impl=tag_ctxp(1), optional=True)), + ("subjectUniqueID", UniqueIdentifier(impl=tag_ctxp(2), optional=True)), + ("extensions", Extensions(expl=tag_ctxc(3), optional=True)), + ) + * - :: + + Version ::= INTEGER { v1(0), v2(1), v3(2) } + - :: + + class Version(Integer): + schema = (("v1", 0), ("v2", 1), ("v3", 2)) + * - :: + + CertificateSerialNumber ::= INTEGER + - :: + + class CertificateSerialNumber(Integer): + pass + * - :: + + Validity ::= SEQUENCE { + notBefore Time, + notAfter Time } + Time ::= CHOICE { + utcTime UTCTime, + generalTime GeneralizedTime } + - :: + + class Validity(Sequence): + schema = ( + ("notBefore", Time()), + ("notAfter", Time()), + ) + class Time(Choice): + schema = ( + ("utcTime", UTCTime()), + ("generalTime", GeneralizedTime()), + ) + * - :: + + SubjectPublicKeyInfo ::= SEQUENCE { + algorithm AlgorithmIdentifier, + subjectPublicKey BIT STRING } + - :: + + class SubjectPublicKeyInfo(Sequence): + schema = ( + ("algorithm", AlgorithmIdentifier()), + ("subjectPublicKey", BitString()), + ) + * - :: + + UniqueIdentifier ::= BIT STRING + - :: + + class UniqueIdentifier(BitString): + pass + * - :: + + Name ::= CHOICE { rdnSequence RDNSequence } + + RDNSequence ::= SEQUENCE OF RelativeDistinguishedName + + RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue + + AttributeTypeAndValue ::= SEQUENCE { type AttributeType, value AttributeValue } + + AttributeType ::= OBJECT IDENTIFIER + + AttributeValue ::= ANY -- DEFINED BY AttributeType + - :: + + class Name(Choice): + schema = (("rdnSequence", RDNSequence()),) + class RDNSequence(SequenceOf): + schema = RelativeDistinguishedName() + class RelativeDistinguishedName(SetOf): + schema = AttributeTypeAndValue() + bounds = (1, float("+inf")) + class AttributeTypeAndValue(Sequence): + schema = ( + ("type", AttributeType()), + ("value", AttributeValue()), + ) + class AttributeType(ObjectIdentifier): + pass + class AttributeValue(Any): + pass + * - :: + + Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension + + Extension ::= SEQUENCE { + extnID OBJECT IDENTIFIER, + critical BOOLEAN DEFAULT FALSE, + extnValue OCTET STRING + } + - :: + + class Extensions(SequenceOf): + schema = Extension() + bounds = (1, float("+inf")) + class Extension(Sequence): + schema = ( + ("extnID", ObjectIdentifier()), + ("critical", Boolean(default=False)), + ("extnValue", OctetString()), + ) + +We are ready to decode PayPal's certificate from Go `encoding/asn1 +`__ test suite (assuming that +it's DER encoded representation is already in ``raw`` variable):: + + >>> crt, tail = Certificate().decode(raw) + >>> crt + Certificate SEQUENCE[TBSCertificate SEQUENCE[[0] EXPLICIT Version + INTEGER v3 OPTIONAL, CertificateSerialNumber INTEGER 61595, + AlgorithmIdentifier SEQUENCE[OBJECT IDENTIFIER 1.2.840.113549.1.1.5... + +Pretty printing +--------------- + +There is huge output. Let's pretty print it:: + + >>> print(pprint(crt)) + 0 [1,3,1604] Certificate SEQUENCE + 4 [1,3,1453] . tbsCertificate: TBSCertificate SEQUENCE + 10-2 [1,1, 1] . . version: [0] EXPLICIT Version INTEGER v3 OPTIONAL + 13 [1,1, 3] . . serialNumber: CertificateSerialNumber INTEGER 61595 + 18 [1,1, 13] . . signature: AlgorithmIdentifier SEQUENCE + 20 [1,1, 9] . . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.5 + 31 [0,0, 2] . . . parameters: [UNIV 5] ANY OPTIONAL + . . . . 05:00 + 33 [0,0, 278] . . issuer: Name CHOICE rdnSequence + 33 [1,3, 274] . . . rdnSequence: RDNSequence SEQUENCE OF + 37 [1,1, 11] . . . . 0: RelativeDistinguishedName SET OF + 39 [1,1, 9] . . . . . 0: AttributeTypeAndValue SEQUENCE + 41 [1,1, 3] . . . . . . type: AttributeType OBJECT IDENTIFIER 2.5.4.6 + 46 [0,0, 4] . . . . . . value: [UNIV 19] AttributeValue ANY + . . . . . . . 13:02:45:53 + [...] + 1461 [1,1, 13] . signatureAlgorithm: AlgorithmIdentifier SEQUENCE + 1463 [1,1, 9] . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.5 + 1474 [0,0, 2] . . parameters: [UNIV 5] ANY OPTIONAL + . . . 05:00 + 1476 [1,2, 129] . signatureValue: BIT STRING 1024 bits + . . 68:EE:79:97:97:DD:3B:EF:16:6A:06:F2:14:9A:6E:CD + . . 9E:12:F7:AA:83:10:BD:D1:7C:98:FA:C7:AE:D4:0E:2C + [...] + + Trailing data: 0a + +Let's parse that output, human:: + + 10-2 [1,1, 1] . . version: [0] EXPLICIT Version INTEGER v3 OPTIONAL + ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ + 0 1 2 3 4 5 6 7 8 9 10 11 + +:: + + 20 [1,1, 9] . . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.5 + ^ ^ ^ ^ ^ ^ ^ ^ + 0 2 3 4 5 6 9 10 + +:: + + 33 [0,0, 278] . . issuer: Name CHOICE rdnSequence + ^ ^ ^ ^ ^ ^ ^ ^ ^ + 0 2 3 4 5 6 8 9 10 + +:0: + Offset of the object, where its DER encoding begins. + Pay attention that it does **not** include explicit tag. +:1: + If explicit tag exists, then this is its length (tag + encoded length). +:2: + Length of object's tag. For example CHOICE does not have its own tag, + so it is zero. +:3: + Length of encoded length. +:4: + Length of encoded value. +:5: + Visual indentation to show the depth of object in the hierarchy. +:6: + Object's name inside SEQUENCE/CHOICE. +:7: + If either IMPLICIT or EXPLICIT tag is set, then it will be shown + here. "IMPLICIT" is omitted. +:8: + Object's class name, if set. Omitted if it is just an ordinary simple + value (like with ``algorithm`` in example above). +:9: + Object's ASN.1 type. +:10: + Object's value, if set. Can consist of multiple words (like OCTET/BIT + STRINGs above). We see ``v3`` value in Version, because it is named. + ``rdnSequence`` is the choice of CHOICE type. +:11: + Possible other flags like OPTIONAL and DEFAULT, if value equals to the + default one, specified in the schema. + +As command line utility +----------------------- + +You can decode DER files using command line abilities and get the same +picture as above by executing:: + + % pyderasn.py --schema tests.test_crts:Certificate path/to/file + +If there is no schema for you file, then you can try parsing it without, +but of course IMPLICIT tags will often make it impossible. But result is +good enough for the certificate above:: + + % pyderasn.py path/to/file + 0 [1,3,1604] . >: SEQUENCE OF + 4 [1,3,1453] . . >: SEQUENCE OF + 8 [0,0, 5] . . . . >: [0] ANY + . . . . . A0:03:02:01:02 + 13 [1,1, 3] . . . . >: INTEGER 61595 + 18 [1,1, 13] . . . . >: SEQUENCE OF + 20 [1,1, 9] . . . . . . >: OBJECT IDENTIFIER 1.2.840.113549.1.1.5 + 31 [1,1, 0] . . . . . . >: NULL + 33 [1,3, 274] . . . . >: SEQUENCE OF + 37 [1,1, 11] . . . . . . >: SET OF + 39 [1,1, 9] . . . . . . . . >: SEQUENCE OF + 41 [1,1, 3] . . . . . . . . . . >: OBJECT IDENTIFIER 2.5.4.6 + 46 [1,1, 2] . . . . . . . . . . >: PrintableString PrintableString ES + [...] + 1409 [1,1, 50] . . . . . . >: SEQUENCE OF + 1411 [1,1, 8] . . . . . . . . >: OBJECT IDENTIFIER 1.3.6.1.5.5.7.1.1 + 1421 [1,1, 38] . . . . . . . . >: OCTET STRING 38 bytes + . . . . . . . . . 30:24:30:22:06:08:2B:06:01:05:05:07:30:01:86:16 + . . . . . . . . . 68:74:74:70:3A:2F:2F:6F:63:73:70:2E:69:70:73:63 + . . . . . . . . . 61:2E:63:6F:6D:2F + 1461 [1,1, 13] . . >: SEQUENCE OF + 1463 [1,1, 9] . . . . >: OBJECT IDENTIFIER 1.2.840.113549.1.1.5 + 1474 [1,1, 0] . . . . >: NULL + 1476 [1,2, 129] . . >: BIT STRING 1024 bits + . . . 68:EE:79:97:97:DD:3B:EF:16:6A:06:F2:14:9A:6E:CD + . . . 9E:12:F7:AA:83:10:BD:D1:7C:98:FA:C7:AE:D4:0E:2C + [...] + +If you have got dictionaries with ObjectIdentifiers, like example one +from ``tests/test_crts.py``:: + + some_oids = { + "1.2.840.113549.1.1.1": "id-rsaEncryption", + "1.2.840.113549.1.1.5": "id-sha1WithRSAEncryption", + [...] + "2.5.4.10": "id-at-organizationName", + "2.5.4.11": "id-at-organizationalUnitName", + } + +then you can pass it to pretty printer to see human readable OIDs:: + + % pyderasn.py --oids tests.test_crts:some_oids path/to/file + [...] + 37 [1,1, 11] . . . . . . >: SET OF + 39 [1,1, 9] . . . . . . . . >: SEQUENCE OF + 41 [1,1, 3] . . . . . . . . . . >: OBJECT IDENTIFIER id-at-countryName (2.5.4.6) + 46 [1,1, 2] . . . . . . . . . . >: PrintableString PrintableString ES + 50 [1,1, 18] . . . . . . >: SET OF + 52 [1,1, 16] . . . . . . . . >: SEQUENCE OF + 54 [1,1, 3] . . . . . . . . . . >: OBJECT IDENTIFIER id-at-stateOrProvinceName (2.5.4.8) + 59 [1,1, 9] . . . . . . . . . . >: PrintableString PrintableString Barcelona + 70 [1,1, 18] . . . . . . >: SET OF + 72 [1,1, 16] . . . . . . . . >: SEQUENCE OF + 74 [1,1, 3] . . . . . . . . . . >: OBJECT IDENTIFIER id-at-localityName (2.5.4.7) + 79 [1,1, 9] . . . . . . . . . . >: PrintableString PrintableString Barcelona + [...] + +Descriptive errors +------------------ + +If you have bad DER, then errors will show you where error occurred:: + + % pyderasn.py --schema tests.test_crts:Certificate path/to/bad/file + Traceback (most recent call last): + [...] + pyderasn.DecodeError: UTCTime (tbsCertificate.validity.notAfter.utcTime) (at 328) invalid UTCTime format + +:: + + % pyderasn.py path/to/bad/file + [...] + pyderasn.DecodeError: UTCTime (0.SequenceOf.4.SequenceOf.1.UTCTime) (at 328) invalid UTCTime format + +You can see, so called, decode path inside the structures: +``tbsCertificate`` -> ``validity`` -> ``notAfter`` -> ``utcTime`` and +that object at byte 328 is invalid. + +X.509 certificate creation +-------------------------- + +Let's create some simple self-signed X.509 certificate from the ground:: + + tbs = TBSCertificate() + tbs["serialNumber"] = CertificateSerialNumber(10143011886257155224) + + sign_algo_id = AlgorithmIdentifier() + sign_algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.5") + sign_algo_id["parameters"] = Any(Null()) + tbs["signature"] = sign_algo_id + + rdnSeq = RDNSequence() + for oid, klass, text in ( + ("2.5.4.6", PrintableString, "XX"), + ("2.5.4.8", PrintableString, "Some-State"), + ("2.5.4.7", PrintableString, "City"), + ("2.5.4.10", PrintableString, "Internet Widgits Pty Ltd"), + ("2.5.4.3", PrintableString, "false.example.com"), + ("1.2.840.113549.1.9.1", IA5String, "false@example.com"), + ): + attr = AttributeTypeAndValue() + attr["type"] = AttributeType(oid) + attr["value"] = AttributeValue(klass(text)) + rdn = RelativeDistinguishedName() + rdn.append(attr) + rdnSeq.append(rdn) + issuer = Name() + issuer["rdnSequence"] = rdnSeq + tbs["issuer"] = issuer + tbs["subject"] = issuer + + validity = Validity() + validity["notBefore"] = Time(("utcTime", UTCTime(datetime(2009, 10, 8, 0, 25, 53)))) + validity["notAfter"] = Time(("utcTime", UTCTime(datetime(2010, 10, 8, 0, 25, 53)))) + tbs["validity"] = validity + + spki = SubjectPublicKeyInfo() + spki_algo_id = sign_algo_id.copy() + spki_algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.1") + spki["algorithm"] = spki_algo_id + spki["subjectPublicKey"] = BitString(hexdec("".join(( + "3048024100cdb7639c3278f006aa277f6eaf42902b592d8cbcbe38a1c92ba4695", + "a331b1deadeadd8e9a5c27e8c4c2fd0a8889657722a4f2af7589cf2c77045dc8f", + "deec357d0203010001", + )))) + tbs["subjectPublicKeyInfo"] = spki + + crt = Certificate() + crt["tbsCertificate"] = tbs + crt["signatureAlgorithm"] = sign_algo_id + crt["signatureValue"] = BitString(hexdec("".join(( + "a67b06ec5ece92772ca413cba3ca12568fdc6c7b4511cd40a7f659980402df2b", + "998bb9a4a8cbeb34c0f0a78cf8d91ede14a5ed76bf116fe360aafa8821490435", + )))) + crt.encode() + +And we will get the same certificate used in Go's library tests. diff --git a/doc/features.rst b/doc/features.rst new file mode 100644 index 0000000..4d94324 --- /dev/null +++ b/doc/features.rst @@ -0,0 +1,50 @@ +Features +======== + +* Basic ASN.1 data types (X.208): BOOLEAN, INTEGER, BIT STRING, OCTET + STRING, NULL, OBJECT IDENTIFIER, ENUMERATED, all strings, UTCTime, + GeneralizedTime, CHOICE, ANY, SEQUENCE (OF), SET (OF) +* Size constraints checking +* Working with sequences as high level data objects with ability to + (un)marshall them +* Python 2.7/3.5 compatibility + +Why yet another library? `pyasn1 `__ +had all of this a long time ago. PyDERASN resembles it in many ways. In +practice it should be relatively easy to convert ``pyasn1``'s code to +``pyderasn``'s one. But additionally it offers: + +* Small, simple and trying to be reviewable code. Just a single file +* ``__slots__`` friendliness +* Ability to know exact decoded objects offsets and lengths in the binary +* Pretty printer and command-line decoder, that could conveniently + replace utilities like either ``dumpasn1`` or ``openssl asn1parse`` +* Some kind of strong typing: SEQUENCEs require the exact **type** of + settable values, even when they are inherited +* However they do not require tags matching: IMPLICIT/EXPLICIT tags will + be set automatically in the given sequence +* Could be significantly faster. For example parsing of CACert.org's CRL + under Python 3.5.2: + + :``pyderasn.py revoke.crl``: + ~2 min + :``pyderasn.py --schema path.to.CertificateList revoke.crl``: + ~38 sec + :``pyasn1.decode(asn1Spec=pyasn1.CertificateList())``: + ~22 min (``pyasn1 == 0.2.3``) + +There are drawbacks: + +* No old Python versions support +* No BER/CER support +* PyDERASN does **not** have object recreation capable ``repr``-s:: + + pyderasn>>> repr(algo_id) + AlgorithmIdentifier SEQUENCE[OBJECT IDENTIFIER 1.3.14.3.2.26, [UNIV 5] ANY 0500 OPTIONAL] + + pyasn1>>> repr(algo_id) + AlgorithmIdentifier().setComponents(ObjectIdentifier('1.3.14.3.2.26'), Any(hexValue='0500')) + +* Strings are not validated in any way, except just trying to be decoded + in ``ascii``, ``iso-8859-1``, ``utf-8/16/32`` correspondingly +* No REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING diff --git a/doc/feedback.rst b/doc/feedback.rst new file mode 100644 index 0000000..d83f51b --- /dev/null +++ b/doc/feedback.rst @@ -0,0 +1,10 @@ +Feedback +======== + +Please send questions regarding the use of PyDERASN, bug reports and +patches to `pyderasn-devel `__ +mailing list. Announcements also go to this mailing list. + +Official website is http://pyderasn.cypherpunks.ru/. +Development Git source code repository is located here: +https://git.cypherpunks.ru/cgit.cgi/pyderasn.git/. diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..edccc23 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,30 @@ +======================================== +PyDERASN -- ASN.1 DER library for Python +======================================== + +.. + + I'm going to build my own ASN.1 library with slots and blobs! + (C) PyDERASN's author + +`ASN.1 `__ (Abstract Syntax +Notation One) is a standard for abstract data serialization. +`DER `__ +(Distinguished Encoding Rules) is a subset of encoding rules suitable +and widely used in cryptography-related stuff. PyDERASN is yet another +library for dealing with the data encoded that way. Although ASN.1 is +written more than 30 years ago by wise Ancients (taken from ``pyasn1``'s +README), it is still often can be seen anywhere in our life. + +PyDERASN is `free software `__, +licenced under `GNU LGPLv3+ `__. + +.. toctree:: + :maxdepth: 1 + + features + examples + reference + install + download + feedback diff --git a/doc/install.rst b/doc/install.rst new file mode 100644 index 0000000..08285de --- /dev/null +++ b/doc/install.rst @@ -0,0 +1,42 @@ +Install +======= + +Preferable way is to :ref:`download ` tarball with the +signature from `official website `__:: + + % wget http://pyderasn.cypherpunks.ru/pyderasn-1.0.tar.xz + % wget http://pyderasn.cypherpunks.ru/pyderasn-1.0.tar.xz.sig + % gpg --verify pyderasn-1.0.tar.xz.sig pyderasn-1.0.tar.xz + % xz -d < pyderasn-1.0.tar.xz | tar xf - + % cd pyderasn-1.0 + % python setup.py install + +PyDERASN depends on `six `__ package +for keeping compatibility with Py27/Py35. If it is not installed on your +system, then ``setup.py install`` will try to download it from PyPI. You +can also find it mirrored on :ref:`download ` page. + +You could use PIP (**no** authentication is performed!):: + + % pip install pyderasn + +You have to verify downloaded tarballs integrity and authenticity to be +sure that you retrieved trusted and untampered software. `GNU Privacy +Guard `__ is used for that purpose. + +For the very first time it is necessary to get signing public key and +import it. It is provided below, but you should check alternative +resources. + +:: + + pub rsa2048/0x04A933D1BA20327A 2017-09-20 + 2ED6 C846 3051 02DF 5B4E 0383 04A9 33D1 BA20 327A + uid PyDERASN releases + + % gpg --keyserver hkp://keys.gnupg.net/ --recv-keys 0x04A933D1BA20327A + % gpg --auto-key-locate dane --locate-keys pyderasn at cypherpunks dot ru + % gpg --auto-key-locate wkd --locate-keys pyderasn at cypherpunks dot ru + % gpg --auto-key-locate pka --locate-keys pyderasn at cypherpunks dot ru + +.. literalinclude:: ../PUBKEY.asc diff --git a/doc/pip-requirements.txt b/doc/pip-requirements.txt new file mode 100644 index 0000000..bc0bbd0 --- /dev/null +++ b/doc/pip-requirements.txt @@ -0,0 +1,16 @@ +alabaster == 0.7.10 +babel == 2.5.1 +certifi == 2017.7.27.1 +chardet == 3.0.4 +docutils == 0.14 +idna == 2.6 +imagesize == 0.7.1 +Jinja2 == 2.9.6 +MarkupSafe == 1.0 +Pygments == 2.2.0 +pytz == 2017.2 +requests == 2.18.4 +snowballstemmer == 1.2.1 +sphinx == 1.6.3 +sphinxcontrib-websupport == 1.0.1 +urllib3 == 1.22 diff --git a/doc/reference.rst b/doc/reference.rst new file mode 100644 index 0000000..e1d5a51 --- /dev/null +++ b/doc/reference.rst @@ -0,0 +1,6 @@ +Library reference +================= + +.. contents:: + +.. automodule:: pyderasn diff --git a/nose.cfg b/nose.cfg new file mode 100644 index 0000000..90a426a --- /dev/null +++ b/nose.cfg @@ -0,0 +1,10 @@ +[nosetests] +tests=tests +verbosity=2 +with-runnable-test-names=1 +nocapture=1 +with-coverage=1 +cover-erase=1 +cover-package=pyderasn +processes=-1 +process-timeout=1800 diff --git a/pip-requirements-tests.txt b/pip-requirements-tests.txt new file mode 100644 index 0000000..625aed7 --- /dev/null +++ b/pip-requirements-tests.txt @@ -0,0 +1,4 @@ +attrs==17.2.0 +coverage==4.4.1 +enum34==1.1.6 ; python_version == '2.7' +hypothesis==3.30.4 diff --git a/pip-requirements.txt b/pip-requirements.txt new file mode 100644 index 0000000..756cde0 --- /dev/null +++ b/pip-requirements.txt @@ -0,0 +1 @@ +six==1.11.0 diff --git a/pyderasn.py b/pyderasn.py new file mode 100755 index 0000000..b1f029c --- /dev/null +++ b/pyderasn.py @@ -0,0 +1,3944 @@ +#!/usr/bin/env python +# coding: utf-8 +# PyDERASN -- Python ASN.1 DER codec with abstract structures +# Copyright (C) 2017 Sergey Matveev +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . +"""Python ASN.1 DER codec with abstract structures + +This library allows you to marshal and unmarshal various structures in +ASN.1 DER format, like this: + + >>> i = Integer(123) + >>> raw = i.encode() + >>> Integer().decode(raw) == i + True + +There are primitive types, holding single values +(:py:class:`pyderasn.BitString`, +:py:class:`pyderasn.Boolean`, +:py:class:`pyderasn.Enumerated`, +:py:class:`pyderasn.GeneralizedTime`, +:py:class:`pyderasn.Integer`, +:py:class:`pyderasn.Null`, +:py:class:`pyderasn.ObjectIdentifier`, +:py:class:`pyderasn.OctetString`, +:py:class:`pyderasn.UTCTime`, +:py:class:`various strings ` +(:py:class:`pyderasn.BMPString`, +:py:class:`pyderasn.GeneralString`, +:py:class:`pyderasn.GraphicString`, +:py:class:`pyderasn.IA5String`, +:py:class:`pyderasn.ISO646String`, +:py:class:`pyderasn.NumericString`, +:py:class:`pyderasn.PrintableString`, +:py:class:`pyderasn.T61String`, +:py:class:`pyderasn.TeletexString`, +:py:class:`pyderasn.UniversalString`, +:py:class:`pyderasn.UTF8String`, +:py:class:`pyderasn.VideotexString`, +:py:class:`pyderasn.VisibleString`)), +constructed types, holding multiple primitive types +(:py:class:`pyderasn.Sequence`, +:py:class:`pyderasn.SequenceOf`, +:py:class:`pyderasn.Set`, +:py:class:`pyderasn.SetOf`), +and special types like +:py:class:`pyderasn.Any` and +:py:class:`pyderasn.Choice`. + +Common for most types +--------------------- + +Tags +____ + +Most types in ASN.1 has specific tag for them. ``Obj.tag_default`` is +the default tag used during coding process. You can override it with +either ``IMPLICIT`` (using ``impl`` keyword argument), or +``EXPLICIT`` one (using ``expl`` keyword argument). Both arguments takes +raw binary string, containing that tag. You can **not** set implicit and +explicit tags simultaneously. + +There are :py:func:`pyderasn.tag_ctxp` and :py:func:`pyderasn.tag_ctxc` +functions, allowing you to easily create ``CONTEXT`` +``PRIMITIVE``/``CONSTRUCTED`` tags, by specifying only the required tag +number. Pay attention that explicit tags always have *constructed* tag +(``tag_ctxc``), but implicit tags for primitive types are primitive +(``tag_ctxp``). + +:: + + >>> Integer(impl=tag_ctxp(1)) + [1] INTEGER + >>> Integer(expl=tag_ctxc(2)) + [2] EXPLICIT INTEGER + +Implicit tag is not explicitly shown. + +Two object of the same type, but with different implicit/explicit tags +are **not** equal. + +You can get objects effective tag (either default or implicited) through +``tag`` property. You can decode it using :py:func:`pyderasn.tag_decode` +function:: + + >>> tag_decode(tag_ctxc(123)) + (128, 32, 123) + >>> klass, form, num = tag_decode(tag_ctxc(123)) + >>> klass == TagClassContext + True + >>> form == TagFormConstructed + True + +To determine if object has explicit tag, use ``expled`` boolean property +and ``expl_tag`` property, returning explicit tag's value. + +Default/optional +________________ + +Many objects in sequences could be ``OPTIONAL`` and could have +``DEFAULT`` value. You can specify that object's property using +corresponding keyword arguments. + + >>> Integer(optional=True, default=123) + INTEGER 123 OPTIONAL DEFAULT + +Those specifications do not play any role in primitive value encoding, +but are taken into account when dealing with sequences holding them. For +example ``TBSCertificate`` sequence holds defaulted, explicitly tagged +``version`` field:: + + class Version(Integer): + schema = ( + ("v1", 0), + ("v2", 1), + ("v3", 2), + ) + class TBSCertificate(Sequence): + schema = ( + ("version", Version(expl=tag_ctxc(0), default="v1")), + [...] + +When default argument is used and value is not specified, then it equals +to default one. + +Size constraints +________________ + +Some objects give ability to set value size constraints. This is either +possible integer value, or allowed length of various strings and +sequences. Constraints are set in the following way:: + + class X(...): + bounds = (MIN, MAX) + +And values satisfaction is checked as: ``MIN <= X <= MAX``. + +For simplicity you can also set bounds the following way:: + + bounded_x = X(bounds=(MIN, MAX)) + +If bounds are not satisfied, then :py:exc:`pyderasn.BoundsError` is +raised. + +Common methods +______________ + +All objects have ``ready`` boolean property, that tells if it is ready +to be encoded. If that kind of action is performed on unready object, +then :py:exc:`pyderasn.ObjNotReady` exception will be raised. + +All objects have ``copy()`` method, returning its copy, that can be safely +mutated. + +Decoding +________ + +Decoding is performed using ``decode()`` method. ``offset`` optional +argument could be used to set initial object's offset in the binary +data, for convenience. It returns decoded object and remaining +unmarshalled data (tail). Internally all work is done on +``memoryview(data)``, and you can leave returning tail as a memoryview, +by specifying ``leavemm=True`` argument. + +When object is decoded, ``decoded`` property is true and you can safely +use following properties: + +* ``offset`` -- position from initial offset where object's tag is started +* ``tlen`` -- length of object's tag +* ``llen`` -- length of object's length value +* ``vlen`` -- length of object's value +* ``tlvlen`` -- length of the whole object + +Pay attention that those values do **not** include anything related to +explicit tag. If you want to know information about it, then use: +``expled`` (to know if explicit tag is set), ``expl_offset`` (it is +lesser than ``offset``), ``expl_tlen``, ``expl_llen``, ``expl_vlen`` +(that actually equals to ordinary ``tlvlen``). + +When error occurs, then :py:exc:`pyderasn.DecodeError` is raised. + +Pretty printing +_______________ + +All objects have ``pps()`` method, that is a generator of +:py:class:`pyderasn.PP` namedtuple, holding various raw information +about the object. If ``pps`` is called on sequences, then all underlying +``PP`` will be yielded. + +You can use :py:func:`pyderasn.pp_console_row` function, converting +those ``PP`` to human readable string. Actually exactly it is used for +all object ``repr``. But it is easy to write custom formatters. + + >>> from pyderasn import pprint + >>> encoded = Integer(-12345).encode() + >>> obj, tail = Integer().decode(encoded) + >>> print(pprint(obj)) + 0 [1,1, 2] INTEGER -12345 + +Primitive types +--------------- + +Boolean +_______ +.. autoclass:: pyderasn.Boolean + :members: __init__ + +Integer +_______ +.. autoclass:: pyderasn.Integer + :members: __init__ + +BitString +_________ +.. autoclass:: pyderasn.BitString + :members: __init__ + +OctetString +___________ +.. autoclass:: pyderasn.OctetString + :members: __init__ + +Null +____ +.. autoclass:: pyderasn.Null + :members: __init__ + +ObjectIdentifier +________________ +.. autoclass:: pyderasn.ObjectIdentifier + :members: __init__ + +Enumerated +__________ +.. autoclass:: pyderasn.Enumerated + +CommonString +____________ +.. autoclass:: pyderasn.CommonString + +UTCTime +_______ +.. autoclass:: pyderasn.UTCTime + :members: __init__, todatetime + +GeneralizedTime +_______________ +.. autoclass:: pyderasn.GeneralizedTime + +Special types +------------- + +Choice +______ +.. autoclass:: pyderasn.Choice + :members: __init__ + +PrimitiveTypes +______________ +.. autoclass:: PrimitiveTypes + +Any +___ +.. autoclass:: pyderasn.Any + :members: __init__ + +Constructed types +----------------- + +Sequence +________ +.. autoclass:: pyderasn.Sequence + :members: __init__ + +Set +___ +.. autoclass:: pyderasn.Set + :members: __init__ + +SequenceOf +__________ +.. autoclass:: pyderasn.SequenceOf + :members: __init__ + +SetOf +_____ +.. autoclass:: pyderasn.SetOf + :members: __init__ +""" + +from codecs import getdecoder +from codecs import getencoder +from collections import namedtuple +from collections import OrderedDict +from datetime import datetime +from math import ceil + +from six import binary_type +from six import byte2int +from six import indexbytes +from six import int2byte +from six import integer_types +from six import iterbytes +from six import PY2 +from six import string_types +from six import text_type +from six.moves import xrange as six_xrange + + +__all__ = ( + "Any", + "BitString", + "BMPString", + "Boolean", + "BoundsError", + "Choice", + "DecodeError", + "Enumerated", + "GeneralizedTime", + "GeneralString", + "GraphicString", + "hexdec", + "hexenc", + "IA5String", + "Integer", + "InvalidLength", + "InvalidOID", + "InvalidValueType", + "ISO646String", + "NotEnoughData", + "Null", + "NumericString", + "obj_by_path", + "ObjectIdentifier", + "ObjNotReady", + "ObjUnknown", + "OctetString", + "PrimitiveTypes", + "PrintableString", + "Sequence", + "SequenceOf", + "Set", + "SetOf", + "T61String", + "tag_ctxc", + "tag_ctxp", + "tag_decode", + "TagClassApplication", + "TagClassContext", + "TagClassPrivate", + "TagClassUniversal", + "TagFormConstructed", + "TagFormPrimitive", + "TagMismatch", + "TeletexString", + "UniversalString", + "UTCTime", + "UTF8String", + "VideotexString", + "VisibleString", +) + +TagClassUniversal = 0 +TagClassApplication = 1 << 6 +TagClassContext = 1 << 7 +TagClassPrivate = 1 << 6 | 1 << 7 +TagFormPrimitive = 0 +TagFormConstructed = 1 << 5 +TagClassReprs = { + TagClassContext: "", + TagClassApplication: "APPLICATION ", + TagClassPrivate: "PRIVATE ", + TagClassUniversal: "UNIV ", +} + + +######################################################################## +# Errors +######################################################################## + +class DecodeError(Exception): + def __init__(self, msg="", klass=None, decode_path=(), offset=0): + super(DecodeError, self).__init__() + self.msg = msg + self.klass = klass + self.decode_path = decode_path + self.offset = offset + + def __str__(self): + return " ".join( + c for c in ( + "" if self.klass is None else self.klass.__name__, + ( + ("(%s)" % ".".join(self.decode_path)) + if len(self.decode_path) > 0 else "" + ), + ("(at %d)" % self.offset) if self.offset > 0 else "", + self.msg, + ) if c != "" + ) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self) + + +class NotEnoughData(DecodeError): + pass + + +class TagMismatch(DecodeError): + pass + + +class InvalidLength(DecodeError): + pass + + +class InvalidOID(DecodeError): + pass + + +class ObjUnknown(ValueError): + def __init__(self, name): + super(ObjUnknown, self).__init__() + self.name = name + + def __str__(self): + return "object is unknown: %s" % self.name + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self) + + +class ObjNotReady(ValueError): + def __init__(self, name): + super(ObjNotReady, self).__init__() + self.name = name + + def __str__(self): + return "object is not ready: %s" % self.name + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self) + + +class InvalidValueType(ValueError): + def __init__(self, expected_types): + super(InvalidValueType, self).__init__() + self.expected_types = expected_types + + def __str__(self): + return "invalid value type, expected: %s" % ", ".join( + [repr(t) for t in self.expected_types] + ) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self) + + +class BoundsError(ValueError): + def __init__(self, bound_min, value, bound_max): + super(BoundsError, self).__init__() + self.bound_min = bound_min + self.value = value + self.bound_max = bound_max + + def __str__(self): + return "unsatisfied bounds: %s <= %s <= %s" % ( + self.bound_min, + self.value, + self.bound_max, + ) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self) + + +######################################################################## +# Basic coders +######################################################################## + +_hexdecoder = getdecoder("hex") +_hexencoder = getencoder("hex") + + +def hexdec(data): + return _hexdecoder(data)[0] + + +def hexenc(data): + return _hexencoder(data)[0].decode("ascii") + + +def int_bytes_len(num, byte_len=8): + if num == 0: + return 1 + return int(ceil(float(num.bit_length()) / byte_len)) + + +def zero_ended_encode(num): + octets = bytearray(int_bytes_len(num, 7)) + i = len(octets) - 1 + octets[i] = num & 0x7F + num >>= 7 + i -= 1 + while num > 0: + octets[i] = 0x80 | (num & 0x7F) + num >>= 7 + i -= 1 + return bytes(octets) + + +def tag_encode(num, klass=TagClassUniversal, form=TagFormPrimitive): + if num < 31: + # [XX|X|.....] + return int2byte(klass | form | num) + # [XX|X|11111][1.......][1.......] ... [0.......] + return int2byte(klass | form | 31) + zero_ended_encode(num) + + +def tag_decode(tag): + """ + assume that data is validated + """ + first_octet = byte2int(tag) + klass = first_octet & 0xC0 + form = first_octet & 0x20 + if first_octet & 0x1F < 0x1F: + return (klass, form, first_octet & 0x1F) + num = 0 + for octet in iterbytes(tag[1:]): + num <<= 7 + num |= octet & 0x7F + return (klass, form, num) + + +def tag_ctxp(num): + return tag_encode(num=num, klass=TagClassContext, form=TagFormPrimitive) + + +def tag_ctxc(num): + return tag_encode(num=num, klass=TagClassContext, form=TagFormConstructed) + + +def tag_strip(data): + """Take off tag from the data + + :returns: (encoded tag, tag length, remaining data) + """ + if len(data) == 0: + raise NotEnoughData("no data at all") + if byte2int(data) & 0x1F < 31: + return data[:1], 1, data[1:] + i = 0 + while True: + i += 1 + if i == len(data): + raise DecodeError("unfinished tag") + if indexbytes(data, i) & 0x80 == 0: + break + i += 1 + return data[:i], i, data[i:] + + +def len_encode(l): + if l < 0x80: + return int2byte(l) + octets = bytearray(int_bytes_len(l) + 1) + octets[0] = 0x80 | (len(octets) - 1) + for i in six_xrange(len(octets) - 1, 0, -1): + octets[i] = l & 0xFF + l >>= 8 + return bytes(octets) + + +def len_decode(data): + if len(data) == 0: + raise NotEnoughData("no data at all") + first_octet = byte2int(data) + if first_octet & 0x80 == 0: + return first_octet, 1, data[1:] + octets_num = first_octet & 0x7F + if octets_num + 1 > len(data): + raise NotEnoughData("encoded length is longer than data") + if octets_num == 0: + raise DecodeError("long form instead of short one") + if byte2int(data[1:]) == 0: + raise DecodeError("leading zeros") + l = 0 + for v in iterbytes(data[1:1 + octets_num]): + l = (l << 8) | v + if l <= 127: + raise DecodeError("long form instead of short one") + return l, 1 + octets_num, data[1 + octets_num:] + + +######################################################################## +# Base class +######################################################################## + +class Obj(object): + """Common ASN.1 object class + """ + __slots__ = ( + "tag", + "_value", + "_expl", + "default", + "optional", + "offset", + "llen", + "vlen", + ) + + def __init__( + self, + impl=None, + expl=None, + default=None, + optional=False, + _decoded=(0, 0, 0), + ): + if impl is None: + self.tag = getattr(self, "impl", self.tag_default) + else: + self.tag = impl + self._expl = getattr(self, "expl", None) if expl is None else expl + if self.tag != self.tag_default and self._expl is not None: + raise ValueError( + "implicit and explicit tags can not be set simultaneously" + ) + if default is not None: + optional = True + self.optional = optional + self.offset, self.llen, self.vlen = _decoded + self.default = None + + @property + def ready(self): # pragma: no cover + raise NotImplementedError() + + def _assert_ready(self): + if not self.ready: + raise ObjNotReady(self.__class__.__name__) + + @property + def decoded(self): + return self.llen > 0 + + def copy(self): # pragma: no cover + raise NotImplementedError() + + @property + def tlen(self): + return len(self.tag) + + @property + def tlvlen(self): + return self.tlen + self.llen + self.vlen + + def __str__(self): # pragma: no cover + return self.__bytes__() if PY2 else self.__unicode__() + + def _encode(self): # pragma: no cover + raise NotImplementedError() + + def _decode(self, tlv, offset=0, decode_path=()): # pragma: no cover + raise NotImplementedError() + + def encode(self): + raw = self._encode() + if self._expl is None: + return raw + return b"".join((self._expl, len_encode(len(raw)), raw)) + + def decode(self, data, offset=0, leavemm=False, decode_path=()): + tlv = memoryview(data) + if self._expl is None: + obj, tail = self._decode( + tlv, + offset, + decode_path=decode_path, + ) + else: + try: + t, tlen, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self._expl: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, llen, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + obj, tail = self._decode( + v, + offset=offset + tlen + llen, + decode_path=(), + ) + return obj, (tail if leavemm else tail.tobytes()) + + @property + def expled(self): + return self._expl is not None + + @property + def expl_tag(self): + return self._expl + + @property + def expl_tlen(self): + return len(self._expl) + + @property + def expl_llen(self): + return len(len_encode(self.tlvlen)) + + @property + def expl_offset(self): + return self.offset - self.expl_tlen - self.expl_llen + + @property + def expl_vlen(self): + return self.tlvlen + + @property + def expl_tlvlen(self): + return self.expl_tlen + self.expl_llen + self.expl_vlen + + +######################################################################## +# Pretty printing +######################################################################## + +PP = namedtuple("PP", ( + "asn1_type_name", + "obj_name", + "decode_path", + "value", + "blob", + "optional", + "default", + "impl", + "expl", + "offset", + "tlen", + "llen", + "vlen", + "expl_offset", + "expl_tlen", + "expl_llen", + "expl_vlen", +)) + + +def _pp( + asn1_type_name="unknown", + obj_name="unknown", + decode_path=(), + value=None, + blob=None, + optional=False, + default=False, + impl=None, + expl=None, + offset=0, + tlen=0, + llen=0, + vlen=0, + expl_offset=None, + expl_tlen=None, + expl_llen=None, + expl_vlen=None, +): + return PP( + asn1_type_name, + obj_name, + decode_path, + value, + blob, + optional, + default, + impl, + expl, + offset, + tlen, + llen, + vlen, + expl_offset, + expl_tlen, + expl_llen, + expl_vlen, + ) + + +def pp_console_row(pp, oids=None, with_offsets=False, with_blob=True): + cols = [] + if with_offsets: + cols.append("%5d%s [%d,%d,%4d]" % ( + pp.offset, + ( + " " if pp.expl_offset is None else + ("-%d" % (pp.offset - pp.expl_offset)) + ), + pp.tlen, + pp.llen, + pp.vlen, + )) + if len(pp.decode_path) > 0: + cols.append(" ." * (len(pp.decode_path))) + cols.append("%s:" % pp.decode_path[-1]) + if pp.expl is not None: + klass, _, num = pp.expl + cols.append("[%s%d] EXPLICIT" % (TagClassReprs[klass], num)) + if pp.impl is not None: + klass, _, num = pp.impl + cols.append("[%s%d]" % (TagClassReprs[klass], num)) + if pp.asn1_type_name.replace(" ", "") != pp.obj_name.upper(): + cols.append(pp.obj_name) + cols.append(pp.asn1_type_name) + if pp.value is not None: + value = pp.value + if ( + oids is not None and + pp.asn1_type_name == ObjectIdentifier.asn1_type_name and + value in oids + ): + value = "%s (%s)" % (oids[value], pp.value) + cols.append(value) + if with_blob: + if isinstance(pp.blob, binary_type): + cols.append(hexenc(pp.blob)) + elif isinstance(pp.blob, tuple): + cols.append(", ".join(pp.blob)) + if pp.optional: + cols.append("OPTIONAL") + if pp.default: + cols.append("DEFAULT") + return " ".join(cols) + + +def pp_console_blob(pp): + cols = [" " * len("XXXXXYY [X,X,XXXX]")] + if len(pp.decode_path) > 0: + cols.append(" ." * (len(pp.decode_path) + 1)) + if isinstance(pp.blob, binary_type): + blob = hexenc(pp.blob).upper() + for i in range(0, len(blob), 32): + chunk = blob[i:i + 32] + yield " ".join(cols + [":".join( + chunk[j:j + 2] for j in range(0, len(chunk), 2) + )]) + elif isinstance(pp.blob, tuple): + yield " ".join(cols + [", ".join(pp.blob)]) + + +def pprint(obj, oids=None, big_blobs=False): + def _pprint_pps(pps): + for pp in pps: + if hasattr(pp, "_fields"): + if big_blobs: + yield pp_console_row( + pp, + oids=oids, + with_offsets=True, + with_blob=False, + ) + for row in pp_console_blob(pp): + yield row + else: + yield pp_console_row(pp, oids=oids, with_offsets=True) + else: + for row in _pprint_pps(pp): + yield row + return "\n".join(_pprint_pps(obj.pps())) + + +######################################################################## +# ASN.1 primitive types +######################################################################## + +class Boolean(Obj): + """``BOOLEAN`` boolean type + + >>> b = Boolean(True) + BOOLEAN True + >>> b == Boolean(True) + True + >>> bool(b) + True + >>> Boolean(optional=True) + BOOLEAN OPTIONAL + >>> Boolean(impl=tag_ctxp(1), default=False) + [1] BOOLEAN False OPTIONAL DEFAULT + """ + __slots__ = () + tag_default = tag_encode(1) + asn1_type_name = "BOOLEAN" + + def __init__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=False, + _decoded=(0, 0, 0), + ): + """ + :param value: set the value. Either boolean type, or + :py:class:`pyderasn.Boolean` object + :param bytes impl: override default tag with ``IMPLICIT`` one + :param bytes expl: override default tag with ``EXPLICIT`` one + :param default: set default value. Type same as in ``value`` + :param bool optional: is object ``OPTIONAL`` in sequence + """ + super(Boolean, self).__init__(impl, expl, default, optional, _decoded) + self._value = None if value is None else self._value_sanitize(value) + if default is not None: + default = self._value_sanitize(default) + self.default = self.__class__( + value=default, + impl=self.tag, + expl=self._expl, + ) + if value is None: + self._value = default + + def _value_sanitize(self, value): + if issubclass(value.__class__, Boolean): + return value._value + if isinstance(value, bool): + return value + raise InvalidValueType((self.__class__, bool)) + + @property + def ready(self): + return self._value is not None + + def copy(self): + obj = self.__class__() + obj._value = self._value + obj.tag = self.tag + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + return obj + + def __nonzero__(self): + self._assert_ready() + return self._value + + def __bool__(self): + self._assert_ready() + return self._value + + def __eq__(self, their): + if isinstance(their, bool): + return self._value == their + if not issubclass(their.__class__, Boolean): + return False + return ( + self._value == their._value and + self.tag == their.tag and + self._expl == their._expl + ) + + def __call__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=None, + ): + return self.__class__( + value=value, + impl=self.tag if impl is None else impl, + expl=self._expl if expl is None else expl, + default=self.default if default is None else default, + optional=self.optional if optional is None else optional, + ) + + def _encode(self): + self._assert_ready() + return b"".join(( + self.tag, + len_encode(1), + (b"\xFF" if self._value else b"\x00"), + )) + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, _, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self.tag: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, _, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l != 1: + raise InvalidLength( + "Boolean's length must be equal to 1", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + first_octet = byte2int(v) + if first_octet == 0: + value = False + elif first_octet == 0xFF: + value = True + else: + raise DecodeError( + "unacceptable Boolean value", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + obj = self.__class__( + value=value, + impl=self.tag, + expl=self._expl, + default=self.default, + optional=self.optional, + _decoded=(offset, 1, 1), + ) + return obj, v[1:] + + def __repr__(self): + return pp_console_row(next(self.pps())) + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + value=str(self._value) if self.ready else None, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + expl_offset=self.expl_offset if self.expled else None, + expl_tlen=self.expl_tlen if self.expled else None, + expl_llen=self.expl_llen if self.expled else None, + expl_vlen=self.expl_vlen if self.expled else None, + ) + + +class Integer(Obj): + """``INTEGER`` integer type + + >>> b = Integer(-123) + INTEGER -123 + >>> b == Integer(-123) + True + >>> int(b) + -123 + >>> Integer(optional=True) + INTEGER OPTIONAL + >>> Integer(impl=tag_ctxp(1), default=123) + [1] INTEGER 123 OPTIONAL DEFAULT + + >>> Integer(2, bounds=(1, 3)) + INTEGER 2 + >>> Integer(5, bounds=(1, 3)) + Traceback (most recent call last): + pyderasn.BoundsError: unsatisfied bounds: 1 <= 5 <= 3 + + :: + + class Version(Integer): + schema = ( + ("v1", 0), + ("v2", 1), + ("v3", 2), + ) + + >>> v = Version("v1") + Version INTEGER v1 + >>> int(v) + 0 + >>> v.named + 'v1' + >>> v.specs + {'v3': 2, 'v1': 0, 'v2': 1} + """ + __slots__ = ("specs", "_bound_min", "_bound_max") + tag_default = tag_encode(2) + asn1_type_name = "INTEGER" + + def __init__( + self, + value=None, + bounds=None, + impl=None, + expl=None, + default=None, + optional=False, + _specs=None, + _decoded=(0, 0, 0), + ): + """ + :param value: set the value. Either integer type, named value + (if ``schema`` is specified in the class), or + :py:class:`pyderasn.Integer` object + :param bounds: set ``(MIN, MAX)`` value constraint. + (-inf, +inf) by default + :param bytes impl: override default tag with ``IMPLICIT`` one + :param bytes expl: override default tag with ``EXPLICIT`` one + :param default: set default value. Type same as in ``value`` + :param bool optional: is object ``OPTIONAL`` in sequence + """ + super(Integer, self).__init__(impl, expl, default, optional, _decoded) + self._value = value + specs = getattr(self, "schema", {}) if _specs is None else _specs + self.specs = specs if isinstance(specs, dict) else dict(specs) + if bounds is None: + self._bound_min, self._bound_max = getattr( + self, + "bounds", + (float("-inf"), float("+inf")), + ) + else: + self._bound_min, self._bound_max = bounds + if value is not None: + self._value = self._value_sanitize(value) + if default is not None: + default = self._value_sanitize(default) + self.default = self.__class__( + value=default, + impl=self.tag, + expl=self._expl, + _specs=self.specs, + ) + if self._value is None: + self._value = default + + def _value_sanitize(self, value): + if issubclass(value.__class__, Integer): + value = value._value + elif isinstance(value, integer_types): + pass + elif isinstance(value, str): + value = self.specs.get(value) + if value is None: + raise ObjUnknown("integer value: %s" % value) + else: + raise InvalidValueType((self.__class__, int, str)) + if not self._bound_min <= value <= self._bound_max: + raise BoundsError(self._bound_min, value, self._bound_max) + return value + + @property + def ready(self): + return self._value is not None + + def copy(self): + obj = self.__class__(_specs=self.specs) + obj._value = self._value + obj._bound_min = self._bound_min + obj._bound_max = self._bound_max + obj.tag = self.tag + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + return obj + + def __int__(self): + self._assert_ready() + return int(self._value) + + def __hash__(self): + self._assert_ready() + return hash( + self.tag + + bytes(self._expl or b"") + + str(self._value).encode("ascii"), + ) + + def __eq__(self, their): + if isinstance(their, integer_types): + return self._value == their + if not issubclass(their.__class__, Integer): + return False + return ( + self._value == their._value and + self.tag == their.tag and + self._expl == their._expl + ) + + def __lt__(self, their): + return self._value < their + + def __gt__(self, their): + return self._value > their + + @property + def named(self): + for name, value in self.specs.items(): + if value == self._value: + return name + + def __call__( + self, + value=None, + bounds=None, + impl=None, + expl=None, + default=None, + optional=None, + ): + return self.__class__( + value=value, + bounds=( + (self._bound_min, self._bound_max) + if bounds is None else bounds + ), + impl=self.tag if impl is None else impl, + expl=self._expl if expl is None else expl, + default=self.default if default is None else default, + optional=self.optional if optional is None else optional, + _specs=self.specs, + ) + + def _encode(self): + self._assert_ready() + value = self._value + if PY2: + if value == 0: + octets = bytearray([0]) + elif value < 0: + value = -value + value -= 1 + octets = bytearray() + while value > 0: + octets.append((value & 0xFF) ^ 0xFF) + value >>= 8 + if len(octets) == 0 or octets[-1] & 0x80 == 0: + octets.append(0xFF) + else: + octets = bytearray() + while value > 0: + octets.append(value & 0xFF) + value >>= 8 + if octets[-1] & 0x80 > 0: + octets.append(0x00) + octets.reverse() + octets = bytes(octets) + else: + bytes_len = ceil(value.bit_length() / 8) or 1 + while True: + try: + octets = value.to_bytes( + bytes_len, + byteorder="big", + signed=True, + ) + except OverflowError: + bytes_len += 1 + else: + break + return b"".join((self.tag, len_encode(len(octets)), octets)) + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, _, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self.tag: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, llen, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l == 0: + raise NotEnoughData( + "zero length", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + v, tail = v[:l], v[l:] + first_octet = byte2int(v) + if l > 1: + second_octet = byte2int(v[1:]) + if ( + ((first_octet == 0x00) and (second_octet & 0x80 == 0)) or + ((first_octet == 0xFF) and (second_octet & 0x80 != 0)) + ): + raise DecodeError( + "non normalized integer", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if PY2: + value = 0 + if first_octet & 0x80 > 0: + octets = bytearray() + for octet in bytearray(v): + octets.append(octet ^ 0xFF) + for octet in octets: + value = (value << 8) | octet + value += 1 + value = -value + else: + for octet in bytearray(v): + value = (value << 8) | octet + else: + value = int.from_bytes(v, byteorder="big", signed=True) + try: + obj = self.__class__( + value=value, + bounds=(self._bound_min, self._bound_max), + impl=self.tag, + expl=self._expl, + default=self.default, + optional=self.optional, + _specs=self.specs, + _decoded=(offset, llen, l), + ) + except BoundsError as err: + raise DecodeError( + msg=str(err), + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + return obj, tail + + def __repr__(self): + return pp_console_row(next(self.pps())) + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + value=(self.named or str(self._value)) if self.ready else None, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + expl_offset=self.expl_offset if self.expled else None, + expl_tlen=self.expl_tlen if self.expled else None, + expl_llen=self.expl_llen if self.expled else None, + expl_vlen=self.expl_vlen if self.expled else None, + ) + + +class BitString(Obj): + """``BIT STRING`` bit string type + + >>> BitString(b"hello world") + BIT STRING 88 bits 68656c6c6f20776f726c64 + >>> bytes(b) + b'hello world' + >>> b == b"hello world" + True + >>> b.bit_len + 88 + + >>> b = BitString("'010110000000'B") + BIT STRING 12 bits 5800 + >>> b.bit_len + 12 + >>> b[0], b[1], b[2], b[3] + (False, True, False, True) + >>> b[1000] + False + >>> [v for v in b] + [False, True, False, True, True, False, False, False, False, False, False, False] + + :: + + class KeyUsage(BitString): + schema = ( + ('digitalSignature', 0), + ('nonRepudiation', 1), + ('keyEncipherment', 2), + ) + + >>> b = KeyUsage(('keyEncipherment', 'nonRepudiation')) + KeyUsage BIT STRING 3 bits nonRepudiation, keyEncipherment + >>> b.named + ['nonRepudiation', 'keyEncipherment'] + >>> b.specs + {'nonRepudiation': 1, 'digitalSignature': 0, 'keyEncipherment': 2} + """ + __slots__ = ("specs",) + tag_default = tag_encode(3) + asn1_type_name = "BIT STRING" + + def __init__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=False, + _specs=None, + _decoded=(0, 0, 0), + ): + """ + :param value: set the value. Either binary type, tuple of named + values (if ``schema`` is specified in the class), + string in ``'XXX...'B`` form, or + :py:class:`pyderasn.BitString` object + :param bytes impl: override default tag with ``IMPLICIT`` one + :param bytes expl: override default tag with ``EXPLICIT`` one + :param default: set default value. Type same as in ``value`` + :param bool optional: is object ``OPTIONAL`` in sequence + """ + super(BitString, self).__init__(impl, expl, default, optional, _decoded) + specs = getattr(self, "schema", {}) if _specs is None else _specs + self.specs = specs if isinstance(specs, dict) else dict(specs) + self._value = None if value is None else self._value_sanitize(value) + if default is not None: + default = self._value_sanitize(default) + self.default = self.__class__( + value=default, + impl=self.tag, + expl=self._expl, + ) + if value is None: + self._value = default + + def _bits2octets(self, bits): + if len(self.specs) > 0: + bits = bits.rstrip("0") + bit_len = len(bits) + bits += "0" * ((8 - (bit_len % 8)) % 8) + octets = bytearray(len(bits) // 8) + for i in six_xrange(len(octets)): + octets[i] = int(bits[i * 8:(i * 8) + 8], 2) + return bit_len, bytes(octets) + + def _value_sanitize(self, value): + if issubclass(value.__class__, BitString): + return value._value + if isinstance(value, (string_types, binary_type)): + if ( + isinstance(value, string_types) and + value.startswith("'") and + value.endswith("'B") + ): + value = value[1:-2] + if not set(value) <= set(("0", "1")): + raise ValueError("B's coding contains unacceptable chars") + return self._bits2octets(value) + elif isinstance(value, binary_type): + return (len(value) * 8, value) + else: + raise InvalidValueType(( + self.__class__, + string_types, + binary_type, + )) + if isinstance(value, tuple): + if ( + len(value) == 2 and + isinstance(value[0], integer_types) and + isinstance(value[1], binary_type) + ): + return value + bits = [] + for name in value: + bit = self.specs.get(name) + if bit is None: + raise ObjUnknown("BitString value: %s" % name) + bits.append(bit) + if len(bits) == 0: + return self._bits2octets("") + bits = set(bits) + return self._bits2octets("".join( + ("1" if bit in bits else "0") + for bit in six_xrange(max(bits) + 1) + )) + raise InvalidValueType((self.__class__, binary_type, string_types)) + + @property + def ready(self): + return self._value is not None + + def copy(self): + obj = self.__class__(_specs=self.specs) + obj._value = self._value + obj.tag = self.tag + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + return obj + + def __iter__(self): + self._assert_ready() + for i in six_xrange(self._value[0]): + yield self[i] + + @property + def bit_len(self): + self._assert_ready() + return self._value[0] + + def __bytes__(self): + self._assert_ready() + return self._value[1] + + def __eq__(self, their): + if isinstance(their, bytes): + return self._value[1] == their + if not issubclass(their.__class__, BitString): + return False + return ( + self._value == their._value and + self.tag == their.tag and + self._expl == their._expl + ) + + @property + def named(self): + return [name for name, bit in self.specs.items() if self[bit]] + + def __call__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=None, + ): + return self.__class__( + value=value, + impl=self.tag if impl is None else impl, + expl=self._expl if expl is None else expl, + default=self.default if default is None else default, + optional=self.optional if optional is None else optional, + _specs=self.specs, + ) + + def __getitem__(self, key): + if isinstance(key, int): + bit_len, octets = self._value + if key >= bit_len: + return False + return ( + byte2int(memoryview(octets)[key // 8:]) >> + (7 - (key % 8)) + ) & 1 == 1 + if isinstance(key, string_types): + value = self.specs.get(key) + if value is None: + raise ObjUnknown("BitString value: %s" % key) + return self[value] + raise InvalidValueType((int, str)) + + def _encode(self): + self._assert_ready() + bit_len, octets = self._value + return b"".join(( + self.tag, + len_encode(len(octets) + 1), + int2byte((8 - bit_len % 8) % 8), + octets, + )) + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, _, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self.tag: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, llen, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l == 0: + raise NotEnoughData( + "zero length", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + pad_size = byte2int(v) + if l == 1 and pad_size != 0: + raise DecodeError( + "invalid empty value", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if pad_size > 7: + raise DecodeError( + "too big pad", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if byte2int(v[-1:]) & ((1 << pad_size) - 1) != 0: + raise DecodeError( + "invalid pad", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + v, tail = v[:l], v[l:] + obj = self.__class__( + value=((len(v) - 1) * 8 - pad_size, v[1:].tobytes()), + impl=self.tag, + expl=self._expl, + default=self.default, + optional=self.optional, + _specs=self.specs, + _decoded=(offset, llen, l), + ) + return obj, tail + + def __repr__(self): + return pp_console_row(next(self.pps())) + + def pps(self, decode_path=()): + value = None + blob = None + if self.ready: + bit_len, blob = self._value + value = "%d bits" % bit_len + if len(self.specs) > 0: + blob = tuple(self.named) + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + value=value, + blob=blob, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + expl_offset=self.expl_offset if self.expled else None, + expl_tlen=self.expl_tlen if self.expled else None, + expl_llen=self.expl_llen if self.expled else None, + expl_vlen=self.expl_vlen if self.expled else None, + ) + + +class OctetString(Obj): + """``OCTET STRING`` binary string type + + >>> s = OctetString(b"hello world") + OCTET STRING 11 bytes 68656c6c6f20776f726c64 + >>> s == OctetString(b"hello world") + True + >>> bytes(s) + b'hello world' + + >>> OctetString(b"hello", bounds=(4, 4)) + Traceback (most recent call last): + pyderasn.BoundsError: unsatisfied bounds: 4 <= 5 <= 4 + >>> OctetString(b"hell", bounds=(4, 4)) + OCTET STRING 4 bytes 68656c6c + """ + __slots__ = ("_bound_min", "_bound_max") + tag_default = tag_encode(4) + asn1_type_name = "OCTET STRING" + + def __init__( + self, + value=None, + bounds=None, + impl=None, + expl=None, + default=None, + optional=False, + _decoded=(0, 0, 0), + ): + """ + :param value: set the value. Either binary type, or + :py:class:`pyderasn.OctetString` object + :param bounds: set ``(MIN, MAX)`` value size constraint. + (-inf, +inf) by default + :param bytes impl: override default tag with ``IMPLICIT`` one + :param bytes expl: override default tag with ``EXPLICIT`` one + :param default: set default value. Type same as in ``value`` + :param bool optional: is object ``OPTIONAL`` in sequence + """ + super(OctetString, self).__init__( + impl, + expl, + default, + optional, + _decoded, + ) + self._value = value + if bounds is None: + self._bound_min, self._bound_max = getattr( + self, + "bounds", + (0, float("+inf")), + ) + else: + self._bound_min, self._bound_max = bounds + if value is not None: + self._value = self._value_sanitize(value) + if default is not None: + default = self._value_sanitize(default) + self.default = self.__class__( + value=default, + impl=self.tag, + expl=self._expl, + ) + if self._value is None: + self._value = default + + def _value_sanitize(self, value): + if issubclass(value.__class__, OctetString): + value = value._value + elif isinstance(value, binary_type): + pass + else: + raise InvalidValueType((self.__class__, bytes)) + if not self._bound_min <= len(value) <= self._bound_max: + raise BoundsError(self._bound_min, len(value), self._bound_max) + return value + + @property + def ready(self): + return self._value is not None + + def copy(self): + obj = self.__class__() + obj._value = self._value + obj._bound_min = self._bound_min + obj._bound_max = self._bound_max + obj.tag = self.tag + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + return obj + + def __bytes__(self): + self._assert_ready() + return self._value + + def __eq__(self, their): + if isinstance(their, binary_type): + return self._value == their + if not issubclass(their.__class__, OctetString): + return False + return ( + self._value == their._value and + self.tag == their.tag and + self._expl == their._expl + ) + + def __call__( + self, + value=None, + bounds=None, + impl=None, + expl=None, + default=None, + optional=None, + ): + return self.__class__( + value=value, + bounds=( + (self._bound_min, self._bound_max) + if bounds is None else bounds + ), + impl=self.tag if impl is None else impl, + expl=self._expl if expl is None else expl, + default=self.default if default is None else default, + optional=self.optional if optional is None else optional, + ) + + def _encode(self): + self._assert_ready() + return b"".join(( + self.tag, + len_encode(len(self._value)), + self._value, + )) + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, _, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self.tag: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, llen, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + v, tail = v[:l], v[l:] + try: + obj = self.__class__( + value=v.tobytes(), + bounds=(self._bound_min, self._bound_max), + impl=self.tag, + expl=self._expl, + default=self.default, + optional=self.optional, + _decoded=(offset, llen, l), + ) + except BoundsError as err: + raise DecodeError( + msg=str(err), + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + return obj, tail + + def __repr__(self): + return pp_console_row(next(self.pps())) + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + value=("%d bytes" % len(self._value)) if self.ready else None, + blob=self._value if self.ready else None, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + expl_offset=self.expl_offset if self.expled else None, + expl_tlen=self.expl_tlen if self.expled else None, + expl_llen=self.expl_llen if self.expled else None, + expl_vlen=self.expl_vlen if self.expled else None, + ) + + +class Null(Obj): + """``NULL`` null object + + >>> n = Null() + NULL + >>> n.ready + True + """ + __slots__ = () + tag_default = tag_encode(5) + asn1_type_name = "NULL" + + def __init__( + self, + value=None, # unused, but Sequence passes it + impl=None, + expl=None, + optional=False, + _decoded=(0, 0, 0), + ): + """ + :param bytes impl: override default tag with ``IMPLICIT`` one + :param bytes expl: override default tag with ``EXPLICIT`` one + :param bool optional: is object ``OPTIONAL`` in sequence + """ + super(Null, self).__init__(impl, expl, None, optional, _decoded) + self.default = None + + @property + def ready(self): + return True + + def copy(self): + obj = self.__class__() + obj.tag = self.tag + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + return obj + + def __eq__(self, their): + if not issubclass(their.__class__, Null): + return False + return ( + self.tag == their.tag and + self._expl == their._expl + ) + + def __call__( + self, + value=None, + impl=None, + expl=None, + optional=None, + ): + return self.__class__( + impl=self.tag if impl is None else impl, + expl=self._expl if expl is None else expl, + optional=self.optional if optional is None else optional, + ) + + def _encode(self): + return self.tag + len_encode(0) + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, _, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self.tag: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, _, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l != 0: + raise InvalidLength( + "Null must have zero length", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + obj = self.__class__( + impl=self.tag, + expl=self._expl, + optional=self.optional, + _decoded=(offset, 1, 0), + ) + return obj, v + + def __repr__(self): + return pp_console_row(next(self.pps())) + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + optional=self.optional, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + expl_offset=self.expl_offset if self.expled else None, + expl_tlen=self.expl_tlen if self.expled else None, + expl_llen=self.expl_llen if self.expled else None, + expl_vlen=self.expl_vlen if self.expled else None, + ) + + +class ObjectIdentifier(Obj): + """``OBJECT IDENTIFIER`` OID type + + >>> oid = ObjectIdentifier((1, 2, 3)) + OBJECT IDENTIFIER 1.2.3 + >>> oid == ObjectIdentifier("1.2.3") + True + >>> tuple(oid) + (1, 2, 3) + >>> str(oid) + '1.2.3' + >>> oid + (4, 5) + ObjectIdentifier("1.7") + OBJECT IDENTIFIER 1.2.3.4.5.1.7 + + >>> str(ObjectIdentifier((3, 1))) + Traceback (most recent call last): + pyderasn.InvalidOID: unacceptable first arc value + """ + __slots__ = () + tag_default = tag_encode(6) + asn1_type_name = "OBJECT IDENTIFIER" + + def __init__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=False, + _decoded=(0, 0, 0), + ): + """ + :param value: set the value. Either tuples of integers, + string of "."-concatenated integers, or + :py:class:`pyderasn.ObjectIdentifier` object + :param bytes impl: override default tag with ``IMPLICIT`` one + :param bytes expl: override default tag with ``EXPLICIT`` one + :param default: set default value. Type same as in ``value`` + :param bool optional: is object ``OPTIONAL`` in sequence + """ + super(ObjectIdentifier, self).__init__( + impl, + expl, + default, + optional, + _decoded, + ) + self._value = value + if value is not None: + self._value = self._value_sanitize(value) + if default is not None: + default = self._value_sanitize(default) + self.default = self.__class__( + value=default, + impl=self.tag, + expl=self._expl, + ) + if self._value is None: + self._value = default + + def __add__(self, their): + if isinstance(their, self.__class__): + return self.__class__(self._value + their._value) + if isinstance(their, tuple): + return self.__class__(self._value + their) + raise InvalidValueType((self.__class__, tuple)) + + def _value_sanitize(self, value): + if issubclass(value.__class__, ObjectIdentifier): + return value._value + if isinstance(value, string_types): + try: + value = tuple(int(arc) for arc in value.split(".")) + except ValueError: + raise InvalidOID("unacceptable arcs values") + if isinstance(value, tuple): + if len(value) < 2: + raise InvalidOID("less than 2 arcs") + first_arc = value[0] + if first_arc in (0, 1): + if not (0 <= value[1] <= 39): + raise InvalidOID("second arc is too wide") + elif first_arc == 2: + pass + else: + raise InvalidOID("unacceptable first arc value") + return value + raise InvalidValueType((self.__class__, str, tuple)) + + @property + def ready(self): + return self._value is not None + + def copy(self): + obj = self.__class__() + obj._value = self._value + obj.tag = self.tag + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + return obj + + def __iter__(self): + self._assert_ready() + return iter(self._value) + + def __str__(self): + return ".".join(str(arc) for arc in self._value or ()) + + def __hash__(self): + self._assert_ready() + return hash( + self.tag + + bytes(self._expl or b"") + + str(self._value).encode("ascii"), + ) + + def __eq__(self, their): + if isinstance(their, tuple): + return self._value == their + if not issubclass(their.__class__, ObjectIdentifier): + return False + return ( + self.tag == their.tag and + self._expl == their._expl and + self._value == their._value + ) + + def __lt__(self, their): + return self._value < their + + def __gt__(self, their): + return self._value > their + + def __call__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=None, + ): + return self.__class__( + value=value, + impl=self.tag if impl is None else impl, + expl=self._expl if expl is None else expl, + default=self.default if default is None else default, + optional=self.optional if optional is None else optional, + ) + + def _encode(self): + self._assert_ready() + value = self._value + first_value = value[1] + first_arc = value[0] + if first_arc == 0: + pass + elif first_arc == 1: + first_value += 40 + elif first_arc == 2: + first_value += 80 + else: # pragma: no cover + raise RuntimeError("invalid arc is stored") + octets = [zero_ended_encode(first_value)] + for arc in value[2:]: + octets.append(zero_ended_encode(arc)) + v = b"".join(octets) + return b"".join((self.tag, len_encode(len(v)), v)) + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, _, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self.tag: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, llen, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l == 0: + raise NotEnoughData( + "zero length", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + v, tail = v[:l], v[l:] + arcs = [] + while len(v) > 0: + i = 0 + arc = 0 + while True: + octet = indexbytes(v, i) + arc = (arc << 7) | (octet & 0x7F) + if octet & 0x80 == 0: + arcs.append(arc) + v = v[i + 1:] + break + i += 1 + if i == len(v): + raise DecodeError( + "unfinished OID", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + first_arc = 0 + second_arc = arcs[0] + if 0 <= second_arc <= 39: + first_arc = 0 + elif 40 <= second_arc <= 79: + first_arc = 1 + second_arc -= 40 + else: + first_arc = 2 + second_arc -= 80 + obj = self.__class__( + value=tuple([first_arc, second_arc] + arcs[1:]), + impl=self.tag, + expl=self._expl, + default=self.default, + optional=self.optional, + _decoded=(offset, llen, l), + ) + return obj, tail + + def __repr__(self): + return pp_console_row(next(self.pps())) + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + value=str(self) if self.ready else None, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + expl_offset=self.expl_offset if self.expled else None, + expl_tlen=self.expl_tlen if self.expled else None, + expl_llen=self.expl_llen if self.expled else None, + expl_vlen=self.expl_vlen if self.expled else None, + ) + + +class Enumerated(Integer): + """``ENUMERATED`` integer type + + This type is identical to :py:class:`pyderasn.Integer`, but requires + schema to be specified and does not accept values missing from it. + """ + __slots__ = () + tag_default = tag_encode(10) + asn1_type_name = "ENUMERATED" + + def __init__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=False, + _specs=None, + _decoded=(0, 0, 0), + bounds=None, # dummy argument, workability for Integer.decode + ): + super(Enumerated, self).__init__( + value=value, + impl=impl, + expl=expl, + default=default, + optional=optional, + _specs=_specs, + _decoded=_decoded, + ) + if len(self.specs) == 0: + raise ValueError("schema must be specified") + + def _value_sanitize(self, value): + if isinstance(value, self.__class__): + value = value._value + elif isinstance(value, integer_types): + if value not in list(self.specs.values()): + raise DecodeError( + "unknown integer value: %s" % value, + klass=self.__class__, + ) + elif isinstance(value, string_types): + value = self.specs.get(value) + if value is None: + raise ObjUnknown("integer value: %s" % value) + else: + raise InvalidValueType((self.__class__, int, str)) + return value + + def copy(self): + obj = self.__class__(_specs=self.specs) + obj._value = self._value + obj._bound_min = self._bound_min + obj._bound_max = self._bound_max + obj.tag = self.tag + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + return obj + + def __call__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=None, + _specs=None, + ): + return self.__class__( + value=value, + impl=self.tag if impl is None else impl, + expl=self._expl if expl is None else expl, + default=self.default if default is None else default, + optional=self.optional if optional is None else optional, + _specs=self.specs, + ) + + +class CommonString(OctetString): + """Common class for all strings + + Everything resembles :py:class:`pyderasn.OctetString`, except + ability to deal with unicode text strings. + + >>> hexenc("привет мир".encode("utf-8")) + 'd0bfd180d0b8d0b2d0b5d18220d0bcd0b8d180' + >>> UTF8String("привет мир") == UTF8String(hexdec("d0...80")) + True + >>> s = UTF8String("привет мир") + UTF8String UTF8String привет мир + >>> str(s) + 'привет мир' + >>> hexenc(bytes(s)) + 'd0bfd180d0b8d0b2d0b5d18220d0bcd0b8d180' + + >>> PrintableString("привет мир") + Traceback (most recent call last): + UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128) + + >>> BMPString("ада", bounds=(2, 2)) + Traceback (most recent call last): + pyderasn.BoundsError: unsatisfied bounds: 2 <= 3 <= 2 + >>> s = BMPString("ад", bounds=(2, 2)) + >>> s.encoding + 'utf-16-be' + >>> hexenc(bytes(s)) + '04300434' + + .. list-table:: + :header-rows: 1 + + * - Class + - Text Encoding + * - :py:class:`pyderasn.UTF8String` + - utf-8 + * - :py:class:`pyderasn.NumericString` + - ascii + * - :py:class:`pyderasn.PrintableString` + - ascii + * - :py:class:`pyderasn.TeletexString` + - ascii + * - :py:class:`pyderasn.T61String` + - ascii + * - :py:class:`pyderasn.VideotexString` + - iso-8859-1 + * - :py:class:`pyderasn.IA5String` + - ascii + * - :py:class:`pyderasn.GraphicString` + - iso-8859-1 + * - :py:class:`pyderasn.VisibleString` + - ascii + * - :py:class:`pyderasn.ISO646String` + - ascii + * - :py:class:`pyderasn.GeneralString` + - iso-8859-1 + * - :py:class:`pyderasn.UniversalString` + - utf-32-be + * - :py:class:`pyderasn.BMPString` + - utf-16-be + """ + __slots__ = ("encoding",) + + def _value_sanitize(self, value): + value_raw = None + value_decoded = None + if isinstance(value, self.__class__): + value_raw = value._value + elif isinstance(value, text_type): + value_decoded = value + elif isinstance(value, binary_type): + value_raw = value + else: + raise InvalidValueType((self.__class__, text_type, binary_type)) + value_raw = ( + value_decoded.encode(self.encoding) + if value_raw is None else value_raw + ) + value_decoded = ( + value_raw.decode(self.encoding) + if value_decoded is None else value_decoded + ) + if not self._bound_min <= len(value_decoded) <= self._bound_max: + raise BoundsError( + self._bound_min, + len(value_decoded), + self._bound_max, + ) + return value_raw + + def __eq__(self, their): + if isinstance(their, binary_type): + return self._value == their + if isinstance(their, text_type): + return self._value == their.encode(self.encoding) + if not isinstance(their, self.__class__): + return False + return ( + self._value == their._value and + self.tag == their.tag and + self._expl == their._expl + ) + + def __unicode__(self): + if self.ready: + return self._value.decode(self.encoding) + return text_type(self._value) + + def __repr__(self): + return pp_console_row(next(self.pps(no_unicode=PY2))) + + def pps(self, decode_path=(), no_unicode=False): + value = None + if self.ready: + value = hexenc(bytes(self)) if no_unicode else self.__unicode__() + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + value=value, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + ) + + +class UTF8String(CommonString): + __slots__ = () + tag_default = tag_encode(12) + encoding = "utf-8" + asn1_type_name = "UTF8String" + + +class NumericString(CommonString): + __slots__ = () + tag_default = tag_encode(18) + encoding = "ascii" + asn1_type_name = "NumericString" + + +class PrintableString(CommonString): + __slots__ = () + tag_default = tag_encode(19) + encoding = "ascii" + asn1_type_name = "PrintableString" + + +class TeletexString(CommonString): + __slots__ = () + tag_default = tag_encode(20) + encoding = "ascii" + asn1_type_name = "TeletexString" + + +class T61String(TeletexString): + __slots__ = () + asn1_type_name = "T61String" + + +class VideotexString(CommonString): + __slots__ = () + tag_default = tag_encode(21) + encoding = "iso-8859-1" + asn1_type_name = "VideotexString" + + +class IA5String(CommonString): + __slots__ = () + tag_default = tag_encode(22) + encoding = "ascii" + asn1_type_name = "IA5" + + +class UTCTime(CommonString): + """``UTCTime`` datetime type + + >>> t = UTCTime(datetime(2017, 9, 30, 22, 7, 50, 123)) + UTCTime UTCTime 2017-09-30T22:07:50 + >>> str(t) + '170930220750Z' + >>> bytes(t) + b'170930220750Z' + >>> t.todatetime() + datetime.datetime(2017, 9, 30, 22, 7, 50) + >>> UTCTime(datetime(2057, 9, 30, 22, 7, 50)).todatetime() + datetime.datetime(1957, 9, 30, 22, 7, 50) + """ + __slots__ = () + tag_default = tag_encode(23) + encoding = "ascii" + asn1_type_name = "UTCTime" + + fmt = "%y%m%d%H%M%SZ" + + def __init__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=False, + _decoded=(0, 0, 0), + bounds=None, # dummy argument, workability for OctetString.decode + ): + """ + :param value: set the value. Either datetime type, or + :py:class:`pyderasn.UTCTime` object + :param bytes impl: override default tag with ``IMPLICIT`` one + :param bytes expl: override default tag with ``EXPLICIT`` one + :param default: set default value. Type same as in ``value`` + :param bool optional: is object ``OPTIONAL`` in sequence + """ + super(UTCTime, self).__init__( + impl=impl, + expl=expl, + default=default, + optional=optional, + _decoded=_decoded, + ) + self._value = value + if value is not None: + self._value = self._value_sanitize(value) + if default is not None: + default = self._value_sanitize(default) + self.default = self.__class__( + value=default, + impl=self.tag, + expl=self._expl, + ) + if self._value is None: + self._value = default + + def _value_sanitize(self, value): + if isinstance(value, self.__class__): + return value._value + if isinstance(value, datetime): + return value.strftime(self.fmt).encode("ascii") + if isinstance(value, binary_type): + value_decoded = value.decode("ascii") + if len(value_decoded) == 2 + 2 + 2 + 2 + 2 + 2 + 1: + try: + datetime.strptime(value_decoded, self.fmt) + except ValueError: + raise DecodeError("invalid UTCTime format") + return value + else: + raise DecodeError("invalid UTCTime length") + raise InvalidValueType((self.__class__, datetime)) + + def __eq__(self, their): + if isinstance(their, binary_type): + return self._value == their + if isinstance(their, datetime): + return self.todatetime() == their + if not isinstance(their, self.__class__): + return False + return ( + self._value == their._value and + self.tag == their.tag and + self._expl == their._expl + ) + + def todatetime(self): + """Convert to datetime + + :returns: datetime + + Pay attention that UTCTime can not hold full year, so all years + having < 50 years are treated as 20xx, 19xx otherwise, according + to X.509 recomendation. + """ + value = datetime.strptime(self._value.decode("ascii"), self.fmt) + year = value.year % 100 + return datetime( + year=(2000 + year) if year < 50 else (1900 + year), + month=value.month, + day=value.day, + hour=value.hour, + minute=value.minute, + second=value.second, + ) + + def __repr__(self): + return pp_console_row(next(self.pps())) + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + value=self.todatetime().isoformat() if self.ready else None, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + ) + + +class GeneralizedTime(UTCTime): + """``GeneralizedTime`` datetime type + + This type is similar to :py:class:`pyderasn.UTCTime`. + + >>> t = GeneralizedTime(datetime(2017, 9, 30, 22, 7, 50, 123)) + GeneralizedTime GeneralizedTime 2017-09-30T22:07:50.000123 + >>> str(t) + '20170930220750.000123Z' + >>> t = GeneralizedTime(datetime(2057, 9, 30, 22, 7, 50)) + GeneralizedTime GeneralizedTime 2057-09-30T22:07:50 + """ + __slots__ = () + tag_default = tag_encode(24) + asn1_type_name = "GeneralizedTime" + + fmt = "%Y%m%d%H%M%SZ" + fmt_ms = "%Y%m%d%H%M%S.%fZ" + + def _value_sanitize(self, value): + if isinstance(value, self.__class__): + return value._value + if isinstance(value, datetime): + return value.strftime( + self.fmt_ms if value.microsecond > 0 else self.fmt + ).encode("ascii") + if isinstance(value, binary_type): + value_decoded = value.decode("ascii") + if len(value_decoded) == 4 + 2 + 2 + 2 + 2 + 2 + 1: + try: + datetime.strptime(value_decoded, self.fmt) + except ValueError: + raise DecodeError( + "invalid GeneralizedTime (without ms) format", + ) + return value + elif len(value_decoded) >= 4 + 2 + 2 + 2 + 2 + 2 + 1 + 1 + 1: + try: + datetime.strptime(value_decoded, self.fmt_ms) + except ValueError: + raise DecodeError( + "invalid GeneralizedTime (with ms) format", + ) + return value + else: + raise DecodeError( + "invalid GeneralizedTime length", + klass=self.__class__, + ) + raise InvalidValueType((self.__class__, datetime)) + + def todatetime(self): + value = self._value.decode("ascii") + if len(value) == 4 + 2 + 2 + 2 + 2 + 2 + 1: + return datetime.strptime(value, self.fmt) + return datetime.strptime(value, self.fmt_ms) + + +class GraphicString(CommonString): + __slots__ = () + tag_default = tag_encode(25) + encoding = "iso-8859-1" + asn1_type_name = "GraphicString" + + +class VisibleString(CommonString): + __slots__ = () + tag_default = tag_encode(26) + encoding = "ascii" + asn1_type_name = "VisibleString" + + +class ISO646String(VisibleString): + __slots__ = () + asn1_type_name = "ISO646String" + + +class GeneralString(CommonString): + __slots__ = () + tag_default = tag_encode(27) + encoding = "iso-8859-1" + asn1_type_name = "GeneralString" + + +class UniversalString(CommonString): + __slots__ = () + tag_default = tag_encode(28) + encoding = "utf-32-be" + asn1_type_name = "UniversalString" + + +class BMPString(CommonString): + __slots__ = () + tag_default = tag_encode(30) + encoding = "utf-16-be" + asn1_type_name = "BMPString" + + +class Choice(Obj): + """``CHOICE`` special type + + :: + + class GeneralName(Choice): + schema = ( + ('rfc822Name', IA5String(impl=tag_ctxp(1))), + ('dNSName', IA5String(impl=tag_ctxp(2))), + ) + + >>> gn = GeneralName() + GeneralName CHOICE + >>> gn["rfc822Name"] = IA5String("foo@bar.baz") + GeneralName CHOICE rfc822Name[[1] IA5String IA5 foo@bar.baz] + >>> gn["dNSName"] = IA5String("bar.baz") + GeneralName CHOICE dNSName[[2] IA5String IA5 bar.baz] + >>> gn["rfc822Name"] + None + >>> gn["dNSName"] + [2] IA5String IA5 bar.baz + >>> gn.choice + 'dNSName' + >>> gn.value == gn["dNSName"] + True + >>> gn.specs + OrderedDict([('rfc822Name', [1] IA5String IA5), ('dNSName', [2] IA5String IA5)]) + + >>> GeneralName(("rfc822Name", IA5String("foo@bar.baz"))) + GeneralName CHOICE rfc822Name[[1] IA5String IA5 foo@bar.baz] + """ + __slots__ = ("specs",) + tag_default = None + asn1_type_name = "CHOICE" + + def __init__( + self, + value=None, + schema=None, + impl=None, + expl=None, + default=None, + optional=False, + _decoded=(0, 0, 0), + ): + """ + :param value: set the value. Either ``(choice, value)`` tuple, or + :py:class:`pyderasn.Choice` object + :param bytes impl: can not be set, do **not** use it + :param bytes expl: override default tag with ``EXPLICIT`` one + :param default: set default value. Type same as in ``value`` + :param bool optional: is object ``OPTIONAL`` in sequence + """ + if impl is not None: + raise ValueError("no implicit tag allowed for CHOICE") + super(Choice, self).__init__(None, expl, default, optional, _decoded) + if schema is None: + schema = getattr(self, "schema", ()) + if len(schema) == 0: + raise ValueError("schema must be specified") + self.specs = ( + schema if isinstance(schema, OrderedDict) else OrderedDict(schema) + ) + self._value = None + if value is not None: + self._value = self._value_sanitize(value) + if default is not None: + default_value = self._value_sanitize(default) + default_obj = self.__class__(impl=self.tag, expl=self._expl) + default_obj.specs = self.specs + default_obj._value = default_value + self.default = default_obj + if value is None: + self._value = default_obj.copy()._value + + def _value_sanitize(self, value): + if isinstance(value, self.__class__): + return value._value + if isinstance(value, tuple) and len(value) == 2: + choice, obj = value + spec = self.specs.get(choice) + if spec is None: + raise ObjUnknown(choice) + if not isinstance(obj, spec.__class__): + raise InvalidValueType((spec,)) + return (choice, spec(obj)) + raise InvalidValueType((self.__class__, tuple)) + + @property + def ready(self): + return self._value is not None and self._value[1].ready + + def copy(self): + obj = self.__class__(schema=self.specs) + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + value = self._value + if value is not None: + obj._value = (value[0], value[1].copy()) + return obj + + def __eq__(self, their): + if isinstance(their, tuple) and len(their) == 2: + return self._value == their + if not isinstance(their, self.__class__): + return False + return ( + self.specs == their.specs and + self._value == their._value + ) + + def __call__( + self, + value=None, + expl=None, + default=None, + optional=None, + ): + return self.__class__( + value=value, + schema=self.specs, + expl=self._expl if expl is None else expl, + default=self.default if default is None else default, + optional=self.optional if optional is None else optional, + ) + + @property + def choice(self): + self._assert_ready() + return self._value[0] + + @property + def value(self): + self._assert_ready() + return self._value[1] + + def __getitem__(self, key): + if key not in self.specs: + raise ObjUnknown(key) + if self._value is None: + return None + choice, value = self._value + if choice != key: + return None + return value + + def __setitem__(self, key, value): + spec = self.specs.get(key) + if spec is None: + raise ObjUnknown(key) + if not isinstance(value, spec.__class__): + raise InvalidValueType((spec.__class__,)) + self._value = (key, spec(value)) + + @property + def tlen(self): + return 0 + + @property + def decoded(self): + return self._value[1].decoded if self.ready else False + + def _encode(self): + self._assert_ready() + return self._value[1].encode() + + def _decode(self, tlv, offset=0, decode_path=()): + for choice, spec in self.specs.items(): + try: + value, tail = spec.decode( + tlv, + offset=offset, + leavemm=True, + decode_path=decode_path + (choice,), + ) + except TagMismatch: + continue + obj = self.__class__( + schema=self.specs, + expl=self._expl, + default=self.default, + optional=self.optional, + _decoded=(offset, 0, value.tlvlen), + ) + obj._value = (choice, value) + return obj, tail + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + + def __repr__(self): + value = pp_console_row(next(self.pps())) + if self.ready: + value = "%s[%r]" % (value, self.value) + return value + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + value=self.choice if self.ready else None, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + ) + if self.ready: + yield self.value.pps(decode_path=decode_path + (self.choice,)) + + +class PrimitiveTypes(Choice): + """Predefined ``CHOICE`` for all generic primitive types + + It could be useful for general decoding of some unspecified values: + + >>> PrimitiveTypes().decode(hexdec("0403666f6f"))[0].value + OCTET STRING 3 bytes 666f6f + >>> PrimitiveTypes().decode(hexdec("0203123456"))[0].value + INTEGER 1193046 + """ + __slots__ = () + schema = tuple((klass.__name__, klass()) for klass in ( + Boolean, + Integer, + BitString, + OctetString, + Null, + ObjectIdentifier, + UTF8String, + NumericString, + PrintableString, + TeletexString, + VideotexString, + IA5String, + UTCTime, + GeneralizedTime, + GraphicString, + VisibleString, + ISO646String, + GeneralString, + UniversalString, + BMPString, + )) + + +class Any(Obj): + """``ANY`` special type + + >>> Any(Integer(-123)) + ANY 020185 + >>> a = Any(OctetString(b"hello world").encode()) + ANY 040b68656c6c6f20776f726c64 + >>> hexenc(bytes(a)) + b'0x040x0bhello world' + """ + __slots__ = () + tag_default = tag_encode(0) + asn1_type_name = "ANY" + + def __init__( + self, + value=None, + expl=None, + optional=False, + _decoded=(0, 0, 0), + ): + """ + :param value: set the value. Either any kind of pyderasn's + **ready** object, or bytes. Pay attention that + **no** validation is performed is raw binary value + is valid TLV + :param bytes expl: override default tag with ``EXPLICIT`` one + :param bool optional: is object ``OPTIONAL`` in sequence + """ + super(Any, self).__init__(None, expl, None, optional, _decoded) + self._value = None if value is None else self._value_sanitize(value) + + def _value_sanitize(self, value): + if isinstance(value, self.__class__): + return value._value + if isinstance(value, Obj): + return value.encode() + if isinstance(value, binary_type): + return value + raise InvalidValueType((self.__class__, Obj, binary_type)) + + @property + def ready(self): + return self._value is not None + + def copy(self): + obj = self.__class__() + obj._value = self._value + obj.tag = self.tag + obj._expl = self._expl + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + return obj + + def __eq__(self, their): + if isinstance(their, binary_type): + return self._value == their + if issubclass(their.__class__, Any): + return self._value == their._value + return False + + def __call__( + self, + value=None, + expl=None, + optional=None, + ): + return self.__class__( + value=value, + expl=self._expl if expl is None else expl, + optional=self.optional if optional is None else optional, + ) + + def __bytes__(self): + self._assert_ready() + return self._value + + @property + def tlen(self): + return 0 + + def _encode(self): + self._assert_ready() + return self._value + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, tlen, lv = tag_strip(tlv) + l, llen, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + tlvlen = tlen + llen + l + v, tail = tlv[:tlvlen], v[l:] + obj = self.__class__( + value=v.tobytes(), + expl=self._expl, + optional=self.optional, + _decoded=(offset, 0, tlvlen), + ) + obj.tag = t + return obj, tail + + def __repr__(self): + return pp_console_row(next(self.pps())) + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + blob=self._value if self.ready else None, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + expl_offset=self.expl_offset if self.expled else None, + expl_tlen=self.expl_tlen if self.expled else None, + expl_llen=self.expl_llen if self.expled else None, + expl_vlen=self.expl_vlen if self.expled else None, + ) + + +######################################################################## +# ASN.1 constructed types +######################################################################## + +class Sequence(Obj): + __slots__ = ("specs",) + tag_default = tag_encode(form=TagFormConstructed, num=16) + asn1_type_name = "SEQUENCE" + + def __init__( + self, + value=None, + schema=None, + impl=None, + expl=None, + default=None, + optional=False, + _decoded=(0, 0, 0), + ): + super(Sequence, self).__init__(impl, expl, default, optional, _decoded) + if schema is None: + schema = getattr(self, "schema", ()) + self.specs = ( + schema if isinstance(schema, OrderedDict) else OrderedDict(schema) + ) + self._value = {} + if value is not None: + self._value = self._value_sanitize(value) + if default is not None: + default_value = self._value_sanitize(default) + default_obj = self.__class__(impl=self.tag, expl=self._expl) + default_obj.specs = self.specs + default_obj._value = default_value + self.default = default_obj + if value is None: + self._value = default_obj.copy()._value + + def _value_sanitize(self, value): + if not issubclass(value.__class__, Sequence): + raise InvalidValueType((Sequence,)) + return value._value + + @property + def ready(self): + for name, spec in self.specs.items(): + value = self._value.get(name) + if value is None: + if spec.optional: + continue + return False + else: + if not value.ready: + return False + return True + + def copy(self): + obj = self.__class__(schema=self.specs) + obj.tag = self.tag + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + obj._value = {k: v.copy() for k, v in self._value.items()} + return obj + + def __eq__(self, their): + if not isinstance(their, self.__class__): + return False + return ( + self.specs == their.specs and + self.tag == their.tag and + self._expl == their._expl and + self._value == their._value + ) + + def __call__( + self, + value=None, + impl=None, + expl=None, + default=None, + optional=None, + ): + return self.__class__( + value=value, + schema=self.specs, + impl=self.tag if impl is None else impl, + expl=self._expl if expl is None else expl, + default=self.default if default is None else default, + optional=self.optional if optional is None else optional, + ) + + def __contains__(self, key): + return key in self._value + + def __setitem__(self, key, value): + spec = self.specs.get(key) + if spec is None: + raise ObjUnknown(key) + if value is None: + self._value.pop(key, None) + return + if not isinstance(value, spec.__class__): + raise InvalidValueType((spec.__class__,)) + value = spec(value=value) + if spec.default is not None and value == spec.default: + self._value.pop(key, None) + return + self._value[key] = value + + def __getitem__(self, key): + value = self._value.get(key) + if value is not None: + return value + spec = self.specs.get(key) + if spec is None: + raise ObjUnknown(key) + if spec.default is not None: + return spec.default + return None + + def _encoded_values(self): + raws = [] + for name, spec in self.specs.items(): + value = self._value.get(name) + if value is None: + if spec.optional: + continue + raise ObjNotReady(name) + raws.append(value.encode()) + return raws + + def _encode(self): + v = b"".join(self._encoded_values()) + return b"".join((self.tag, len_encode(len(v)), v)) + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, tlen, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self.tag: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, llen, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + v, tail = v[:l], v[l:] + sub_offset = offset + tlen + llen + values = {} + for name, spec in self.specs.items(): + if len(v) == 0 and spec.optional: + continue + try: + value, v_tail = spec.decode( + v, + sub_offset, + leavemm=True, + decode_path=decode_path + (name,), + ) + except TagMismatch: + if spec.optional: + continue + raise + sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen) + v = v_tail + if spec.default is not None and value == spec.default: + # Encoded default values are not valid in DER, + # but we still allow that + continue + values[name] = value + if len(v) > 0: + raise DecodeError( + "remaining data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + obj = self.__class__( + schema=self.specs, + impl=self.tag, + expl=self._expl, + default=self.default, + optional=self.optional, + _decoded=(offset, llen, l), + ) + obj._value = values + return obj, tail + + def __repr__(self): + value = pp_console_row(next(self.pps())) + cols = [] + for name in self.specs: + _value = self._value.get(name) + if _value is None: + continue + cols.append(repr(_value)) + return "%s[%s]" % (value, ", ".join(cols)) + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + expl_offset=self.expl_offset if self.expled else None, + expl_tlen=self.expl_tlen if self.expled else None, + expl_llen=self.expl_llen if self.expled else None, + expl_vlen=self.expl_vlen if self.expled else None, + ) + for name in self.specs: + value = self._value.get(name) + if value is None: + continue + yield value.pps(decode_path=decode_path + (name,)) + + +class Set(Sequence): + __slots__ = () + tag_default = tag_encode(form=TagFormConstructed, num=17) + asn1_type_name = "SET" + + def _encode(self): + raws = self._encoded_values() + raws.sort() + v = b"".join(raws) + return b"".join((self.tag, len_encode(len(v)), v)) + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, tlen, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self.tag: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, llen, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + offset=offset, + ) + v, tail = v[:l], v[l:] + sub_offset = offset + tlen + llen + values = {} + specs_items = self.specs.items + while len(v) > 0: + for name, spec in specs_items(): + try: + value, v_tail = spec.decode( + v, + sub_offset, + leavemm=True, + decode_path=decode_path + (name,), + ) + except TagMismatch: + continue + sub_offset += ( + value.expl_tlvlen if value.expled else value.tlvlen + ) + v = v_tail + if spec.default is None or value != spec.default: # pragma: no cover + # SeqMixing.test_encoded_default_accepted covers that place + values[name] = value + break + else: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + obj = self.__class__( + schema=self.specs, + impl=self.tag, + expl=self._expl, + default=self.default, + optional=self.optional, + _decoded=(offset, llen, l), + ) + obj._value = values + return obj, tail + + +class SequenceOf(Obj): + __slots__ = ("spec", "_bound_min", "_bound_max") + tag_default = tag_encode(form=TagFormConstructed, num=16) + asn1_type_name = "SEQUENCE OF" + + def __init__( + self, + value=None, + schema=None, + bounds=None, + impl=None, + expl=None, + default=None, + optional=False, + _decoded=(0, 0, 0), + ): + super(SequenceOf, self).__init__( + impl, + expl, + default, + optional, + _decoded, + ) + if schema is None: + schema = getattr(self, "schema", None) + if schema is None: + raise ValueError("schema must be specified") + self.spec = schema + if bounds is None: + self._bound_min, self._bound_max = getattr( + self, + "bounds", + (0, float("+inf")), + ) + else: + self._bound_min, self._bound_max = bounds + self._value = [] + if value is not None: + self._value = self._value_sanitize(value) + if default is not None: + default_value = self._value_sanitize(default) + default_obj = self.__class__( + schema=schema, + impl=self.tag, + expl=self._expl, + ) + default_obj._value = default_value + self.default = default_obj + if value is None: + self._value = default_obj.copy()._value + + def _value_sanitize(self, value): + if issubclass(value.__class__, SequenceOf): + value = value._value + elif hasattr(value, "__iter__"): + value = list(value) + else: + raise InvalidValueType((self.__class__, iter)) + if not self._bound_min <= len(value) <= self._bound_max: + raise BoundsError(self._bound_min, len(value), self._bound_max) + for v in value: + if not isinstance(v, self.spec.__class__): + raise InvalidValueType((self.spec.__class__,)) + return value + + @property + def ready(self): + return all(v.ready for v in self._value) + + def copy(self): + obj = self.__class__(schema=self.spec) + obj._bound_min = self._bound_min + obj._bound_max = self._bound_max + obj.tag = self.tag + obj._expl = self._expl + obj.default = self.default + obj.optional = self.optional + obj.offset = self.offset + obj.llen = self.llen + obj.vlen = self.vlen + obj._value = [v.copy() for v in self._value] + return obj + + def __eq__(self, their): + if isinstance(their, self.__class__): + return ( + self.spec == their.spec and + self.tag == their.tag and + self._expl == their._expl and + self._value == their._value + ) + if hasattr(their, "__iter__"): + return self._value == list(their) + return False + + def __call__( + self, + value=None, + bounds=None, + impl=None, + expl=None, + default=None, + optional=None, + ): + return self.__class__( + value=value, + schema=self.spec, + bounds=( + (self._bound_min, self._bound_max) + if bounds is None else bounds + ), + impl=self.tag if impl is None else impl, + expl=self._expl if expl is None else expl, + default=self.default if default is None else default, + optional=self.optional if optional is None else optional, + ) + + def __contains__(self, key): + return key in self._value + + def append(self, value): + if not isinstance(value, self.spec.__class__): + raise InvalidValueType((self.spec.__class__,)) + if len(self._value) + 1 > self._bound_max: + raise BoundsError( + self._bound_min, + len(self._value) + 1, + self._bound_max, + ) + self._value.append(value) + + def __iter__(self): + self._assert_ready() + return iter(self._value) + + def __len__(self): + self._assert_ready() + return len(self._value) + + def __setitem__(self, key, value): + if not isinstance(value, self.spec.__class__): + raise InvalidValueType((self.spec.__class__,)) + self._value[key] = self.spec(value=value) + + def __getitem__(self, key): + return self._value[key] + + def _encoded_values(self): + return [v.encode() for v in self._value] + + def _encode(self): + v = b"".join(self._encoded_values()) + return b"".join((self.tag, len_encode(len(v)), v)) + + def _decode(self, tlv, offset=0, decode_path=()): + try: + t, tlen, lv = tag_strip(tlv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if t != self.tag: + raise TagMismatch( + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + try: + l, llen, v = len_decode(lv) + except DecodeError as err: + raise err.__class__( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + if l > len(v): + raise NotEnoughData( + "encoded length is longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + v, tail = v[:l], v[l:] + sub_offset = offset + tlen + llen + _value = [] + spec = self.spec + while len(v) > 0: + value, v_tail = spec.decode( + v, + sub_offset, + leavemm=True, + decode_path=decode_path + (str(len(_value)),), + ) + sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen) + v = v_tail + _value.append(value) + obj = self.__class__( + value=_value, + schema=spec, + bounds=(self._bound_min, self._bound_max), + impl=self.tag, + expl=self._expl, + default=self.default, + optional=self.optional, + _decoded=(offset, llen, l), + ) + return obj, tail + + def __repr__(self): + return "%s[%s]" % ( + pp_console_row(next(self.pps())), + ", ".join(repr(v) for v in self._value), + ) + + def pps(self, decode_path=()): + yield _pp( + asn1_type_name=self.asn1_type_name, + obj_name=self.__class__.__name__, + decode_path=decode_path, + optional=self.optional, + default=self == self.default, + impl=None if self.tag == self.tag_default else tag_decode(self.tag), + expl=None if self._expl is None else tag_decode(self._expl), + offset=self.offset, + tlen=self.tlen, + llen=self.llen, + vlen=self.vlen, + expl_offset=self.expl_offset if self.expled else None, + expl_tlen=self.expl_tlen if self.expled else None, + expl_llen=self.expl_llen if self.expled else None, + expl_vlen=self.expl_vlen if self.expled else None, + ) + for i, value in enumerate(self._value): + yield value.pps(decode_path=decode_path + (str(i),)) + + +class SetOf(SequenceOf): + __slots__ = () + tag_default = tag_encode(form=TagFormConstructed, num=17) + asn1_type_name = "SET OF" + + def _encode(self): + raws = self._encoded_values() + raws.sort() + v = b"".join(raws) + return b"".join((self.tag, len_encode(len(v)), v)) + + +def obj_by_path(pypath): # pragma: no cover + """Import object specified as string Python path + + Modules must be separated from classes/functions with ``:``. + + >>> obj_by_path("foo.bar:Baz") + + >>> obj_by_path("foo.bar:Baz.boo") + + """ + mod, objs = pypath.rsplit(":", 1) + from importlib import import_module + obj = import_module(mod) + for obj_name in objs.split("."): + obj = getattr(obj, obj_name) + return obj + + +def main(): # pragma: no cover + import argparse + parser = argparse.ArgumentParser(description="PyDERASN ASN.1 DER decoder") + parser.add_argument( + "--oids", + help="Python path to dictionary with OIDs", + ) + parser.add_argument( + "--schema", + help="Python path to schema definition to use", + ) + parser.add_argument( + "DERFile", + type=argparse.FileType("rb"), + help="Python path to schema definition to use", + ) + args = parser.parse_args() + der = memoryview(args.DERFile.read()) + args.DERFile.close() + oids = obj_by_path(args.oids) if args.oids else {} + if args.schema: + schema = obj_by_path(args.schema) + from functools import partial + pprinter = partial(pprint, big_blobs=True) + else: + # All of this below is a big hack with self references + choice = PrimitiveTypes() + choice.specs["SequenceOf"] = SequenceOf(schema=choice) + choice.specs["SetOf"] = SetOf(schema=choice) + for i in range(31): + choice.specs["SequenceOf%d" % i] = SequenceOf( + schema=choice, + expl=tag_ctxc(i), + ) + choice.specs["Any"] = Any() + + # Class name equals to type name, to omit it from output + class SEQUENCEOF(SequenceOf): + __slots__ = () + schema = choice + schema = SEQUENCEOF() + + def pprint_any(obj, oids=None): + def _pprint_pps(pps): + for pp in pps: + if hasattr(pp, "_fields"): + if pp.asn1_type_name == Choice.asn1_type_name: + continue + pp_kwargs = pp._asdict() + pp_kwargs["decode_path"] = pp.decode_path[:-1] + (">",) + pp = _pp(**pp_kwargs) + yield pp_console_row( + pp, + oids=oids, + with_offsets=True, + with_blob=False, + ) + for row in pp_console_blob(pp): + yield row + else: + for row in _pprint_pps(pp): + yield row + return "\n".join(_pprint_pps(obj.pps())) + pprinter = pprint_any + obj, tail = schema().decode(der) + print(pprinter(obj, oids=oids)) + if tail != b"": + print("\nTrailing data: %s" % hexenc(tail)) + + +if __name__ == "__main__": + main() diff --git a/pyderasn.pyi b/pyderasn.pyi new file mode 100644 index 0000000..83f0032 --- /dev/null +++ b/pyderasn.pyi @@ -0,0 +1,822 @@ +from datetime import datetime +from typing import Any as TAny +from typing import Dict +from typing import NamedTuple +from typing import Optional +from typing import Sequence as TSequence +from typing import Tuple +from typing import Type +from typing import Union + + +TagClassUniversal = ... # type: int +TagClassApplication = ... # type: int +TagClassContext = ... # type: int +TagClassPrivate = ... # type: int +TagFormPrimitive = ... # type: int +TagFormConstructed = ... # type: int +TagClassReprs = ... # type: Dict[int, str] + + +class DecodeError(Exception): + msg = ... # type: str + klass = ... # type: Type + decode_path = ... # type: Tuple[str, ...] + offset = ... # type: int + + def __init__( + self, + msg: str=..., + klass: Optional[TAny]=..., + decode_path: TAny=..., + offset: int=..., + ) -> None: ... + +class NotEnoughData(DecodeError): ... + +class TagMismatch(DecodeError): ... + +class InvalidLength(DecodeError): ... + +class InvalidOID(DecodeError): ... + +class ObjUnknown(ValueError): + name = ... # type: str + + def __init__(self, name: str) -> None: ... + +class ObjNotReady(ValueError): + name = ... # type: str + + def __init__(self, str) -> None: ... + +class InvalidValueType(ValueError): + expected_types = ... # type: Tuple[Type, ...] + + def __init__(self, expected_types: Tuple[Type, ...]) -> None: ... + +class BoundsError(ValueError): + bound_min = ... # type: int + value = ... # type: int + bound_max = ... # type: int + + def __init__(self, bound_min: int, value: int, bound_max: int) -> None: ... + +def hexdec(data: str) -> bytes: ... + +def hexenc(data: bytes) -> str: ... + +def int_bytes_len(num: int, byte_len: int=...) -> int: ... + +def zero_ended_encode(num: int) -> bytes: ... + +def tag_encode(num: int, klass: int=..., form: int=...) -> bytes: ... + +def tag_decode(tag: bytes) -> Tuple[int, int, int]: ... + +def tag_ctxp(num: int) -> bytes: ... + +def tag_ctxc(num: int) -> bytes: ... + +def tag_strip(data: memoryview) -> Tuple[memoryview, int, memoryview]: ... + +def len_encode(l: int) -> bytes: ... + +def len_decode(data: memoryview) -> Tuple[int, int, memoryview]: ... + +class Obj: + tag = ... # type: bytes + optional = ... # type: bool + + def __init__( + self, + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[TAny]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + @property + def decoded(self) -> bool: ... + + def copy(self) -> "Obj": ... + + @property + def tlen(self) -> int: ... + + @property + def tlvlen(self) -> int: ... + + def encode(self) -> bytes: ... + + def decode( + self, + data: bytes, + offset: int=..., + leavemm: bool=..., + decode_path: Tuple[str, ...]=..., + ) -> Tuple[Obj, bytes]: ... + + @property + def expled(self) -> bool: ... + + @property + def expl_tag(self) -> bytes: ... + + @property + def expl_tlen(self) -> int: ... + + @property + def expl_llen(self) -> int: ... + + @property + def expl_offset(self) -> int: ... + + @property + def expl_vlen(self) -> int: ... + + @property + def expl_tlvlen(self) -> int: ... + + +PP = NamedTuple("PP", ( + ("asn1_type_name", str), + ("obj_name", str), + ("decode_path", Tuple[str, ...]), + ("value", Optional[str]), + ("blob", Optional[Union[bytes, Tuple[str, ...]]]), + ("optional", bool), + ("default", bool), + ("impl", Optional[Tuple[int, int, int]]), + ("expl", Optional[Tuple[int, int, int]]), + ("offset", int), + ("tlen", int), + ("llen", int), + ("vlen", int), + ("expl_offset", int), + ("expl_tlen", int), + ("expl_llen", int), + ("expl_vlen", int), +)) + + +def pp_console_row( + pp: PP, + oids: Optional[Dict[str, str]]=..., + with_offsets: bool=..., + with_blob: bool=..., +): ... + +def pp_console_blob(pp: PP) -> TSequence[str]: ... + +def pprint( + obj: Obj, + oids: Optional[Dict[str, str]]=..., + big_blobs: bool=..., +): ... + + +class Boolean(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + default = ... # type: "Boolean" + + def __init__( + self, + value: Optional[Union["Boolean", bool]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["Boolean", bool]]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "Boolean": ... + + def __call__( + self, + value: Optional[Union["Boolean", bool]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["Boolean", bool]]=..., + optional: Optional[bool]=..., + ) -> "Boolean": ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class Integer(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + specs = ... # type: Dict[str, int] + default = ... # type: "Integer" + + def __init__( + self, + value: Optional[Union["Integer", int, str]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["Integer", int, str]]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "Integer": ... + + @property + def named(self) -> Optional[str]: ... + + def __call__( + self, + value: Optional[Union["Integer", int, str]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["Integer", int, str]]=..., + optional: Optional[bool]=..., + ) -> "Integer": ... + + def __int__(self) -> int: ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class BitString(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + specs = ... # type: Dict[str, int] + default = ... # type: "BitString" + + def __init__( + self, + value: Optional[Union["BitString", bytes, Tuple[str, ...]]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["BitString", bytes, Tuple[str, ...]]]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "BitString": ... + + @property + def bit_len(self) -> int: ... + + @property + def named(self) -> TSequence[str]: ... + + def __call__( + self, + value: Optional[Union["BitString", bytes, Tuple[str, ...]]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["BitString", bytes, Tuple[str, ...]]]=..., + optional: Optional[bool]=..., + ) -> "BitString": ... + + def __bytes__(self) -> bytes: ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class OctetString(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + default = ... # type: "OctetString" + + def __init__( + self, + value: Optional[Union["OctetString", bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["OctetString", bytes]]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "OctetString": ... + + def __call__( + self, + value: Optional[Union["OctetString", bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["OctetString", bytes]]=..., + optional: Optional[bool]=..., + ) -> "OctetString": ... + + def __bytes__(self) -> bytes: ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class Null(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + default = ... # type: "Null" + + def __init__( + self, + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "Null": ... + + def __call__( + self, + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + optional: Optional[bool]=..., + ) -> "Null": ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class ObjectIdentifier(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + default = ... # type: "ObjectIdentifier" + + def __init__( + self, + value: Optional[Union["ObjectIdentifier", str, Tuple[int, ...]]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["ObjectIdentifier", str, Tuple[int, ...]]]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "ObjectIdentifier": ... + + def __call__( + self, + value: Optional[Union["ObjectIdentifier", str, Tuple[int, ...]]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["ObjectIdentifier", str, Tuple[int, ...]]]=..., + optional: Optional[bool]=..., + ) -> "ObjectIdentifier": ... + + def __add__( + self, + their: Union["ObjectIdentifier", Tuple[int, ...]], + ) -> "ObjectIdentifier": ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class Enumerated(Integer): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + default = ... # type: "Enumerated" + + def __init__( + self, + value: Optional[Union["Enumerated", str, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["Enumerated", str, int]]=..., + optional: bool=..., + ) -> None: ... + + def copy(self) -> "Enumerated": ... + + def __call__( # type: ignore + self, + value: Optional[Union["Enumerated", str, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["Enumerated", str, int]]=..., + optional: Optional[bool]=..., + ) -> "Enumerated": ... + + +class CommonString(OctetString): + def pps( + self, + decode_path: Tuple[str, ...]=..., + no_unicode: bool=..., + ) -> TSequence[PP]: ... + + +class UTF8String(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "UTF8String" + + def __init__( + self, + value: Optional[Union["UTF8String", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["UTF8String", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + def __str__(self) -> str: ... + + +class NumericString(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "NumericString" + + def __init__( + self, + value: Optional[Union["NumericString", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["NumericString", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class PrintableString(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "PrintableString" + + def __init__( + self, + value: Optional[Union["PrintableString", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["PrintableString", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class TeletexString(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "TeletexString" + + def __init__( + self, + value: Optional[Union["TeletexString", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["TeletexString", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class T61String(TeletexString): + asn1_type_name = ... # type: str + default = ... # type: "T61String" + + +class VideotexString(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "VideotexString" + + def __init__( + self, + value: Optional[Union["VideotexString", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["VideotexString", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class IA5String(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "IA5String" + + def __init__( + self, + value: Optional[Union["IA5String", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["IA5String", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class UTCTime(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "UTCTime" + + def __init__( + self, + value: Optional[Union["UTCTime", datetime]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["UTCTime", datetime]]=..., + optional: bool=..., + ) -> None: ... + + def todatetime(self) -> datetime: ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... # type: ignore + + +class GeneralizedTime(UTCTime): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + default = ... # type: "GeneralizedTime" + + def todatetime(self) -> datetime: ... + + +class GraphicString(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "GraphicString" + + def __init__( + self, + value: Optional[Union["GraphicString", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["GraphicString", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class VisibleString(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "VisibleString" + + def __init__( + self, + value: Optional[Union["VisibleString", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["VisibleString", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class ISO646String(VisibleString): + asn1_type_name = ... # type: str + default = ... # type: "ISO646String" + + +class GeneralString(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "GeneralString" + + def __init__( + self, + value: Optional[Union["GeneralString", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["GeneralString", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class UniversalString(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "UniversalString" + + def __init__( + self, + value: Optional[Union["UniversalString", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["UniversalString", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class BMPString(CommonString): + tag_default = ... # type: bytes + encoding = ... # type: str + asn1_type_name = ... # type: str + default = ... # type: "BMPString" + + def __init__( + self, + value: Optional[Union["BMPString", str, bytes]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["BMPString", str, bytes]]=..., + optional: bool=..., + ) -> None: ... + + +class Choice(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + specs = ... # type: Dict[str, Obj] + default = ... # type: "Choice" + + def __init__( + self, + value: Optional[Union["Choice", Tuple[str, Obj]]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["Choice", Tuple[str, Obj]]]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "Choice": ... + + def __call__( + self, + value: Optional[Union["Choice", Tuple[str, Obj]]]=..., + expl: Optional[bytes]=..., + default: Optional[Union["Choice", Tuple[str, Obj]]]=..., + optional: Optional[bool]=..., + ) -> "Choice": ... + + def __getitem__(self, key: str) -> Optional[Obj]: ... + + def __setitem__(self, key: str, value: Obj) -> None: ... + + @property + def choice(self) -> str: ... + + @property + def value(self) -> Obj: ... + + @property + def tlen(self) -> int: ... + + @property + def decoded(self) -> bool: ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class PrimitiveTypes(Choice): + schema = ... # type: Dict[str, Obj] + + +class Any(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + default = ... # type: "Any" + + def __init__( + self, + value: Optional[Union[Obj, bytes]]=..., + expl: Optional[bytes]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "Any": ... + + def __call__( + self, + value: Optional[Union[Obj, bytes]]=..., + expl: Optional[bytes]=..., + optional: Optional[bool]=..., + ) -> "Any": ... + + def __bytes__(self) -> bytes: ... + + @property + def tlen(self) -> int: ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class Sequence(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + specs = ... # type: Dict[str, Obj] + default = ... # type: "Sequence" + + def __init__( + self, + value: Optional["Sequence"]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional["Sequence"]=..., + optional: bool=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "Sequence": ... + + def __call__( + self, + value: Optional["Sequence"]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional["Sequence"]=..., + optional: Optional[bool]=..., + ) -> "Sequence": ... + + def __getitem__(self, key: str) -> Optional[Obj]: ... + + def __setitem__(self, key: str, value: Obj) -> None: ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class Set(Sequence): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + default = ... # type: "Set" + + +class SequenceOf(Obj): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + spec = ... # type: Obj + default = ... # type: "SequenceOf" + + def __init__( + self, + value: Optional[Union["SequenceOf", TSequence[Obj]]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["SequenceOf", TSequence[Obj]]]=..., + optional: Optional[bool]=..., + ) -> None: ... + + @property + def ready(self) -> bool: ... + + def copy(self) -> "SequenceOf": ... + + def __call__( + self, + value: Optional[Union["SequenceOf", TSequence[Obj]]]=..., + bounds: Optional[Tuple[int, int]]=..., + impl: Optional[bytes]=..., + expl: Optional[bytes]=..., + default: Optional[Union["SequenceOf", TSequence[Obj]]]=..., + optional: Optional[bool]=..., + ) -> "SequenceOf": ... + + def __getitem__(self, key: int) -> Obj: ... + + def __iter__(self) -> TSequence[Obj]: ... + + def append(self, value: Obj) -> None: ... + + def pps(self, decode_path: Tuple[str, ...]=...) -> TSequence[PP]: ... + + +class SetOf(SequenceOf): + tag_default = ... # type: bytes + asn1_type_name = ... # type: str + default = ... # type: "SetOf" + + +def obj_by_path(pypath: str) -> TAny: ... diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..762ee5b --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +from setuptools import setup + + +version = open("VERSION", "rb").read().strip().decode("ascii") + +setup( + name="pyderasn", + version=version, + description="Python ASN.1 DER codec with abstract structures", + long_description=open("README", "rb").read().decode("utf-8"), + author="Sergey Matveev", + author_email="stargrave@stargrave.org", + url="http://pyderasn.cypherpunks.ru/", + license="LGPLv3+", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Intended Audience :: Telecommunications Industry", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Topic :: Communications", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + py_modules=["pyderasn"], + install_requires=["six"], +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_crts.py b/tests/test_crts.py new file mode 100644 index 0000000..93162e0 --- /dev/null +++ b/tests/test_crts.py @@ -0,0 +1,397 @@ +# coding: utf-8 +# PyDERASN -- Python ASN.1 DER codec with abstract structures +# Copyright (C) 2017 Sergey Matveev +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + +from datetime import datetime +from unittest import TestCase + +from pyderasn import Any +from pyderasn import BitString +from pyderasn import Boolean +from pyderasn import Choice +from pyderasn import GeneralizedTime +from pyderasn import hexdec +from pyderasn import IA5String +from pyderasn import Integer +from pyderasn import Null +from pyderasn import ObjectIdentifier +from pyderasn import OctetString +from pyderasn import pprint +from pyderasn import PrintableString +from pyderasn import Sequence +from pyderasn import SequenceOf +from pyderasn import SetOf +from pyderasn import tag_ctxc +from pyderasn import tag_ctxp +from pyderasn import UTCTime + + +some_oids = { + "1.2.840.113549.1.1.1": "id-rsaEncryption", + "1.2.840.113549.1.1.5": "id-sha1WithRSAEncryption", + "1.2.840.113549.1.9.1": "id-emailAddress", + "2.5.29.14": "id-ce-subjectKeyIdentifier", + "2.5.29.15": "id-ce-keyUsage", + "2.5.29.17": "id-ce-subjectAltName", + "2.5.29.18": "id-ce-issuerAltName", + "2.5.29.19": "id-ce-basicConstraints", + "2.5.29.31": "id-ce-cRLDistributionPoints", + "2.5.29.35": "id-ce-authorityKeyIdentifier", + "2.5.29.37": "id-ce-extKeyUsage", + "2.5.4.3": "id-at-commonName", + "2.5.4.6": "id-at-countryName", + "2.5.4.7": "id-at-localityName", + "2.5.4.8": "id-at-stateOrProvinceName", + "2.5.4.10": "id-at-organizationName", + "2.5.4.11": "id-at-organizationalUnitName", +} + + +class Version(Integer): + __slots__ = () + schema = ( + ("v1", 0), + ("v2", 1), + ("v3", 2), + ) + + +class CertificateSerialNumber(Integer): + __slots__ = () + pass + + +class AlgorithmIdentifier(Sequence): + __slots__ = () + schema = ( + ("algorithm", ObjectIdentifier()), + ("parameters", Any(optional=True)), + ) + + +class AttributeType(ObjectIdentifier): + __slots__ = () + pass + + +class AttributeValue(Any): + __slots__ = () + pass + + +class AttributeTypeAndValue(Sequence): + __slots__ = () + schema = ( + ("type", AttributeType()), + ("value", AttributeValue()), + ) + + +class RelativeDistinguishedName(SetOf): + __slots__ = () + schema = AttributeTypeAndValue() + bounds = (1, float("+inf")) + + +class RDNSequence(SequenceOf): + __slots__ = () + schema = RelativeDistinguishedName() + + +class Name(Choice): + __slots__ = () + schema = ( + ("rdnSequence", RDNSequence()), + ) + + +class Time(Choice): + __slots__ = () + schema = ( + ("utcTime", UTCTime()), + ("generalTime", GeneralizedTime()), + ) + + +class Validity(Sequence): + __slots__ = () + schema = ( + ("notBefore", Time()), + ("notAfter", Time()), + ) + + +class SubjectPublicKeyInfo(Sequence): + __slots__ = () + schema = ( + ("algorithm", AlgorithmIdentifier()), + ("subjectPublicKey", BitString()), + ) + + +class UniqueIdentifier(BitString): + __slots__ = () + pass + + +class Extension(Sequence): + __slots__ = () + schema = ( + ("extnID", ObjectIdentifier()), + ("critical", Boolean(default=False)), + ("extnValue", OctetString()), + ) + + +class Extensions(SequenceOf): + __slots__ = () + schema = Extension() + bounds = (1, float("+inf")) + + +class TBSCertificate(Sequence): + __slots__ = () + schema = ( + ("version", Version(expl=tag_ctxc(0), default="v1")), + ("serialNumber", CertificateSerialNumber()), + ("signature", AlgorithmIdentifier()), + ("issuer", Name()), + ("validity", Validity()), + ("subject", Name()), + ("subjectPublicKeyInfo", SubjectPublicKeyInfo()), + ("issuerUniqueID", UniqueIdentifier(impl=tag_ctxp(1), optional=True)), + ("subjectUniqueID", UniqueIdentifier(impl=tag_ctxp(2), optional=True)), + ("extensions", Extensions(expl=tag_ctxc(3), optional=True)), + ) + + +class Certificate(Sequence): + __slots__ = () + schema = ( + ("tbsCertificate", TBSCertificate()), + ("signatureAlgorithm", AlgorithmIdentifier()), + ("signatureValue", BitString()), + ) + + +class TestGoSelfSignedVector(TestCase): + def runTest(self): + raw = hexdec("".join(( + "30820218308201c20209008cc3379210ec2c98300d06092a864886f70d0101050", + "500308192310b3009060355040613025858311330110603550408130a536f6d65", + "2d5374617465310d300b06035504071304436974793121301f060355040a13184", + "96e7465726e6574205769646769747320507479204c7464311a30180603550403", + "131166616c73652e6578616d706c652e636f6d3120301e06092a864886f70d010", + "901161166616c7365406578616d706c652e636f6d301e170d3039313030383030", + "323535335a170d3130313030383030323535335a308192310b300906035504061", + "3025858311330110603550408130a536f6d652d5374617465310d300b06035504", + "071304436974793121301f060355040a1318496e7465726e65742057696467697", + "47320507479204c7464311a30180603550403131166616c73652e6578616d706c", + "652e636f6d3120301e06092a864886f70d010901161166616c7365406578616d7", + "06c652e636f6d305c300d06092a864886f70d0101010500034b003048024100cd", + "b7639c3278f006aa277f6eaf42902b592d8cbcbe38a1c92ba4695a331b1deadea", + "dd8e9a5c27e8c4c2fd0a8889657722a4f2af7589cf2c77045dc8fdeec357d0203", + "010001300d06092a864886f70d0101050500034100a67b06ec5ece92772ca413c", + "ba3ca12568fdc6c7b4511cd40a7f659980402df2b998bb9a4a8cbeb34c0f0a78c", + "f8d91ede14a5ed76bf116fe360aafa8821490435", + ))) + crt, tail = Certificate().decode(raw) + self.assertSequenceEqual(tail, b"") + tbs = crt["tbsCertificate"] + self.assertEqual(tbs["version"], 0) + self.assertFalse(tbs["version"].decoded) + self.assertNotIn("version", tbs) + self.assertEqual(tbs["serialNumber"], 10143011886257155224) + + def assert_raw_equals(obj, expect): + self.assertTrue(obj.decoded) + self.assertSequenceEqual( + raw[obj.offset:obj.offset + obj.tlvlen], + expect.encode(), + ) + assert_raw_equals(tbs["serialNumber"], Integer(10143011886257155224)) + algo_id = AlgorithmIdentifier() + algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.5") + algo_id["parameters"] = Any(Null()) + self.assertEqual(tbs["signature"], algo_id) + assert_raw_equals(tbs["signature"], algo_id) + issuer = Name() + rdnSeq = RDNSequence() + for oid, klass, text in ( + ("2.5.4.6", PrintableString, "XX"), + ("2.5.4.8", PrintableString, "Some-State"), + ("2.5.4.7", PrintableString, "City"), + ("2.5.4.10", PrintableString, "Internet Widgits Pty Ltd"), + ("2.5.4.3", PrintableString, "false.example.com"), + ("1.2.840.113549.1.9.1", IA5String, "false@example.com"), + ): + attr = AttributeTypeAndValue() + attr["type"] = AttributeType(oid) + attr["value"] = AttributeValue(klass(text)) + rdn = RelativeDistinguishedName() + rdn.append(attr) + rdnSeq.append(rdn) + issuer["rdnSequence"] = rdnSeq + self.assertEqual(tbs["issuer"], issuer) + assert_raw_equals(tbs["issuer"], issuer) + validity = Validity() + validity["notBefore"] = Time( + ("utcTime", UTCTime(datetime(2009, 10, 8, 0, 25, 53))) + ) + validity["notAfter"] = Time( + ("utcTime", UTCTime(datetime(2010, 10, 8, 0, 25, 53))) + ) + self.assertEqual(tbs["validity"], validity) + assert_raw_equals(tbs["validity"], validity) + self.assertEqual(tbs["subject"], issuer) + assert_raw_equals(tbs["subject"], issuer) + spki = SubjectPublicKeyInfo() + algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.1") + spki["algorithm"] = algo_id + spki["subjectPublicKey"] = BitString(hexdec("".join(( + "3048024100cdb7639c3278f006aa277f6eaf42902b592d8cbcbe38a1c92ba4695", + "a331b1deadeadd8e9a5c27e8c4c2fd0a8889657722a4f2af7589cf2c77045dc8f", + "deec357d0203010001", + )))) + self.assertEqual(tbs["subjectPublicKeyInfo"], spki) + assert_raw_equals(tbs["subjectPublicKeyInfo"], spki) + self.assertNotIn("issuerUniqueID", tbs) + self.assertNotIn("subjectUniqueID", tbs) + self.assertNotIn("extensions", tbs) + algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.5") + self.assertEqual(crt["signatureAlgorithm"], algo_id) + self.assertEqual(crt["signatureValue"], BitString(hexdec("".join(( + "a67b06ec5ece92772ca413cba3ca12568fdc6c7b4511cd40a7f659980402df2b", + "998bb9a4a8cbeb34c0f0a78cf8d91ede14a5ed76bf116fe360aafa8821490435", + ))))) + self.assertSequenceEqual(crt.encode(), raw) + pprint(crt) + repr(crt) + + tbs = TBSCertificate() + tbs["serialNumber"] = CertificateSerialNumber(10143011886257155224) + + sign_algo_id = AlgorithmIdentifier() + sign_algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.5") + sign_algo_id["parameters"] = Any(Null()) + tbs["signature"] = sign_algo_id + + rdnSeq = RDNSequence() + for oid, klass, text in ( + ("2.5.4.6", PrintableString, "XX"), + ("2.5.4.8", PrintableString, "Some-State"), + ("2.5.4.7", PrintableString, "City"), + ("2.5.4.10", PrintableString, "Internet Widgits Pty Ltd"), + ("2.5.4.3", PrintableString, "false.example.com"), + ("1.2.840.113549.1.9.1", IA5String, "false@example.com"), + ): + attr = AttributeTypeAndValue() + attr["type"] = AttributeType(oid) + attr["value"] = AttributeValue(klass(text)) + rdn = RelativeDistinguishedName() + rdn.append(attr) + rdnSeq.append(rdn) + issuer = Name() + issuer["rdnSequence"] = rdnSeq + tbs["issuer"] = issuer + tbs["subject"] = issuer + + validity = Validity() + validity["notBefore"] = Time(("utcTime", UTCTime(datetime(2009, 10, 8, 0, 25, 53)))) + validity["notAfter"] = Time(("utcTime", UTCTime(datetime(2010, 10, 8, 0, 25, 53)))) + tbs["validity"] = validity + + spki = SubjectPublicKeyInfo() + spki_algo_id = sign_algo_id.copy() + spki_algo_id["algorithm"] = ObjectIdentifier("1.2.840.113549.1.1.1") + spki["algorithm"] = spki_algo_id + spki["subjectPublicKey"] = BitString(hexdec("".join(( + "3048024100cdb7639c3278f006aa277f6eaf42902b592d8cbcbe38a1c92ba4695", + "a331b1deadeadd8e9a5c27e8c4c2fd0a8889657722a4f2af7589cf2c77045dc8f", + "deec357d0203010001", + )))) + tbs["subjectPublicKeyInfo"] = spki + + crt = Certificate() + crt["tbsCertificate"] = tbs + crt["signatureAlgorithm"] = sign_algo_id + crt["signatureValue"] = BitString(hexdec("".join(( + "a67b06ec5ece92772ca413cba3ca12568fdc6c7b4511cd40a7f659980402df2b", + "998bb9a4a8cbeb34c0f0a78cf8d91ede14a5ed76bf116fe360aafa8821490435", + )))) + self.assertSequenceEqual(crt.encode(), raw) + + +class TestGoPayPalVector(TestCase): + def runTest(self): + raw = hexdec("".join(( + "30820644308205ada003020102020300f09b300d06092a864886f70d010105050", + "030820112310b3009060355040613024553311230100603550408130942617263", + "656c6f6e61311230100603550407130942617263656c6f6e61312930270603550", + "40a13204950532043657274696669636174696f6e20417574686f726974792073", + "2e6c2e312e302c060355040a142567656e6572616c4069707363612e636f6d204", + "32e492e462e2020422d423632323130363935312e302c060355040b1325697073", + "434120434c41534541312043657274696669636174696f6e20417574686f72697", + "479312e302c06035504031325697073434120434c415345413120436572746966", + "69636174696f6e20417574686f726974793120301e06092a864886f70d0109011", + "61167656e6572616c4069707363612e636f6d301e170d30393032323432333034", + "31375a170d3131303232343233303431375a308194310b3009060355040613025", + "553311330110603550408130a43616c69666f726e696131163014060355040713", + "0d53616e204672616e636973636f3111300f060355040a1308536563757269747", + "931143012060355040b130b53656375726520556e6974312f302d060355040313", + "267777772e70617970616c2e636f6d0073736c2e736563757265636f6e6e65637", + "4696f6e2e636330819f300d06092a864886f70d010101050003818d0030818902", + "818100d269fa6f3a00b4211bc8b102d73f19b2c46db454f88b8accdb72c29e3c6", + "0b9c6913d82b77d99ffd12984c173539c82ddfc248c77d541f3e81e42a1ad2d9e", + "ff5b1026ce9d571773162338c8d6f1baa3965b16674a4f73973a4d14a4f4e23f8", + "b058342d1d0dc2f7ae5b610b211c0dc212a90ffae97715a4981ac40f33bb859b2", + "4f0203010001a38203213082031d30090603551d1304023000301106096086480", + "186f8420101040403020640300b0603551d0f0404030203f830130603551d2504", + "0c300a06082b06010505070301301d0603551d0e04160414618f61344355147f2", + "709ce4c8bea9b7b1925bc6e301f0603551d230418301680140e0760d439c91b5b", + "5d907b23c8d2349d4a9a463930090603551d1104023000301c0603551d1204153", + "013811167656e6572616c4069707363612e636f6d307206096086480186f84201", + "0d046516634f7267616e697a6174696f6e20496e666f726d6174696f6e204e4f5", + "42056414c4944415445442e20434c415345413120536572766572204365727469", + "666963617465206973737565642062792068747470733a2f2f7777772e6970736", + "3612e636f6d2f302f06096086480186f84201020422162068747470733a2f2f77", + "77772e69707363612e636f6d2f6970736361323030322f304306096086480186f", + "84201040436163468747470733a2f2f7777772e69707363612e636f6d2f697073", + "6361323030322f697073636132303032434c41534541312e63726c30460609608", + "6480186f84201030439163768747470733a2f2f7777772e69707363612e636f6d", + "2f6970736361323030322f7265766f636174696f6e434c41534541312e68746d6", + "c3f304306096086480186f84201070436163468747470733a2f2f7777772e6970", + "7363612e636f6d2f6970736361323030322f72656e6577616c434c41534541312", + "e68746d6c3f304106096086480186f84201080434163268747470733a2f2f7777", + "772e69707363612e636f6d2f6970736361323030322f706f6c696379434c41534", + "541312e68746d6c3081830603551d1f047c307a3039a037a0358633687474703a", + "2f2f7777772e69707363612e636f6d2f6970736361323030322f6970736361323", + "03032434c41534541312e63726c303da03ba0398637687474703a2f2f77777762", + "61636b2e69707363612e636f6d2f6970736361323030322f69707363613230303", + "2434c41534541312e63726c303206082b0601050507010104263024302206082b", + "060105050730018616687474703a2f2f6f6373702e69707363612e636f6d2f300", + "d06092a864886f70d01010505000381810068ee799797dd3bef166a06f2149a6e", + "cd9e12f7aa8310bdd17c98fac7aed40e2c9e38059d5260a9990a81b498901daeb", + "b4ad7b9dc889e3778415bf782a5f2ba41255a901a1e4538a1525875942644fb20", + "07ba44cce54a2d723f9847f626dc054605076321ab469b9c78d5545b3d0c1ec86", + "48cb55023826fdbb8221c439607a8bb", + ))) + crt, tail = Certificate().decode(raw) + self.assertSequenceEqual(tail, b"") + self.assertSequenceEqual(crt.encode(), raw) + pprint(crt) + repr(crt) diff --git a/tests/test_pyderasn.py b/tests/test_pyderasn.py new file mode 100644 index 0000000..2fa22ab --- /dev/null +++ b/tests/test_pyderasn.py @@ -0,0 +1,4887 @@ +# coding: utf-8 +# PyDERASN -- Python ASN.1 DER codec with abstract structures +# Copyright (C) 2017 Sergey Matveev +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# . + +from datetime import datetime +from string import ascii_letters +from string import printable +from string import whitespace +from unittest import TestCase + +from hypothesis import assume +from hypothesis import given +from hypothesis import settings +from hypothesis.strategies import binary +from hypothesis.strategies import booleans +from hypothesis.strategies import composite +from hypothesis.strategies import data as data_strategy +from hypothesis.strategies import datetimes +from hypothesis.strategies import dictionaries +from hypothesis.strategies import integers +from hypothesis.strategies import just +from hypothesis.strategies import lists +from hypothesis.strategies import none +from hypothesis.strategies import one_of +from hypothesis.strategies import permutations +from hypothesis.strategies import sampled_from +from hypothesis.strategies import sets +from hypothesis.strategies import text +from hypothesis.strategies import tuples +from six import assertRaisesRegex +from six import byte2int +from six import indexbytes +from six import int2byte +from six import iterbytes +from six import PY2 +from six import text_type + +from pyderasn import _pp +from pyderasn import Any +from pyderasn import BitString +from pyderasn import BMPString +from pyderasn import Boolean +from pyderasn import BoundsError +from pyderasn import Choice +from pyderasn import DecodeError +from pyderasn import Enumerated +from pyderasn import GeneralizedTime +from pyderasn import GeneralString +from pyderasn import GraphicString +from pyderasn import hexdec +from pyderasn import hexenc +from pyderasn import IA5String +from pyderasn import Integer +from pyderasn import InvalidLength +from pyderasn import InvalidOID +from pyderasn import InvalidValueType +from pyderasn import len_decode +from pyderasn import len_encode +from pyderasn import NotEnoughData +from pyderasn import Null +from pyderasn import NumericString +from pyderasn import ObjectIdentifier +from pyderasn import ObjNotReady +from pyderasn import ObjUnknown +from pyderasn import OctetString +from pyderasn import pp_console_row +from pyderasn import pprint +from pyderasn import PrintableString +from pyderasn import Sequence +from pyderasn import SequenceOf +from pyderasn import Set +from pyderasn import SetOf +from pyderasn import tag_ctxc +from pyderasn import tag_decode +from pyderasn import tag_encode +from pyderasn import tag_strip +from pyderasn import TagClassApplication +from pyderasn import TagClassContext +from pyderasn import TagClassPrivate +from pyderasn import TagClassUniversal +from pyderasn import TagFormConstructed +from pyderasn import TagFormPrimitive +from pyderasn import TagMismatch +from pyderasn import TeletexString +from pyderasn import UniversalString +from pyderasn import UTCTime +from pyderasn import UTF8String +from pyderasn import VideotexString +from pyderasn import VisibleString + + +settings.register_profile('local', settings( + deadline=5000, + perform_health_check=False, +)) +settings.load_profile('local') +LONG_TEST_MAX_EXAMPLES = settings().max_examples * 4 + +tag_classes = sampled_from(( + TagClassApplication, + TagClassContext, + TagClassPrivate, + TagClassUniversal, +)) +tag_forms = sampled_from((TagFormConstructed, TagFormPrimitive)) + + +class TestHex(TestCase): + @given(binary()) + def test_symmetric(self, data): + self.assertEqual(hexdec(hexenc(data)), data) + + +class TestTagCoder(TestCase): + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + tag_classes, + tag_forms, + integers(min_value=0, max_value=30), + binary(max_size=5), + ) + def test_short(self, klass, form, num, junk): + raw = tag_encode(klass=klass, form=form, num=num) + self.assertEqual(tag_decode(raw), (klass, form, num)) + self.assertEqual(len(raw), 1) + self.assertEqual( + byte2int(tag_encode(klass=klass, form=form, num=0)), + byte2int(raw) & (1 << 7 | 1 << 6 | 1 << 5), + ) + stripped, tlen, tail = tag_strip(memoryview(raw + junk)) + self.assertSequenceEqual(stripped.tobytes(), raw) + self.assertEqual(tlen, len(raw)) + self.assertSequenceEqual(tail, junk) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + tag_classes, + tag_forms, + integers(min_value=31), + binary(max_size=5), + ) + def test_long(self, klass, form, num, junk): + raw = tag_encode(klass=klass, form=form, num=num) + self.assertEqual(tag_decode(raw), (klass, form, num)) + self.assertGreater(len(raw), 1) + self.assertEqual( + byte2int(tag_encode(klass=klass, form=form, num=0)) | 31, + byte2int(raw[:1]), + ) + self.assertEqual(byte2int(raw[-1:]) & 0x80, 0) + self.assertTrue(all(b & 0x80 > 0 for b in iterbytes(raw[1:-1]))) + stripped, tlen, tail = tag_strip(memoryview(raw + junk)) + self.assertSequenceEqual(stripped.tobytes(), raw) + self.assertEqual(tlen, len(raw)) + self.assertSequenceEqual(tail, junk) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(integers(min_value=31)) + def test_unfinished_tag(self, num): + raw = bytearray(tag_encode(num=num)) + for i in range(1, len(raw)): + raw[i] |= 0x80 + with assertRaisesRegex(self, DecodeError, "unfinished tag"): + tag_strip(bytes(raw)) + + def test_go_vectors_valid(self): + for data, (eklass, etag, elen, eform) in ( + (b"\x80\x01", (TagClassContext, 0, 1, TagFormPrimitive)), + (b"\xa0\x01", (TagClassContext, 0, 1, TagFormConstructed)), + (b"\x02\x00", (TagClassUniversal, 2, 0, TagFormPrimitive)), + (b"\xfe\x00", (TagClassPrivate, 30, 0, TagFormConstructed)), + (b"\x1f\x1f\x00", (TagClassUniversal, 31, 0, TagFormPrimitive)), + (b"\x1f\x81\x00\x00", (TagClassUniversal, 128, 0, TagFormPrimitive)), + (b"\x1f\x81\x80\x01\x00", (TagClassUniversal, 0x4001, 0, TagFormPrimitive)), + (b"\x00\x81\x80", (TagClassUniversal, 0, 128, TagFormPrimitive)), + (b"\x00\x82\x01\x00", (TagClassUniversal, 0, 256, TagFormPrimitive)), + (b"\xa0\x84\x7f\xff\xff\xff", (TagClassContext, 0, 0x7fffffff, TagFormConstructed)), + ): + tag, _, len_encoded = tag_strip(memoryview(data)) + klass, form, num = tag_decode(tag) + _len, _, tail = len_decode(len_encoded) + self.assertSequenceEqual(tail, b"") + self.assertEqual(klass, eklass) + self.assertEqual(num, etag) + self.assertEqual(_len, elen) + self.assertEqual(form, eform) + + def test_go_vectors_invalid(self): + for data in ( + b"\x00\x83\x01\x00", + b"\x1f\x85", + b"\x30\x80", + b"\xa0\x82\x00\xff", + b"\xa0\x81\x7f", + ): + with self.assertRaises(DecodeError): + _, _, len_encoded = tag_strip(memoryview(data)) + len_decode(len_encoded) + + @given( + integers(min_value=0, max_value=127), + integers(min_value=0, max_value=2), + ) + def test_long_instead_of_short(self, l, dummy_num): + octets = (b"\x00" * dummy_num) + int2byte(l) + octets = int2byte((dummy_num + 1) | 0x80) + octets + with self.assertRaises(DecodeError): + len_decode(octets) + + +class TestLenCoder(TestCase): + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + integers(min_value=0, max_value=127), + binary(max_size=5), + ) + def test_short(self, l, junk): + raw = len_encode(l) + junk + decoded, llen, tail = len_decode(memoryview(raw)) + self.assertEqual(decoded, l) + self.assertEqual(llen, 1) + self.assertEqual(len(raw), 1 + len(junk)) + self.assertEqual(tail.tobytes(), junk) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + integers(min_value=128), + binary(max_size=5), + ) + def test_long(self, l, junk): + raw = len_encode(l) + junk + decoded, llen, tail = len_decode(memoryview(raw)) + self.assertEqual(decoded, l) + self.assertEqual((llen - 1) | 0x80, byte2int(raw)) + self.assertEqual(llen, len(raw) - len(junk)) + self.assertNotEqual(indexbytes(raw, 1), 0) + self.assertSequenceEqual(tail.tobytes(), junk) + + def test_empty(self): + with self.assertRaises(NotEnoughData): + len_decode(b"") + + @given(integers(min_value=128)) + def test_stripped(self, _len): + with self.assertRaises(NotEnoughData): + len_decode(len_encode(_len)[:-1]) + + +text_printable = text(alphabet=printable, min_size=1) + + +@composite +def text_letters(draw): + result = draw(text(alphabet=ascii_letters, min_size=1)) + if PY2: + result = result.encode("ascii") + return result + + +class CommonMixin(object): + def test_tag_default(self): + obj = self.base_klass() + self.assertEqual(obj.tag, obj.tag_default) + + def test_simultaneous_impl_expl(self): + with self.assertRaises(ValueError): + self.base_klass(impl=b"whatever", expl=b"whenever") + + @given(binary(), integers(), integers(), integers()) + def test_decoded(self, impl, offset, llen, vlen): + obj = self.base_klass(impl=impl, _decoded=(offset, llen, vlen)) + self.assertEqual(obj.offset, offset) + self.assertEqual(obj.llen, llen) + self.assertEqual(obj.vlen, vlen) + self.assertEqual(obj.tlen, len(impl)) + self.assertEqual(obj.tlvlen, obj.tlen + obj.llen + obj.vlen) + + @given(binary()) + def test_impl_inherited(self, impl_tag): + class Inherited(self.base_klass): + __slots__ = () + impl = impl_tag + obj = Inherited() + self.assertSequenceEqual(obj.impl, impl_tag) + self.assertFalse(obj.expled) + + @given(binary()) + def test_expl_inherited(self, expl_tag): + class Inherited(self.base_klass): + __slots__ = () + expl = expl_tag + obj = Inherited() + self.assertSequenceEqual(obj.expl, expl_tag) + self.assertTrue(obj.expled) + + def assert_copied_basic_fields(self, obj, obj_copied): + self.assertEqual(obj, obj_copied) + self.assertSequenceEqual(obj.tag, obj_copied.tag) + self.assertEqual(obj.expl_tag, obj_copied.expl_tag) + self.assertEqual(obj.default, obj_copied.default) + self.assertEqual(obj.optional, obj_copied.optional) + self.assertEqual(obj.offset, obj_copied.offset) + self.assertEqual(obj.llen, obj_copied.llen) + self.assertEqual(obj.vlen, obj_copied.vlen) + + +@composite +def boolean_values_strat(draw, do_expl=False): + value = draw(one_of(none(), booleans())) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + default = draw(one_of(none(), booleans())) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (value, impl, expl, default, optional, _decoded) + + +class BooleanInherited(Boolean): + __slots__ = () + + +class TestBoolean(CommonMixin, TestCase): + base_klass = Boolean + + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + Boolean(123) + repr(err.exception) + + @given(booleans()) + def test_optional(self, optional): + obj = Boolean(default=Boolean(False), optional=optional) + self.assertTrue(obj.optional) + + @given(booleans()) + def test_ready(self, value): + obj = Boolean() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + obj = Boolean(value) + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + + @given(booleans(), booleans(), binary(), binary()) + def test_comparison(self, value1, value2, tag1, tag2): + for klass in (Boolean, BooleanInherited): + obj1 = klass(value1) + obj2 = klass(value2) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == bool(obj2), value1 == value2) + obj1 = klass(value1, impl=tag1) + obj2 = klass(value1, impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + + @given(data_strategy()) + def test_call(self, d): + for klass in (Boolean, BooleanInherited): + ( + value_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(boolean_values_strat()) + obj_initial = klass( + value_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial or False, + _decoded_initial, + ) + ( + value, + impl, + expl, + default, + optional, + _decoded, + ) = d.draw(boolean_values_strat(do_expl=impl_initial is None)) + obj = obj_initial(value, impl, expl, default, optional) + if obj.ready: + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + self.assertEqual(obj, value_expected) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + self.assertEqual( + obj.default, + default_initial if default is None else default, + ) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + + @given(boolean_values_strat()) + def test_copy(self, values): + for klass in (Boolean, BooleanInherited): + obj = klass(*values) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + + @given( + booleans(), + integers(min_value=1).map(tag_encode), + ) + def test_stripped(self, value, tag_impl): + obj = Boolean(value, impl=tag_impl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + booleans(), + integers(min_value=1).map(tag_ctxc), + ) + def test_stripped_expl(self, value, tag_expl): + obj = Boolean(value, expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Boolean().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_expl_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Boolean(expl=Boolean.tag_default).decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Boolean().decode( + Boolean.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_expl_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Boolean(expl=Boolean.tag_default).decode( + Boolean.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + boolean_values_strat(), + booleans(), + integers(min_value=1).map(tag_ctxc), + integers(min_value=0), + ) + def test_symmetric(self, values, value, tag_expl, offset): + for klass in (Boolean, BooleanInherited): + _, _, _, default, optional, _decoded = values + obj = klass( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertNotEqual(obj_decoded, obj) + self.assertEqual(bool(obj_decoded), bool(obj_expled)) + self.assertEqual(bool(obj_decoded), bool(obj)) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + + @given(integers(min_value=2)) + def test_invalid_len(self, l): + with self.assertRaises(InvalidLength): + Boolean().decode(b"".join(( + Boolean.tag_default, + len_encode(l), + b"\x00" * l, + ))) + + @given(integers(min_value=0 + 1, max_value=255 - 1)) + def test_invalid_value(self, value): + with assertRaisesRegex(self, DecodeError, "unacceptable Boolean value"): + Boolean().decode(b"".join(( + Boolean.tag_default, + len_encode(1), + int2byte(value), + ))) + + +@composite +def integer_values_strat(draw, do_expl=False): + bound_min, value, default, bound_max = sorted(draw(sets( + integers(), + min_size=4, + max_size=4, + ))) + if draw(booleans()): + value = None + _specs = None + if draw(booleans()): + _specs = draw(sets(text_letters())) + values = draw(sets( + integers(), + min_size=len(_specs), + max_size=len(_specs), + )) + _specs = list(zip(_specs, values)) + bounds = None + if draw(booleans()): + bounds = (bound_min, bound_max) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + if draw(booleans()): + default = None + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (value, bounds, impl, expl, default, optional, _specs, _decoded) + + +class IntegerInherited(Integer): + __slots__ = () + + +class TestInteger(CommonMixin, TestCase): + base_klass = Integer + + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + Integer(12.3) + repr(err.exception) + + @given(sets(text_letters(), min_size=2)) + def test_unknown_name(self, names_input): + missing = names_input.pop() + + class Int(Integer): + __slots__ = () + schema = [(n, 123) for n in names_input] + with self.assertRaises(ObjUnknown) as err: + Int(missing) + repr(err.exception) + + @given(sets(text_letters(), min_size=2)) + def test_known_name(self, names_input): + class Int(Integer): + __slots__ = () + schema = [(n, 123) for n in names_input] + Int(names_input.pop()) + + @given(booleans()) + def test_optional(self, optional): + obj = Integer(default=Integer(0), optional=optional) + self.assertTrue(obj.optional) + + @given(integers()) + def test_ready(self, value): + obj = Integer() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + obj = Integer(value) + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + hash(obj) + + @given(integers(), integers(), binary(), binary()) + def test_comparison(self, value1, value2, tag1, tag2): + for klass in (Integer, IntegerInherited): + obj1 = klass(value1) + obj2 = klass(value2) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == int(obj2), value1 == value2) + obj1 = klass(value1, impl=tag1) + obj2 = klass(value1, impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + + @given(lists(integers())) + def test_sorted_works(self, values): + self.assertSequenceEqual( + [int(v) for v in sorted(Integer(v) for v in values)], + sorted(values), + ) + + @given(data_strategy()) + def test_named(self, d): + names_input = list(d.draw(sets(text_letters(), min_size=1))) + values_input = list(d.draw(sets( + integers(), + min_size=len(names_input), + max_size=len(names_input), + ))) + chosen_name = d.draw(sampled_from(names_input)) + names_input = dict(zip(names_input, values_input)) + + class Int(Integer): + __slots__ = () + schema = names_input + _int = Int(chosen_name) + self.assertEqual(_int.named, chosen_name) + self.assertEqual(int(_int), names_input[chosen_name]) + + @given(integers(), integers(min_value=0), integers(min_value=0)) + def test_bounds_satisfied(self, bound_min, bound_delta, value_delta): + value = bound_min + value_delta + bound_max = value + bound_delta + Integer(value=value, bounds=(bound_min, bound_max)) + + @given(sets(integers(), min_size=3, max_size=3)) + def test_bounds_unsatisfied(self, values): + values = sorted(values) + with self.assertRaises(BoundsError) as err: + Integer(value=values[0], bounds=(values[1], values[2])) + repr(err.exception) + with self.assertRaises(BoundsError) as err: + Integer(value=values[2], bounds=(values[0], values[1])) + repr(err.exception) + + @given(data_strategy()) + def test_call(self, d): + for klass in (Integer, IntegerInherited): + ( + value_initial, + bounds_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _specs_initial, + _decoded_initial, + ) = d.draw(integer_values_strat()) + obj_initial = klass( + value_initial, + bounds_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial or False, + _specs_initial, + _decoded_initial, + ) + ( + value, + bounds, + impl, + expl, + default, + optional, + _, + _decoded, + ) = d.draw(integer_values_strat(do_expl=impl_initial is None)) + if (default is None) and (obj_initial.default is not None): + bounds = None + if ( + (bounds is None) and + (value is not None) and + (bounds_initial is not None) and + not (bounds_initial[0] <= value <= bounds_initial[1]) + ): + value = None + if ( + (bounds is None) and + (default is not None) and + (bounds_initial is not None) and + not (bounds_initial[0] <= default <= bounds_initial[1]) + ): + default = None + obj = obj_initial(value, bounds, impl, expl, default, optional) + if obj.ready: + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + self.assertEqual(obj, value_expected) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + self.assertEqual( + obj.default, + default_initial if default is None else default, + ) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + self.assertEqual( + (obj._bound_min, obj._bound_max), + bounds or bounds_initial or (float("-inf"), float("+inf")), + ) + self.assertEqual( + obj.specs, + {} if _specs_initial is None else dict(_specs_initial), + ) + + @given(integer_values_strat()) + def test_copy(self, values): + for klass in (Integer, IntegerInherited): + obj = klass(*values) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj.specs, obj_copied.specs) + self.assertEqual(obj._bound_min, obj_copied._bound_min) + self.assertEqual(obj._bound_max, obj_copied._bound_max) + self.assertEqual(obj._value, obj_copied._value) + + @given( + integers(), + integers(min_value=1).map(tag_encode), + ) + def test_stripped(self, value, tag_impl): + obj = Integer(value, impl=tag_impl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + integers(), + integers(min_value=1).map(tag_ctxc), + ) + def test_stripped_expl(self, value, tag_expl): + obj = Integer(value, expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + def test_zero_len(self): + with self.assertRaises(NotEnoughData): + Integer().decode(b"".join(( + Integer.tag_default, + len_encode(0), + ))) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Integer().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Integer().decode( + Integer.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + sets(integers(), min_size=2, max_size=2), + integers(min_value=0), + lists(integers()), + ) + def test_invalid_bounds_while_decoding(self, ints, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + value, bound_min = list(sorted(ints)) + + class Int(Integer): + bounds = (bound_min, bound_min) + with self.assertRaises(DecodeError) as err: + Int().decode( + Integer(value).encode(), + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + integer_values_strat(), + integers(), + integers(min_value=1).map(tag_ctxc), + integers(min_value=0), + ) + def test_symmetric(self, values, value, tag_expl, offset): + for klass in (Integer, IntegerInherited): + _, _, _, _, default, optional, _, _decoded = values + obj = klass( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertNotEqual(obj_decoded, obj) + self.assertEqual(int(obj_decoded), int(obj_expled)) + self.assertEqual(int(obj_decoded), int(obj)) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + + def test_go_vectors_valid(self): + for data, expect in (( + (b"\x00", 0), + (b"\x7f", 127), + (b"\x80", -128), + (b"\xff\x7f", -129), + (b"\xff", -1), + (b"\x01", 1), + (b"\x00\xff", 255), + (b"\xff\x00", -256), + (b"\x01\x00", 256), + (b"\x00\x80", 128), + (b"\x01\x00", 256), + (b"\x80\x00\x00\x00\x00\x00\x00\x00", -9223372036854775808), + (b"\x80\x00\x00\x00", -2147483648), + )): + self.assertEqual( + Integer().decode(b"".join(( + Integer.tag_default, + len_encode(len(data)), + data, + )))[0], + expect, + ) + + def test_go_vectors_invalid(self): + for data in (( + b"\x00\x7f", + b"\xff\xf0", + )): + with self.assertRaises(DecodeError): + Integer().decode(b"".join(( + Integer.tag_default, + len_encode(len(data)), + data, + ))) + + +@composite +def bit_string_values_strat(draw, schema=None, value_required=False, do_expl=False): + if schema is None: + schema = () + if draw(booleans()): + schema = draw(sets(text_letters(), min_size=1, max_size=256)) + bits = draw(sets( + integers(min_value=0, max_value=255), + min_size=len(schema), + max_size=len(schema), + )) + schema = list(zip(schema, bits)) + + def _value(value_required): + if not value_required and draw(booleans()): + return + generation_choice = 0 + if value_required: + generation_choice = draw(sampled_from((1, 2, 3))) + if generation_choice == 1 or draw(booleans()): + return "'%s'B" % "".join(draw(lists( + sampled_from(("0", "1")), + max_size=len(schema), + ))) + elif generation_choice == 2 or draw(booleans()): + return draw(binary(max_size=len(schema) // 8)) + elif generation_choice == 3 or draw(booleans()): + return tuple(draw(lists(sampled_from([name for name, _ in schema])))) + return None + value = _value(value_required) + default = _value(value_required=False) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (schema, value, impl, expl, default, optional, _decoded) + + +class BitStringInherited(BitString): + __slots__ = () + + +class TestBitString(CommonMixin, TestCase): + base_klass = BitString + + @given(lists(booleans())) + def test_b_encoding(self, bits): + obj = BitString("'%s'B" % "".join("1" if bit else "0" for bit in bits)) + self.assertEqual(obj.bit_len, len(bits)) + self.assertSequenceEqual(list(obj), bits) + for i, bit in enumerate(bits): + self.assertEqual(obj[i], bit) + + @given(lists(booleans())) + def test_out_of_bounds_bits(self, bits): + obj = BitString("'%s'B" % "".join("1" if bit else "0" for bit in bits)) + for i in range(len(bits), len(bits) * 2): + self.assertFalse(obj[i]) + + def test_bad_b_encoding(self): + with self.assertRaises(ValueError): + BitString("'010120101'B") + + @given( + integers(min_value=1, max_value=255), + integers(min_value=1, max_value=255), + ) + def test_named_are_stripped(self, leading_zeros, trailing_zeros): + obj = BitString("'%s1%s'B" % (("0" * leading_zeros), ("0" * trailing_zeros))) + self.assertEqual(obj.bit_len, leading_zeros + 1 + trailing_zeros) + self.assertGreater(len(obj.encode()), (leading_zeros + 1 + trailing_zeros) // 8) + + class BS(BitString): + __slots__ = () + schema = (("whatever", 0),) + obj = BS("'%s1%s'B" % (("0" * leading_zeros), ("0" * trailing_zeros))) + self.assertEqual(obj.bit_len, leading_zeros + 1) + self.assertGreater(len(obj.encode()), (leading_zeros + 1) // 8) + + def test_zero_len(self): + with self.assertRaises(NotEnoughData): + BitString().decode(b"".join(( + BitString.tag_default, + len_encode(0), + ))) + + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + BitString(123) + repr(err.exception) + with self.assertRaises(InvalidValueType) as err: + BitString(u"123") + repr(err.exception) + + def test_obj_unknown(self): + with self.assertRaises(ObjUnknown) as err: + BitString(b"whatever")["whenever"] + repr(err.exception) + + def test_get_invalid_typ(self): + with self.assertRaises(InvalidValueType) as err: + BitString(b"whatever")[(1, 2, 3)] + repr(err.exception) + + @given(data_strategy()) + def test_unknown_name(self, d): + _schema = d.draw(sets(text_letters(), min_size=2, max_size=5)) + missing = _schema.pop() + + class BS(BitString): + __slots__ = () + schema = [(n, i) for i, n in enumerate(_schema)] + with self.assertRaises(ObjUnknown) as err: + BS((missing,)) + repr(err.exception) + + @given(booleans()) + def test_optional(self, optional): + obj = BitString(default=BitString(b""), optional=optional) + self.assertTrue(obj.optional) + + @given(binary()) + def test_ready(self, value): + obj = BitString() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + obj = BitString(value) + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + + @given( + tuples(integers(min_value=0), binary()), + tuples(integers(min_value=0), binary()), + binary(), + binary(), + ) + def test_comparison(self, value1, value2, tag1, tag2): + for klass in (BitString, BitStringInherited): + obj1 = klass(value1) + obj2 = klass(value2) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == bytes(obj2), value1[1] == value2[1]) + obj1 = klass(value1, impl=tag1) + obj2 = klass(value1, impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + + @given(data_strategy()) + def test_call(self, d): + for klass in (BitString, BitStringInherited): + ( + schema_initial, + value_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(bit_string_values_strat()) + + class BS(klass): + __slots__ = () + schema = schema_initial + obj_initial = BS( + value=value_initial, + impl=impl_initial, + expl=expl_initial, + default=default_initial, + optional=optional_initial or False, + _decoded=_decoded_initial, + ) + ( + _, + value, + impl, + expl, + default, + optional, + _decoded, + ) = d.draw(bit_string_values_strat( + schema=schema_initial, + do_expl=impl_initial is None, + )) + obj = obj_initial( + value=value, + impl=impl, + expl=expl, + default=default, + optional=optional, + ) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + self.assertEqual(obj.specs, obj_initial.specs) + + @given(bit_string_values_strat()) + def test_copy(self, values): + for klass in (BitString, BitStringInherited): + _schema, value, impl, expl, default, optional, _decoded = values + + class BS(klass): + __slots__ = () + schema = _schema + obj = BS( + value=value, + impl=impl, + expl=expl, + default=default, + optional=optional or False, + _decoded=_decoded, + ) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj.specs, obj_copied.specs) + self.assertEqual(obj._value, obj_copied._value) + + @given( + binary(), + integers(min_value=1).map(tag_encode), + ) + def test_stripped(self, value, tag_impl): + obj = BitString(value, impl=tag_impl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + binary(), + integers(min_value=1).map(tag_ctxc), + ) + def test_stripped_expl(self, value, tag_expl): + obj = BitString(value, expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + BitString().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + BitString().decode( + BitString.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(data_strategy()) + def test_symmetric(self, d): + ( + _schema, + value, + _, + _, + default, + optional, + _decoded, + ) = d.draw(bit_string_values_strat(value_required=True)) + tag_expl = tag_ctxc(d.draw(integers(min_value=1))) + offset = d.draw(integers(min_value=0)) + for klass in (BitString, BitStringInherited): + class BS(klass): + __slots__ = () + schema = _schema + obj = BS( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertNotEqual(obj_decoded, obj) + self.assertEqual(bytes(obj_decoded), bytes(obj_expled)) + self.assertEqual(bytes(obj_decoded), bytes(obj)) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + if isinstance(value, tuple): + self.assertSetEqual(set(value), set(obj_decoded.named)) + for name in value: + obj_decoded[name] + + @given(integers(min_value=1, max_value=255)) + def test_bad_zero_value(self, pad_size): + with self.assertRaises(DecodeError): + BitString().decode(b"".join(( + BitString.tag_default, + len_encode(1), + int2byte(pad_size), + ))) + + def test_go_vectors_invalid(self): + for data in (( + b"\x07\x01", + b"\x07\x40", + b"\x08\x00", + )): + with self.assertRaises(DecodeError): + BitString().decode(b"".join(( + BitString.tag_default, + len_encode(2), + data, + ))) + + def test_go_vectors_valid(self): + obj, _ = BitString().decode(b"".join(( + BitString.tag_default, + len_encode(1), + b"\x00", + ))) + self.assertEqual(bytes(obj), b"") + self.assertEqual(obj.bit_len, 0) + + obj, _ = BitString().decode(b"".join(( + BitString.tag_default, + len_encode(2), + b"\x07\x00", + ))) + self.assertEqual(bytes(obj), b"\x00") + self.assertEqual(obj.bit_len, 1) + + obj = BitString((16, b"\x82\x40")) + self.assertTrue(obj[0]) + self.assertFalse(obj[1]) + self.assertTrue(obj[6]) + self.assertTrue(obj[9]) + self.assertFalse(obj[17]) + + +@composite +def octet_string_values_strat(draw, do_expl=False): + bound_min, bound_max = sorted(draw(sets( + integers(min_value=0, max_value=1 << 7), + min_size=2, + max_size=2, + ))) + value = draw(one_of( + none(), + binary(min_size=bound_min, max_size=bound_max), + )) + default = draw(one_of( + none(), + binary(min_size=bound_min, max_size=bound_max), + )) + bounds = None + if draw(booleans()): + bounds = (bound_min, bound_max) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (value, bounds, impl, expl, default, optional, _decoded) + + +class OctetStringInherited(OctetString): + __slots__ = () + + +class TestOctetString(CommonMixin, TestCase): + base_klass = OctetString + + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + OctetString(text_type(123)) + repr(err.exception) + + @given(booleans()) + def test_optional(self, optional): + obj = OctetString(default=OctetString(b""), optional=optional) + self.assertTrue(obj.optional) + + @given(binary()) + def test_ready(self, value): + obj = OctetString() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + obj = OctetString(value) + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + + @given(binary(), binary(), binary(), binary()) + def test_comparison(self, value1, value2, tag1, tag2): + for klass in (OctetString, OctetStringInherited): + obj1 = klass(value1) + obj2 = klass(value2) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == bytes(obj2), value1 == value2) + obj1 = klass(value1, impl=tag1) + obj2 = klass(value1, impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + + @given(data_strategy()) + def test_bounds_satisfied(self, d): + bound_min = d.draw(integers(min_value=0, max_value=1 << 7)) + bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7)) + value = d.draw(binary(min_size=bound_min, max_size=bound_max)) + OctetString(value=value, bounds=(bound_min, bound_max)) + + @given(data_strategy()) + def test_bounds_unsatisfied(self, d): + bound_min = d.draw(integers(min_value=1, max_value=1 << 7)) + bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7)) + value = d.draw(binary(max_size=bound_min - 1)) + with self.assertRaises(BoundsError) as err: + OctetString(value=value, bounds=(bound_min, bound_max)) + repr(err.exception) + value = d.draw(binary(min_size=bound_max + 1)) + with self.assertRaises(BoundsError) as err: + OctetString(value=value, bounds=(bound_min, bound_max)) + repr(err.exception) + + @given(data_strategy()) + def test_call(self, d): + for klass in (OctetString, OctetStringInherited): + ( + value_initial, + bounds_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(octet_string_values_strat()) + obj_initial = klass( + value_initial, + bounds_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial or False, + _decoded_initial, + ) + ( + value, + bounds, + impl, + expl, + default, + optional, + _decoded, + ) = d.draw(octet_string_values_strat(do_expl=impl_initial is None)) + if (default is None) and (obj_initial.default is not None): + bounds = None + if ( + (bounds is None) and + (value is not None) and + (bounds_initial is not None) and + not (bounds_initial[0] <= len(value) <= bounds_initial[1]) + ): + value = None + if ( + (bounds is None) and + (default is not None) and + (bounds_initial is not None) and + not (bounds_initial[0] <= len(default) <= bounds_initial[1]) + ): + default = None + obj = obj_initial(value, bounds, impl, expl, default, optional) + if obj.ready: + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + self.assertEqual(obj, value_expected) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + self.assertEqual( + obj.default, + default_initial if default is None else default, + ) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + self.assertEqual( + (obj._bound_min, obj._bound_max), + bounds or bounds_initial or (0, float("+inf")), + ) + + @given(octet_string_values_strat()) + def test_copy(self, values): + for klass in (OctetString, OctetStringInherited): + obj = klass(*values) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj._bound_min, obj_copied._bound_min) + self.assertEqual(obj._bound_max, obj_copied._bound_max) + self.assertEqual(obj._value, obj_copied._value) + + @given( + binary(), + integers(min_value=1).map(tag_encode), + ) + def test_stripped(self, value, tag_impl): + obj = OctetString(value, impl=tag_impl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + binary(), + integers(min_value=1).map(tag_ctxc), + ) + def test_stripped_expl(self, value, tag_expl): + obj = OctetString(value, expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + OctetString().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + OctetString().decode( + OctetString.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + sets(integers(min_value=0, max_value=10), min_size=2, max_size=2), + integers(min_value=0), + lists(integers()), + ) + def test_invalid_bounds_while_decoding(self, ints, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + value, bound_min = list(sorted(ints)) + + class String(OctetString): + bounds = (bound_min, bound_min) + with self.assertRaises(DecodeError) as err: + String().decode( + OctetString(b"\x00" * value).encode(), + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + octet_string_values_strat(), + binary(), + integers(min_value=1).map(tag_ctxc), + integers(min_value=0), + ) + def test_symmetric(self, values, value, tag_expl, offset): + for klass in (OctetString, OctetStringInherited): + _, _, _, _, default, optional, _decoded = values + obj = klass( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertNotEqual(obj_decoded, obj) + self.assertEqual(bytes(obj_decoded), bytes(obj_expled)) + self.assertEqual(bytes(obj_decoded), bytes(obj)) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + + +@composite +def null_values_strat(draw, do_expl=False): + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (impl, expl, optional, _decoded) + + +class NullInherited(Null): + __slots__ = () + + +class TestNull(CommonMixin, TestCase): + base_klass = Null + + def test_ready(self): + obj = Null() + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + + @given(binary(), binary()) + def test_comparison(self, tag1, tag2): + for klass in (Null, NullInherited): + obj1 = klass(impl=tag1) + obj2 = klass(impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + self.assertNotEqual(obj1, tag2) + + @given(data_strategy()) + def test_call(self, d): + for klass in (Null, NullInherited): + ( + impl_initial, + expl_initial, + optional_initial, + _decoded_initial, + ) = d.draw(null_values_strat()) + obj_initial = klass( + impl=impl_initial, + expl=expl_initial, + optional=optional_initial or False, + _decoded=_decoded_initial, + ) + ( + impl, + expl, + optional, + _decoded, + ) = d.draw(null_values_strat(do_expl=impl_initial is None)) + obj = obj_initial(impl=impl, expl=expl, optional=optional) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + self.assertEqual(obj.optional, optional) + + @given(null_values_strat()) + def test_copy(self, values): + for klass in (Null, NullInherited): + impl, expl, optional, _decoded = values + obj = klass( + impl=impl, + expl=expl, + optional=optional or False, + _decoded=_decoded, + ) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + + @given(integers(min_value=1).map(tag_encode)) + def test_stripped(self, tag_impl): + obj = Null(impl=tag_impl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given(integers(min_value=1).map(tag_ctxc)) + def test_stripped_expl(self, tag_expl): + obj = Null(expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Null().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Null().decode( + Null.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given(binary(min_size=1)) + def test_tag_mismatch(self, impl): + assume(impl != Null.tag_default) + with self.assertRaises(TagMismatch): + Null(impl=impl).decode(Null().encode()) + + @given( + null_values_strat(), + integers(min_value=1).map(tag_ctxc), + integers(min_value=0), + ) + def test_symmetric(self, values, tag_expl, offset): + for klass in (Null, NullInherited): + _, _, optional, _decoded = values + obj = klass(optional=optional, _decoded=_decoded) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertNotEqual(obj_decoded, obj) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + + @given(integers(min_value=1)) + def test_invalid_len(self, l): + with self.assertRaises(InvalidLength): + Null().decode(b"".join(( + Null.tag_default, + len_encode(l), + ))) + + +@composite +def oid_strategy(draw): + first_arc = draw(integers(min_value=0, max_value=2)) + second_arc = 0 + if first_arc in (0, 1): + second_arc = draw(integers(min_value=0, max_value=39)) + else: + second_arc = draw(integers(min_value=0)) + other_arcs = draw(lists(integers(min_value=0))) + return tuple([first_arc, second_arc] + other_arcs) + + +@composite +def oid_values_strat(draw, do_expl=False): + value = draw(one_of(none(), oid_strategy())) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + default = draw(one_of(none(), oid_strategy())) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (value, impl, expl, default, optional, _decoded) + + +class ObjectIdentifierInherited(ObjectIdentifier): + __slots__ = () + + +class TestObjectIdentifier(CommonMixin, TestCase): + base_klass = ObjectIdentifier + + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + ObjectIdentifier(123) + repr(err.exception) + + @given(booleans()) + def test_optional(self, optional): + obj = ObjectIdentifier(default=ObjectIdentifier("1.2.3"), optional=optional) + self.assertTrue(obj.optional) + + @given(oid_strategy()) + def test_ready(self, value): + obj = ObjectIdentifier() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + obj = ObjectIdentifier(value) + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + hash(obj) + + @given(oid_strategy(), oid_strategy(), binary(), binary()) + def test_comparison(self, value1, value2, tag1, tag2): + for klass in (ObjectIdentifier, ObjectIdentifierInherited): + obj1 = klass(value1) + obj2 = klass(value2) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == tuple(obj2), value1 == value2) + self.assertEqual(str(obj1) == str(obj2), value1 == value2) + obj1 = klass(value1, impl=tag1) + obj2 = klass(value1, impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + + @given(lists(oid_strategy())) + def test_sorted_works(self, values): + self.assertSequenceEqual( + [tuple(v) for v in sorted(ObjectIdentifier(v) for v in values)], + sorted(values), + ) + + @given(data_strategy()) + def test_call(self, d): + for klass in (ObjectIdentifier, ObjectIdentifierInherited): + ( + value_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(oid_values_strat()) + obj_initial = klass( + value_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial or False, + _decoded_initial, + ) + ( + value, + impl, + expl, + default, + optional, + _decoded, + ) = d.draw(oid_values_strat(do_expl=impl_initial is None)) + obj = obj_initial(value, impl, expl, default, optional) + if obj.ready: + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + self.assertEqual(obj, value_expected) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + self.assertEqual( + obj.default, + default_initial if default is None else default, + ) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + + @given(oid_values_strat()) + def test_copy(self, values): + for klass in (ObjectIdentifier, ObjectIdentifierInherited): + obj = klass(*values) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj._value, obj_copied._value) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + oid_strategy(), + integers(min_value=1).map(tag_encode), + ) + def test_stripped(self, value, tag_impl): + obj = ObjectIdentifier(value, impl=tag_impl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + oid_strategy(), + integers(min_value=1).map(tag_ctxc), + ) + def test_stripped_expl(self, value, tag_expl): + obj = ObjectIdentifier(value, expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + ObjectIdentifier().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + ObjectIdentifier().decode( + ObjectIdentifier.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + def test_zero_oid(self): + with self.assertRaises(NotEnoughData): + ObjectIdentifier().decode( + b"".join((ObjectIdentifier.tag_default, len_encode(0))) + ) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(oid_strategy()) + def test_unfinished_oid(self, value): + assume(list(value)[-1] > 255) + obj_encoded = ObjectIdentifier(value).encode() + obj, _ = ObjectIdentifier().decode(obj_encoded) + data = obj_encoded[obj.tlen + obj.llen:-1] + data = b"".join(( + ObjectIdentifier.tag_default, + len_encode(len(data)), + data, + )) + with assertRaisesRegex(self, DecodeError, "unfinished OID"): + obj.decode(data) + + @given(integers(min_value=0)) + def test_invalid_short(self, value): + with self.assertRaises(InvalidOID): + ObjectIdentifier((value,)) + with self.assertRaises(InvalidOID): + ObjectIdentifier("%d" % value) + + @given(integers(min_value=3), integers(min_value=0)) + def test_invalid_first_arc(self, first_arc, second_arc): + with self.assertRaises(InvalidOID): + ObjectIdentifier((first_arc, second_arc)) + with self.assertRaises(InvalidOID): + ObjectIdentifier("%d.%d" % (first_arc, second_arc)) + + @given(integers(min_value=0, max_value=1), integers(min_value=40)) + def test_invalid_second_arc(self, first_arc, second_arc): + with self.assertRaises(InvalidOID): + ObjectIdentifier((first_arc, second_arc)) + with self.assertRaises(InvalidOID): + ObjectIdentifier("%d.%d" % (first_arc, second_arc)) + + @given(text(alphabet=ascii_letters + ".", min_size=1)) + def test_junk(self, oid): + with self.assertRaises(InvalidOID): + ObjectIdentifier(oid) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(oid_strategy()) + def test_validness(self, oid): + obj = ObjectIdentifier(oid) + self.assertEqual(obj, ObjectIdentifier(".".join(str(arc) for arc in oid))) + str(obj) + repr(obj) + pprint(obj) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + oid_values_strat(), + oid_strategy(), + integers(min_value=1).map(tag_ctxc), + integers(min_value=0), + ) + def test_symmetric(self, values, value, tag_expl, offset): + for klass in (ObjectIdentifier, ObjectIdentifierInherited): + _, _, _, default, optional, _decoded = values + obj = klass( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertNotEqual(obj_decoded, obj) + self.assertEqual(tuple(obj_decoded), tuple(obj_expled)) + self.assertEqual(tuple(obj_decoded), tuple(obj)) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + + @given( + oid_strategy().map(ObjectIdentifier), + oid_strategy().map(ObjectIdentifier), + ) + def test_add(self, oid1, oid2): + oid_expect = ObjectIdentifier(str(oid1) + "." + str(oid2)) + for oid_to_add in (oid2, tuple(oid2)): + self.assertEqual(oid1 + oid_to_add, oid_expect) + with self.assertRaises(InvalidValueType): + oid1 + str(oid2) + + def test_go_vectors_valid(self): + for data, expect in ( + (b"\x55", (2, 5)), + (b"\x55\x02", (2, 5, 2)), + (b"\x55\x02\xc0\x00", (2, 5, 2, 8192)), + (b"\x81\x34\x03", (2, 100, 3)), + ): + self.assertEqual( + ObjectIdentifier().decode(b"".join(( + ObjectIdentifier.tag_default, + len_encode(len(data)), + data, + )))[0], + expect, + ) + + def test_go_vectors_invalid(self): + data = b"\x55\x02\xc0\x80\x80\x80\x80" + with self.assertRaises(DecodeError): + ObjectIdentifier().decode(b"".join(( + Integer.tag_default, + len_encode(len(data)), + data, + ))) + + +@composite +def enumerated_values_strat(draw, schema=None, do_expl=False): + if schema is None: + schema = list(draw(sets(text_printable, min_size=1, max_size=3))) + values = list(draw(sets( + integers(), + min_size=len(schema), + max_size=len(schema), + ))) + schema = list(zip(schema, values)) + value = draw(one_of(none(), sampled_from([k for k, v in schema]))) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + default = draw(one_of(none(), sampled_from([v for k, v in schema]))) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (schema, value, impl, expl, default, optional, _decoded) + + +class TestEnumerated(CommonMixin, TestCase): + class EWhatever(Enumerated): + __slots__ = () + schema = (("whatever", 0),) + + base_klass = EWhatever + + def test_schema_required(self): + with assertRaisesRegex(self, ValueError, "schema must be specified"): + Enumerated() + + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + self.base_klass((1, 2)) + repr(err.exception) + + @given(sets(text_letters(), min_size=2)) + def test_unknown_name(self, schema_input): + missing = schema_input.pop() + + class E(Enumerated): + __slots__ = () + schema = [(n, 123) for n in schema_input] + with self.assertRaises(ObjUnknown) as err: + E(missing) + repr(err.exception) + + @given( + sets(text_letters(), min_size=2), + sets(integers(), min_size=2), + ) + def test_unknown_value(self, schema_input, values_input): + schema_input.pop() + missing_value = values_input.pop() + _input = list(zip(schema_input, values_input)) + + class E(Enumerated): + __slots__ = () + schema = _input + with self.assertRaises(DecodeError) as err: + E(missing_value) + repr(err.exception) + + @given(booleans()) + def test_optional(self, optional): + obj = self.base_klass(default="whatever", optional=optional) + self.assertTrue(obj.optional) + + def test_ready(self): + obj = self.base_klass() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + obj = self.base_klass("whatever") + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + + @given(integers(), integers(), binary(), binary()) + def test_comparison(self, value1, value2, tag1, tag2): + class E(Enumerated): + __slots__ = () + schema = ( + ("whatever0", value1), + ("whatever1", value2), + ) + + class EInherited(E): + __slots__ = () + for klass in (E, EInherited): + obj1 = klass(value1) + obj2 = klass(value2) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == int(obj2), value1 == value2) + obj1 = klass(value1, impl=tag1) + obj2 = klass(value1, impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + + @given(data_strategy()) + def test_call(self, d): + ( + schema_initial, + value_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(enumerated_values_strat()) + + class E(Enumerated): + __slots__ = () + schema = schema_initial + obj_initial = E( + value=value_initial, + impl=impl_initial, + expl=expl_initial, + default=default_initial, + optional=optional_initial or False, + _decoded=_decoded_initial, + ) + ( + _, + value, + impl, + expl, + default, + optional, + _decoded, + ) = d.draw(enumerated_values_strat( + schema=schema_initial, + do_expl=impl_initial is None, + )) + obj = obj_initial( + value=value, + impl=impl, + expl=expl, + default=default, + optional=optional, + ) + if obj.ready: + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + self.assertEqual( + int(obj), + dict(schema_initial).get(value_expected, value_expected), + ) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + self.assertEqual( + obj.default, + default_initial if default is None else default, + ) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + self.assertEqual(obj.specs, dict(schema_initial)) + + @given(enumerated_values_strat()) + def test_copy(self, values): + schema_input, value, impl, expl, default, optional, _decoded = values + + class E(Enumerated): + __slots__ = () + schema = schema_input + obj = E( + value=value, + impl=impl, + expl=expl, + default=default, + optional=optional, + _decoded=_decoded, + ) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj.specs, obj_copied.specs) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(data_strategy()) + def test_symmetric(self, d): + schema_input, _, _, _, default, optional, _decoded = d.draw( + enumerated_values_strat(), + ) + tag_expl = d.draw(integers(min_value=1).map(tag_ctxc)) + offset = d.draw(integers(min_value=0)) + value = d.draw(sampled_from(sorted([v for _, v in schema_input]))) + + class E(Enumerated): + __slots__ = () + schema = schema_input + obj = E( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertNotEqual(obj_decoded, obj) + self.assertEqual(int(obj_decoded), int(obj_expled)) + self.assertEqual(int(obj_decoded), int(obj)) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + + +@composite +def string_values_strat(draw, alphabet, do_expl=False): + bound_min, bound_max = sorted(draw(sets( + integers(min_value=0, max_value=1 << 7), + min_size=2, + max_size=2, + ))) + value = draw(one_of( + none(), + text(alphabet=alphabet, min_size=bound_min, max_size=bound_max), + )) + default = draw(one_of( + none(), + text(alphabet=alphabet, min_size=bound_min, max_size=bound_max), + )) + bounds = None + if draw(booleans()): + bounds = (bound_min, bound_max) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (value, bounds, impl, expl, default, optional, _decoded) + + +class StringMixin(object): + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + self.base_klass((1, 2)) + repr(err.exception) + + def text_alphabet(self): + if self.base_klass.encoding in ("ascii", "iso-8859-1"): + return printable + whitespace + return None + + @given(booleans()) + def test_optional(self, optional): + obj = self.base_klass(default=self.base_klass(""), optional=optional) + self.assertTrue(obj.optional) + + @given(data_strategy()) + def test_ready(self, d): + obj = self.base_klass() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + text_type(obj) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + value = d.draw(text(alphabet=self.text_alphabet())) + obj = self.base_klass(value) + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + text_type(obj) + + @given(data_strategy()) + def test_comparison(self, d): + value1 = d.draw(text(alphabet=self.text_alphabet())) + value2 = d.draw(text(alphabet=self.text_alphabet())) + tag1 = d.draw(binary()) + tag2 = d.draw(binary()) + obj1 = self.base_klass(value1) + obj2 = self.base_klass(value2) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == bytes(obj2), value1 == value2) + self.assertEqual(obj1 == text_type(obj2), value1 == value2) + obj1 = self.base_klass(value1, impl=tag1) + obj2 = self.base_klass(value1, impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + + @given(data_strategy()) + def test_bounds_satisfied(self, d): + bound_min = d.draw(integers(min_value=0, max_value=1 << 7)) + bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7)) + value = d.draw(text( + alphabet=self.text_alphabet(), + min_size=bound_min, + max_size=bound_max, + )) + self.base_klass(value=value, bounds=(bound_min, bound_max)) + + @given(data_strategy()) + def test_bounds_unsatisfied(self, d): + bound_min = d.draw(integers(min_value=1, max_value=1 << 7)) + bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7)) + value = d.draw(text(alphabet=self.text_alphabet(), max_size=bound_min - 1)) + with self.assertRaises(BoundsError) as err: + self.base_klass(value=value, bounds=(bound_min, bound_max)) + repr(err.exception) + value = d.draw(text(alphabet=self.text_alphabet(), min_size=bound_max + 1)) + with self.assertRaises(BoundsError) as err: + self.base_klass(value=value, bounds=(bound_min, bound_max)) + repr(err.exception) + + @given(data_strategy()) + def test_call(self, d): + ( + value_initial, + bounds_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(string_values_strat(self.text_alphabet())) + obj_initial = self.base_klass( + value_initial, + bounds_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial or False, + _decoded_initial, + ) + ( + value, + bounds, + impl, + expl, + default, + optional, + _decoded, + ) = d.draw(string_values_strat( + self.text_alphabet(), + do_expl=impl_initial is None, + )) + if (default is None) and (obj_initial.default is not None): + bounds = None + if ( + (bounds is None) and + (value is not None) and + (bounds_initial is not None) and + not (bounds_initial[0] <= len(value) <= bounds_initial[1]) + ): + value = None + if ( + (bounds is None) and + (default is not None) and + (bounds_initial is not None) and + not (bounds_initial[0] <= len(default) <= bounds_initial[1]) + ): + default = None + obj = obj_initial(value, bounds, impl, expl, default, optional) + if obj.ready: + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + self.assertEqual(obj, value_expected) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + self.assertEqual( + obj.default, + default_initial if default is None else default, + ) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + self.assertEqual( + (obj._bound_min, obj._bound_max), + bounds or bounds_initial or (0, float("+inf")), + ) + + @given(data_strategy()) + def test_copy(self, d): + values = d.draw(string_values_strat(self.text_alphabet())) + obj = self.base_klass(*values) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj._bound_min, obj_copied._bound_min) + self.assertEqual(obj._bound_max, obj_copied._bound_max) + self.assertEqual(obj._value, obj_copied._value) + + @given(data_strategy()) + def test_stripped(self, d): + value = d.draw(text(alphabet=self.text_alphabet())) + tag_impl = tag_encode(d.draw(integers(min_value=1))) + obj = self.base_klass(value, impl=tag_impl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given(data_strategy()) + def test_stripped_expl(self, d): + value = d.draw(text(alphabet=self.text_alphabet())) + tag_expl = tag_ctxc(d.draw(integers(min_value=1))) + obj = self.base_klass(value, expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + self.base_klass().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + self.base_klass().decode( + self.base_klass.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + sets(integers(min_value=0, max_value=10), min_size=2, max_size=2), + integers(min_value=0), + lists(integers()), + ) + def test_invalid_bounds_while_decoding(self, ints, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + value, bound_min = list(sorted(ints)) + + class String(self.base_klass): + # Multiply this value by four, to satisfy UTF-32 bounds + # (4 bytes per character) validation + bounds = (bound_min * 4, bound_min * 4) + with self.assertRaises(DecodeError) as err: + String().decode( + self.base_klass(b"\x00\x00\x00\x00" * value).encode(), + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given(data_strategy()) + def test_symmetric(self, d): + values = d.draw(string_values_strat(self.text_alphabet())) + value = d.draw(text(alphabet=self.text_alphabet())) + tag_expl = tag_ctxc(d.draw(integers(min_value=1))) + offset = d.draw(integers(min_value=0)) + _, _, _, _, default, optional, _decoded = values + obj = self.base_klass( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertNotEqual(obj_decoded, obj) + self.assertEqual(bytes(obj_decoded), bytes(obj_expled)) + self.assertEqual(bytes(obj_decoded), bytes(obj)) + self.assertEqual(text_type(obj_decoded), text_type(obj_expled)) + self.assertEqual(text_type(obj_decoded), text_type(obj)) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + + +class TestUTF8String(StringMixin, CommonMixin, TestCase): + base_klass = UTF8String + + +class TestNumericString(StringMixin, CommonMixin, TestCase): + base_klass = NumericString + + +class TestPrintableString(StringMixin, CommonMixin, TestCase): + base_klass = PrintableString + + +class TestTeletexString(StringMixin, CommonMixin, TestCase): + base_klass = TeletexString + + +class TestVideotexString(StringMixin, CommonMixin, TestCase): + base_klass = VideotexString + + +class TestIA5String(StringMixin, CommonMixin, TestCase): + base_klass = IA5String + + +class TestGraphicString(StringMixin, CommonMixin, TestCase): + base_klass = GraphicString + + +class TestVisibleString(StringMixin, CommonMixin, TestCase): + base_klass = VisibleString + + +class TestGeneralString(StringMixin, CommonMixin, TestCase): + base_klass = GeneralString + + +class TestUniversalString(StringMixin, CommonMixin, TestCase): + base_klass = UniversalString + + +class TestBMPString(StringMixin, CommonMixin, TestCase): + base_klass = BMPString + + +@composite +def generalized_time_values_strat( + draw, + min_datetime, + max_datetime, + omit_ms=False, + do_expl=False, +): + value = None + if draw(booleans()): + value = draw(datetimes(min_value=min_datetime, max_value=max_datetime)) + if omit_ms: + value = value.replace(microsecond=0) + default = None + if draw(booleans()): + default = draw(datetimes(min_value=min_datetime, max_value=max_datetime)) + if omit_ms: + default = default.replace(microsecond=0) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (value, impl, expl, default, optional, _decoded) + + +class TimeMixin(object): + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + self.base_klass(datetime.now().timetuple()) + repr(err.exception) + + @given(data_strategy()) + def test_optional(self, d): + default = d.draw(datetimes( + min_value=self.min_datetime, + max_value=self.max_datetime, + )) + optional = d.draw(booleans()) + obj = self.base_klass(default=default, optional=optional) + self.assertTrue(obj.optional) + + @given(data_strategy()) + def test_ready(self, d): + obj = self.base_klass() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + value = d.draw(datetimes(min_value=self.min_datetime)) + obj = self.base_klass(value) + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + + @given(data_strategy()) + def test_comparison(self, d): + value1 = d.draw(datetimes( + min_value=self.min_datetime, + max_value=self.max_datetime, + )) + value2 = d.draw(datetimes( + min_value=self.min_datetime, + max_value=self.max_datetime, + )) + tag1 = d.draw(binary()) + tag2 = d.draw(binary()) + if self.omit_ms: + value1 = value1.replace(microsecond=0) + value2 = value2.replace(microsecond=0) + obj1 = self.base_klass(value1) + obj2 = self.base_klass(value2) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == obj2.todatetime(), value1 == value2) + self.assertEqual(obj1 == bytes(obj2), value1 == value2) + obj1 = self.base_klass(value1, impl=tag1) + obj2 = self.base_klass(value1, impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + + @given(data_strategy()) + def test_call(self, d): + ( + value_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(generalized_time_values_strat( + min_datetime=self.min_datetime, + max_datetime=self.max_datetime, + omit_ms=self.omit_ms, + )) + obj_initial = self.base_klass( + value=value_initial, + impl=impl_initial, + expl=expl_initial, + default=default_initial, + optional=optional_initial or False, + _decoded=_decoded_initial, + ) + ( + value, + impl, + expl, + default, + optional, + _decoded, + ) = d.draw(generalized_time_values_strat( + min_datetime=self.min_datetime, + max_datetime=self.max_datetime, + omit_ms=self.omit_ms, + do_expl=impl_initial is None, + )) + obj = obj_initial( + value=value, + impl=impl, + expl=expl, + default=default, + optional=optional, + ) + if obj.ready: + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + self.assertEqual(obj, value_expected) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + self.assertEqual( + obj.default, + default_initial if default is None else default, + ) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + + @given(data_strategy()) + def test_copy(self, d): + values = d.draw(generalized_time_values_strat( + min_datetime=self.min_datetime, + max_datetime=self.max_datetime, + )) + obj = self.base_klass(*values) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj._value, obj_copied._value) + + @given(data_strategy()) + def test_stripped(self, d): + value = d.draw(datetimes( + min_value=self.min_datetime, + max_value=self.max_datetime, + )) + tag_impl = tag_encode(d.draw(integers(min_value=1))) + obj = self.base_klass(value, impl=tag_impl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given(data_strategy()) + def test_stripped_expl(self, d): + value = d.draw(datetimes( + min_value=self.min_datetime, + max_value=self.max_datetime, + )) + tag_expl = tag_ctxc(d.draw(integers(min_value=1))) + obj = self.base_klass(value, expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(data_strategy()) + def test_symmetric(self, d): + values = d.draw(generalized_time_values_strat( + min_datetime=self.min_datetime, + max_datetime=self.max_datetime, + )) + value = d.draw(datetimes( + min_value=self.min_datetime, + max_value=self.max_datetime, + )) + tag_expl = tag_ctxc(d.draw(integers(min_value=1))) + offset = d.draw(integers(min_value=0)) + _, _, _, default, optional, _decoded = values + obj = self.base_klass( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertEqual(obj_decoded.todatetime(), obj_expled.todatetime()) + self.assertEqual(obj_decoded.todatetime(), obj.todatetime()) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + + +class TestGeneralizedTime(TimeMixin, CommonMixin, TestCase): + base_klass = GeneralizedTime + omit_ms = False + min_datetime = datetime(1900, 1, 1) + max_datetime = datetime(9999, 12, 31) + + def test_go_vectors_invalid(self): + for data in (( + b"20100102030405", + b"00000100000000Z", + b"20101302030405Z", + b"20100002030405Z", + b"20100100030405Z", + b"20100132030405Z", + b"20100231030405Z", + b"20100102240405Z", + b"20100102036005Z", + b"20100102030460Z", + b"-20100102030410Z", + b"2010-0102030410Z", + b"2010-0002030410Z", + b"201001-02030410Z", + b"20100102-030410Z", + b"2010010203-0410Z", + b"201001020304-10Z", + # These ones are INVALID in *DER*, but accepted + # by Go's encoding/asn1 + b"20100102030405+0607", + b"20100102030405-0607", + )): + with self.assertRaises(DecodeError) as err: + GeneralizedTime(data) + repr(err.exception) + + def test_go_vectors_valid(self): + self.assertEqual( + GeneralizedTime(b"20100102030405Z").todatetime(), + datetime(2010, 1, 2, 3, 4, 5, 0), + ) + + +class TestUTCTime(TimeMixin, CommonMixin, TestCase): + base_klass = UTCTime + omit_ms = True + min_datetime = datetime(2000, 1, 1) + max_datetime = datetime(2049, 12, 31) + + def test_go_vectors_invalid(self): + for data in (( + b"a10506234540Z", + b"91a506234540Z", + b"9105a6234540Z", + b"910506a34540Z", + b"910506334a40Z", + b"91050633444aZ", + b"910506334461Z", + b"910506334400Za", + b"000100000000Z", + b"101302030405Z", + b"100002030405Z", + b"100100030405Z", + b"100132030405Z", + b"100231030405Z", + b"100102240405Z", + b"100102036005Z", + b"100102030460Z", + b"-100102030410Z", + b"10-0102030410Z", + b"10-0002030410Z", + b"1001-02030410Z", + b"100102-030410Z", + b"10010203-0410Z", + b"1001020304-10Z", + # These ones are INVALID in *DER*, but accepted + # by Go's encoding/asn1 + b"910506164540-0700", + b"910506164540+0730", + b"9105062345Z", + b"5105062345Z", + )): + with self.assertRaises(DecodeError) as err: + UTCTime(data) + repr(err.exception) + + def test_go_vectors_valid(self): + self.assertEqual( + UTCTime(b"910506234540Z").todatetime(), + datetime(1991, 5, 6, 23, 45, 40, 0), + ) + + @given(integers(min_value=0, max_value=49)) + def test_pre50(self, year): + self.assertEqual( + UTCTime(("%02d1231235959Z" % year).encode("ascii")).todatetime().year, + 2000 + year, + ) + + @given(integers(min_value=50, max_value=99)) + def test_post50(self, year): + self.assertEqual( + UTCTime(("%02d1231235959Z" % year).encode("ascii")).todatetime().year, + 1900 + year, + ) + + +@composite +def any_values_strat(draw, do_expl=False): + value = draw(one_of(none(), binary())) + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (value, expl, optional, _decoded) + + +class AnyInherited(Any): + __slots__ = () + + +class TestAny(CommonMixin, TestCase): + base_klass = Any + + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + Any(123) + repr(err.exception) + + @given(booleans()) + def test_optional(self, optional): + obj = Any(optional=optional) + self.assertEqual(obj.optional, optional) + + @given(binary()) + def test_ready(self, value): + obj = Any() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + obj = Any(value) + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + + @given(integers()) + def test_basic(self, value): + integer_encoded = Integer(value).encode() + for obj in ( + Any(integer_encoded), + Any(Integer(value)), + Any(Any(Integer(value))), + ): + self.assertSequenceEqual(bytes(obj), integer_encoded) + self.assertEqual( + obj.decode(obj.encode())[0].vlen, + len(integer_encoded), + ) + repr(obj) + pprint(obj) + self.assertSequenceEqual(obj.encode(), integer_encoded) + + @given(binary(), binary()) + def test_comparison(self, value1, value2): + for klass in (Any, AnyInherited): + obj1 = klass(value1) + obj2 = klass(value2) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == bytes(obj2), value1 == value2) + + @given(data_strategy()) + def test_call(self, d): + for klass in (Any, AnyInherited): + ( + value_initial, + expl_initial, + optional_initial, + _decoded_initial, + ) = d.draw(any_values_strat()) + obj_initial = klass( + value_initial, + expl_initial, + optional_initial or False, + _decoded_initial, + ) + ( + value, + expl, + optional, + _decoded, + ) = d.draw(any_values_strat(do_expl=True)) + obj = obj_initial(value, expl, optional) + if obj.ready: + value_expected = None if value is None else value + self.assertEqual(obj, value_expected) + self.assertEqual(obj.expl_tag, expl or expl_initial) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + self.assertEqual(obj.optional, optional) + + def test_simultaneous_impl_expl(self): + # override it, as Any does not have implicit tag + pass + + def test_decoded(self): + # override it, as Any does not have implicit tag + pass + + @given(any_values_strat()) + def test_copy(self, values): + for klass in (Any, AnyInherited): + obj = klass(*values) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj._value, obj_copied._value) + + @given(binary().map(OctetString)) + def test_stripped(self, value): + obj = Any(value) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + binary(), + integers(min_value=1).map(tag_ctxc), + ) + def test_stripped_expl(self, value, tag_expl): + obj = Any(value, expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Any().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + Any().decode( + Any.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + any_values_strat(), + integers().map(lambda x: Integer(x).encode()), + integers(min_value=1).map(tag_ctxc), + integers(min_value=0), + ) + def test_symmetric(self, values, value, tag_expl, offset): + for klass in (Any, AnyInherited): + _, _, optional, _decoded = values + obj = klass(value=value, optional=optional, _decoded=_decoded) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertEqual(bytes(obj_decoded), bytes(obj_expled)) + self.assertEqual(bytes(obj_decoded), bytes(obj)) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + self.assertEqual(obj_decoded.tlen, 0) + self.assertEqual(obj_decoded.llen, 0) + self.assertEqual(obj_decoded.vlen, len(value)) + + +@composite +def choice_values_strat(draw, value_required=False, schema=None, do_expl=False): + if schema is None: + names = list(draw(sets(text_letters(), min_size=1, max_size=5))) + tags = [tag_encode(tag) for tag in draw(sets( + integers(min_value=0), + min_size=len(names), + max_size=len(names), + ))] + schema = [(name, Integer(impl=tag)) for name, tag in zip(names, tags)] + value = None + if value_required or draw(booleans()): + value = draw(tuples( + sampled_from([name for name, _ in schema]), + integers().map(Integer), + )) + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + default = draw(one_of( + none(), + tuples(sampled_from([name for name, _ in schema]), integers().map(Integer)), + )) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (schema, value, expl, default, optional, _decoded) + + +class ChoiceInherited(Choice): + __slots__ = () + + +class TestChoice(CommonMixin, TestCase): + class Wahl(Choice): + schema = (("whatever", Boolean()),) + base_klass = Wahl + + def test_schema_required(self): + with assertRaisesRegex(self, ValueError, "schema must be specified"): + Choice() + + def test_impl_forbidden(self): + with assertRaisesRegex(self, ValueError, "no implicit tag allowed"): + Choice(impl=b"whatever") + + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + self.base_klass(123) + repr(err.exception) + with self.assertRaises(ObjUnknown) as err: + self.base_klass(("whenever", Boolean(False))) + repr(err.exception) + with self.assertRaises(InvalidValueType) as err: + self.base_klass(("whatever", Integer(123))) + repr(err.exception) + + @given(booleans()) + def test_optional(self, optional): + obj = self.base_klass( + default=self.base_klass(("whatever", Boolean(False))), + optional=optional, + ) + self.assertTrue(obj.optional) + + @given(booleans()) + def test_ready(self, value): + obj = self.base_klass() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + self.assertIsNone(obj["whatever"]) + with self.assertRaises(ObjNotReady) as err: + obj.encode() + repr(err.exception) + obj["whatever"] = Boolean() + self.assertFalse(obj.ready) + repr(obj) + pprint(obj) + obj["whatever"] = Boolean(value) + self.assertTrue(obj.ready) + repr(obj) + pprint(obj) + + @given(booleans(), booleans()) + def test_comparison(self, value1, value2): + class WahlInherited(self.base_klass): + __slots__ = () + for klass in (self.base_klass, WahlInherited): + obj1 = klass(("whatever", Boolean(value1))) + obj2 = klass(("whatever", Boolean(value2))) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == obj2._value, value1 == value2) + self.assertFalse(obj1 == obj2._value[1]) + + @given(data_strategy()) + def test_call(self, d): + for klass in (Choice, ChoiceInherited): + ( + schema_initial, + value_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(choice_values_strat()) + + class Wahl(klass): + __slots__ = () + schema = schema_initial + obj_initial = Wahl( + value=value_initial, + expl=expl_initial, + default=default_initial, + optional=optional_initial or False, + _decoded=_decoded_initial, + ) + ( + _, + value, + expl, + default, + optional, + _decoded, + ) = d.draw(choice_values_strat(schema=schema_initial, do_expl=True)) + obj = obj_initial(value, expl, default, optional) + if obj.ready: + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + self.assertEqual(obj.choice, value_expected[0]) + self.assertEqual(obj.value, int(value_expected[1])) + self.assertEqual(obj.expl_tag, expl or expl_initial) + default_expect = default_initial if default is None else default + if default_expect is not None: + self.assertEqual(obj.default.choice, default_expect[0]) + self.assertEqual(obj.default.value, int(default_expect[1])) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + self.assertEqual(obj.specs, obj_initial.specs) + + def test_simultaneous_impl_expl(self): + # override it, as Any does not have implicit tag + pass + + def test_decoded(self): + # override it, as Any does not have implicit tag + pass + + @given(choice_values_strat()) + def test_copy(self, values): + _schema, value, expl, default, optional, _decoded = values + + class Wahl(self.base_klass): + __slots__ = () + schema = _schema + obj = Wahl( + value=value, + expl=expl, + default=default, + optional=optional or False, + _decoded=_decoded, + ) + obj_copied = obj.copy() + self.assertIsNone(obj.tag) + self.assertIsNone(obj_copied.tag) + # hack for assert_copied_basic_fields + obj.tag = "whatever" + obj_copied.tag = "whatever" + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj._value, obj_copied._value) + self.assertEqual(obj.specs, obj_copied.specs) + + @given(booleans()) + def test_stripped(self, value): + obj = self.base_klass(("whatever", Boolean(value))) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + booleans(), + integers(min_value=1).map(tag_ctxc), + ) + def test_stripped_expl(self, value, tag_expl): + obj = self.base_klass(("whatever", Boolean(value)), expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(data_strategy()) + def test_symmetric(self, d): + _schema, value, _, default, optional, _decoded = d.draw( + choice_values_strat(value_required=True) + ) + tag_expl = tag_ctxc(d.draw(integers(min_value=1))) + offset = d.draw(integers(min_value=0)) + + class Wahl(self.base_klass): + __slots__ = () + schema = _schema + obj = Wahl( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self.assertEqual(obj_decoded, obj_expled) + self.assertEqual(obj_decoded.choice, obj_expled.choice) + self.assertEqual(obj_decoded.value, obj_expled.value) + self.assertEqual(obj_decoded.choice, obj.choice) + self.assertEqual(obj_decoded.value, obj.value) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + self.assertSequenceEqual( + obj_expled_encoded[ + obj_decoded.value.offset - offset: + obj_decoded.value.offset + obj_decoded.value.tlvlen - offset + ], + obj_encoded, + ) + + @given(integers()) + def test_set_get(self, value): + class Wahl(Choice): + schema = ( + ("erste", Boolean()), + ("zweite", Integer()), + ) + obj = Wahl() + with self.assertRaises(ObjUnknown) as err: + obj["whatever"] = "whenever" + with self.assertRaises(InvalidValueType) as err: + obj["zweite"] = Boolean(False) + obj["zweite"] = Integer(value) + repr(err.exception) + with self.assertRaises(ObjUnknown) as err: + obj["whatever"] + repr(err.exception) + self.assertIsNone(obj["erste"]) + self.assertEqual(obj["zweite"], Integer(value)) + + def test_tag_mismatch(self): + class Wahl(Choice): + schema = ( + ("erste", Boolean()), + ) + int_encoded = Integer(123).encode() + bool_encoded = Boolean(False).encode() + obj = Wahl() + obj.decode(bool_encoded) + with self.assertRaises(TagMismatch): + obj.decode(int_encoded) + + +@composite +def seq_values_strat(draw, seq_klass, do_expl=False): + value = None + if draw(booleans()): + value = seq_klass() + value._value = { + k: v for k, v in draw(dictionaries( + integers(), + one_of( + booleans().map(Boolean), + integers().map(Integer), + ), + )).items() + } + schema = None + if draw(booleans()): + schema = list(draw(dictionaries( + integers(), + one_of( + booleans().map(Boolean), + integers().map(Integer), + ), + )).items()) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + default = None + if draw(booleans()): + default = seq_klass() + default._value = { + k: v for k, v in draw(dictionaries( + integers(), + one_of( + booleans().map(Boolean), + integers().map(Integer), + ), + )).items() + } + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return (value, schema, impl, expl, default, optional, _decoded) + + +@composite +def sequence_strat(draw, seq_klass): + inputs = draw(lists( + one_of( + tuples(just(Boolean), booleans(), one_of(none(), booleans())), + tuples(just(Integer), integers(), one_of(none(), integers())), + ), + max_size=6, + )) + tags = draw(sets( + integers(min_value=1), + min_size=len(inputs), + max_size=len(inputs), + )) + inits = [ + ({"expl": tag_ctxc(tag)} if expled else {"impl": tag_encode(tag)}) + for tag, expled in zip(tags, draw(lists( + booleans(), + min_size=len(inputs), + max_size=len(inputs), + ))) + ] + empties = [] + for i, optional in enumerate(draw(lists( + sampled_from(("required", "optional", "empty")), + min_size=len(inputs), + max_size=len(inputs), + ))): + if optional in ("optional", "empty"): + inits[i]["optional"] = True + if optional == "empty": + empties.append(i) + empties = set(empties) + names = list(draw(sets( + text_printable, + min_size=len(inputs), + max_size=len(inputs), + ))) + schema = [] + for i, (klass, value, default) in enumerate(inputs): + schema.append((names[i], klass(default=default, **inits[i]))) + seq_name = draw(text_letters()) + Seq = type(seq_name, (seq_klass,), {"__slots__": (), "schema": tuple(schema)}) + seq = Seq() + expects = [] + for i, (klass, value, default) in enumerate(inputs): + name = names[i] + _, spec = schema[i] + expect = { + "name": name, + "optional": False, + "presented": False, + "default_value": None if spec.default is None else default, + "value": None, + } + if i in empties: + expect["optional"] = True + else: + expect["presented"] = True + expect["value"] = value + if spec.optional: + expect["optional"] = True + if default is not None and default == value: + expect["presented"] = False + seq[name] = klass(value) + expects.append(expect) + return seq, expects + + +@composite +def sequences_strat(draw, seq_klass): + tags = draw(sets(integers(min_value=1), min_size=0, max_size=5)) + inits = [ + ({"expl": tag_ctxc(tag)} if expled else {"impl": tag_encode(tag)}) + for tag, expled in zip(tags, draw(lists( + booleans(), + min_size=len(tags), + max_size=len(tags), + ))) + ] + defaulted = set( + i for i, is_default in enumerate(draw(lists( + booleans(), + min_size=len(tags), + max_size=len(tags), + ))) if is_default + ) + names = list(draw(sets( + text_printable, + min_size=len(tags), + max_size=len(tags), + ))) + seq_expectses = draw(lists( + sequence_strat(seq_klass=seq_klass), + min_size=len(tags), + max_size=len(tags), + )) + seqs = [seq for seq, _ in seq_expectses] + schema = [] + for i, (name, seq) in enumerate(zip(names, seqs)): + schema.append(( + name, + seq(default=(seq if i in defaulted else None), **inits[i]), + )) + seq_name = draw(text_letters()) + Seq = type(seq_name, (seq_klass,), {"__slots__": (), "schema": tuple(schema)}) + seq_outer = Seq() + expect_outers = [] + for name, (seq_inner, expects_inner) in zip(names, seq_expectses): + expect = { + "name": name, + "expects": expects_inner, + "presented": False, + } + seq_outer[name] = seq_inner + if seq_outer.specs[name].default is None: + expect["presented"] = True + expect_outers.append(expect) + return seq_outer, expect_outers + + +class SeqMixing(object): + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + self.base_klass((1, 2, 3)) + repr(err.exception) + + def test_invalid_value_type_set(self): + class Seq(self.base_klass): + __slots__ = () + schema = (("whatever", Boolean()),) + seq = Seq() + with self.assertRaises(InvalidValueType) as err: + seq["whatever"] = Integer(123) + repr(err.exception) + + @given(booleans()) + def test_optional(self, optional): + obj = self.base_klass(default=self.base_klass(), optional=optional) + self.assertTrue(obj.optional) + + @given(data_strategy()) + def test_ready(self, d): + ready = { + str(i): v for i, v in enumerate(d.draw(lists( + booleans(), + min_size=1, + max_size=3, + ))) + } + non_ready = { + str(i + len(ready)): v for i, v in enumerate(d.draw(lists( + booleans(), + min_size=1, + max_size=3, + ))) + } + schema_input = [] + for name in d.draw(permutations( + list(ready.keys()) + list(non_ready.keys()), + )): + schema_input.append((name, Boolean())) + + class Seq(self.base_klass): + __slots__ = () + schema = tuple(schema_input) + seq = Seq() + for name in ready.keys(): + seq[name] + seq[name] = Boolean() + self.assertFalse(seq.ready) + repr(seq) + pprint(seq) + for name, value in ready.items(): + seq[name] = Boolean(value) + self.assertFalse(seq.ready) + repr(seq) + pprint(seq) + with self.assertRaises(ObjNotReady) as err: + seq.encode() + repr(err.exception) + for name, value in non_ready.items(): + seq[name] = Boolean(value) + self.assertTrue(seq.ready) + repr(seq) + pprint(seq) + + @given(data_strategy()) + def test_call(self, d): + class SeqInherited(self.base_klass): + __slots__ = () + for klass in (self.base_klass, SeqInherited): + ( + value_initial, + schema_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(seq_values_strat(seq_klass=klass)) + obj_initial = klass( + value_initial, + schema_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial or False, + _decoded_initial, + ) + ( + value, + _, + impl, + expl, + default, + optional, + _decoded, + ) = d.draw(seq_values_strat( + seq_klass=klass, + do_expl=impl_initial is None, + )) + obj = obj_initial(value, impl, expl, default, optional) + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + self.assertEqual(obj._value, getattr(value_expected, "_value", {})) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + self.assertEqual( + {} if obj.default is None else obj.default._value, + getattr(default_initial if default is None else default, "_value", {}), + ) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(list(obj.specs.items()), schema_initial or []) + self.assertEqual(obj.optional, optional) + + @given(data_strategy()) + def test_copy(self, d): + class SeqInherited(self.base_klass): + __slots__ = () + for klass in (self.base_klass, SeqInherited): + values = d.draw(seq_values_strat(seq_klass=klass)) + obj = klass(*values) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj.specs, obj_copied.specs) + self.assertEqual(obj._value, obj_copied._value) + + @given(data_strategy()) + def test_stripped(self, d): + value = d.draw(integers()) + tag_impl = tag_encode(d.draw(integers(min_value=1))) + + class Seq(self.base_klass): + __slots__ = () + impl = tag_impl + schema = (("whatever", Integer()),) + seq = Seq() + seq["whatever"] = Integer(value) + with self.assertRaises(NotEnoughData): + seq.decode(seq.encode()[:-1]) + + @given(data_strategy()) + def test_stripped_expl(self, d): + value = d.draw(integers()) + tag_expl = tag_ctxc(d.draw(integers(min_value=1))) + + class Seq(self.base_klass): + __slots__ = () + expl = tag_expl + schema = (("whatever", Integer()),) + seq = Seq() + seq["whatever"] = Integer(value) + with self.assertRaises(NotEnoughData): + seq.decode(seq.encode()[:-1]) + + @given(binary(min_size=2)) + def test_non_tag_mismatch_raised(self, junk): + try: + _, _, len_encoded = tag_strip(memoryview(junk)) + len_decode(len_encoded) + except Exception: + assume(True) + else: + assume(False) + + class Seq(self.base_klass): + __slots__ = () + schema = ( + ("whatever", Integer()), + ("junk", Any()), + ("whenever", Integer()), + ) + seq = Seq() + seq["whatever"] = Integer(123) + seq["junk"] = Any(junk) + seq["whenever"] = Integer(123) + with self.assertRaises(DecodeError): + seq.decode(seq.encode()) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + self.base_klass().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + self.base_klass().decode( + self.base_klass.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + def _assert_expects(self, seq, expects): + for expect in expects: + self.assertEqual( + seq.specs[expect["name"]].optional, + expect["optional"], + ) + if expect["default_value"] is not None: + self.assertEqual( + seq.specs[expect["name"]].default, + expect["default_value"], + ) + if expect["presented"]: + self.assertIn(expect["name"], seq) + self.assertEqual(seq[expect["name"]], expect["value"]) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(data_strategy()) + def test_symmetric(self, d): + seq, expects = d.draw(sequence_strat(seq_klass=self.base_klass)) + self.assertTrue(seq.ready) + self.assertFalse(seq.decoded) + self._assert_expects(seq, expects) + repr(seq) + pprint(seq) + seq_encoded = seq.encode() + seq_decoded, tail = seq.decode(seq_encoded) + self.assertEqual(tail, b"") + self.assertTrue(seq.ready) + self._assert_expects(seq_decoded, expects) + self.assertEqual(seq, seq_decoded) + self.assertEqual(seq_decoded.encode(), seq_encoded) + for expect in expects: + if not expect["presented"]: + self.assertNotIn(expect["name"], seq_decoded) + continue + self.assertIn(expect["name"], seq_decoded) + obj = seq_decoded[expect["name"]] + self.assertTrue(obj.decoded) + offset = obj.expl_offset if obj.expled else obj.offset + tlvlen = obj.expl_tlvlen if obj.expled else obj.tlvlen + self.assertSequenceEqual( + seq_encoded[offset:offset + tlvlen], + obj.encode(), + ) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(data_strategy()) + def test_symmetric_with_seq(self, d): + seq, expect_outers = d.draw(sequences_strat(seq_klass=self.base_klass)) + self.assertTrue(seq.ready) + seq_encoded = seq.encode() + seq_decoded, tail = seq.decode(seq_encoded) + self.assertEqual(tail, b"") + self.assertTrue(seq.ready) + self.assertEqual(seq, seq_decoded) + self.assertEqual(seq_decoded.encode(), seq_encoded) + for expect_outer in expect_outers: + if not expect_outer["presented"]: + self.assertNotIn(expect_outer["name"], seq_decoded) + continue + self.assertIn(expect_outer["name"], seq_decoded) + obj = seq_decoded[expect_outer["name"]] + self.assertTrue(obj.decoded) + offset = obj.expl_offset if obj.expled else obj.offset + tlvlen = obj.expl_tlvlen if obj.expled else obj.tlvlen + self.assertSequenceEqual( + seq_encoded[offset:offset + tlvlen], + obj.encode(), + ) + self._assert_expects(obj, expect_outer["expects"]) + + @given(data_strategy()) + def test_default_disappears(self, d): + _schema = list(d.draw(dictionaries( + text_letters(), + sets(integers(), min_size=2, max_size=2), + min_size=1, + )).items()) + + class Seq(self.base_klass): + __slots__ = () + schema = [ + (n, Integer(default=d)) + for n, (_, d) in _schema + ] + seq = Seq() + for name, (value, _) in _schema: + seq[name] = Integer(value) + self.assertEqual(len(seq._value), len(_schema)) + empty_seq = b"".join((self.base_klass.tag_default, len_encode(0))) + self.assertGreater(len(seq.encode()), len(empty_seq)) + for name, (_, default) in _schema: + seq[name] = Integer(default) + self.assertEqual(len(seq._value), 0) + self.assertSequenceEqual(seq.encode(), empty_seq) + + @given(data_strategy()) + def test_encoded_default_accepted(self, d): + _schema = list(d.draw(dictionaries( + text_letters(), + integers(), + min_size=1, + )).items()) + tags = [tag_encode(tag) for tag in d.draw(sets( + integers(min_value=0), + min_size=len(_schema), + max_size=len(_schema), + ))] + + class SeqWithoutDefault(self.base_klass): + __slots__ = () + schema = [ + (n, Integer(impl=t)) + for (n, _), t in zip(_schema, tags) + ] + seq_without_default = SeqWithoutDefault() + for name, value in _schema: + seq_without_default[name] = Integer(value) + seq_encoded = seq_without_default.encode() + + class SeqWithDefault(self.base_klass): + __slots__ = () + schema = [ + (n, Integer(default=v, impl=t)) + for (n, v), t in zip(_schema, tags) + ] + seq_with_default = SeqWithDefault() + seq_decoded, _ = seq_with_default.decode(seq_encoded) + for name, value in _schema: + self.assertEqual(seq_decoded[name], seq_with_default[name]) + self.assertEqual(seq_decoded[name], value) + + @given(data_strategy()) + def test_missing_from_spec(self, d): + names = list(d.draw(sets(text_letters(), min_size=2))) + tags = [tag_encode(tag) for tag in d.draw(sets( + integers(min_value=0), + min_size=len(names), + max_size=len(names), + ))] + names_tags = [(name, tag) for tag, name in sorted(zip(tags, names))] + + class SeqFull(self.base_klass): + __slots__ = () + schema = [(n, Integer(impl=t)) for n, t in names_tags] + seq_full = SeqFull() + for i, name in enumerate(names): + seq_full[name] = Integer(i) + seq_encoded = seq_full.encode() + altered = names_tags[:-2] + names_tags[-1:] + + class SeqMissing(self.base_klass): + __slots__ = () + schema = [(n, Integer(impl=t)) for n, t in altered] + seq_missing = SeqMissing() + with self.assertRaises(TagMismatch): + seq_missing.decode(seq_encoded) + + +class TestSequence(SeqMixing, CommonMixin, TestCase): + base_klass = Sequence + + @given( + integers(), + binary(min_size=1), + ) + def test_remaining(self, value, junk): + class Seq(Sequence): + __slots__ = () + schema = ( + ("whatever", Integer()), + ) + int_encoded = Integer(value).encode() + junked = b"".join(( + Sequence.tag_default, + len_encode(len(int_encoded + junk)), + int_encoded + junk, + )) + with assertRaisesRegex(self, DecodeError, "remaining"): + Seq().decode(junked) + + @given(sets(text_letters(), min_size=2)) + def test_obj_unknown(self, names): + missing = names.pop() + + class Seq(Sequence): + __slots__ = () + schema = [(n, Boolean()) for n in names] + seq = Seq() + with self.assertRaises(ObjUnknown) as err: + seq[missing] + repr(err.exception) + with self.assertRaises(ObjUnknown) as err: + seq[missing] = Boolean() + repr(err.exception) + + +class TestSet(SeqMixing, CommonMixin, TestCase): + base_klass = Set + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(data_strategy()) + def test_sorted(self, d): + tags = [ + tag_encode(tag) for tag in + d.draw(sets(integers(min_value=1), min_size=1, max_size=10)) + ] + + class Seq(Set): + __slots__ = () + schema = [(str(i), OctetString(impl=t)) for i, t in enumerate(tags)] + seq = Seq() + for name, _ in Seq.schema: + seq[name] = OctetString(b"") + seq_encoded = seq.encode() + seq_decoded, _ = seq.decode(seq_encoded) + self.assertSequenceEqual( + seq_encoded[seq_decoded.tlen + seq_decoded.llen:], + b"".join(sorted([seq[name].encode() for name, _ in Seq.schema])), + ) + + +@composite +def seqof_values_strat(draw, schema=None, do_expl=False): + if schema is None: + schema = draw(sampled_from((Boolean(), Integer()))) + bound_min, bound_max = sorted(draw(sets( + integers(min_value=0, max_value=10), + min_size=2, + max_size=2, + ))) + if isinstance(schema, Boolean): + values_generator = booleans().map(Boolean) + elif isinstance(schema, Integer): + values_generator = integers().map(Integer) + values_generator = lists( + values_generator, + min_size=bound_min, + max_size=bound_max, + ) + values = draw(one_of(none(), values_generator)) + impl = None + expl = None + if do_expl: + expl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + else: + impl = draw(one_of(none(), integers(min_value=1).map(tag_encode))) + default = draw(one_of(none(), values_generator)) + optional = draw(one_of(none(), booleans())) + _decoded = ( + draw(integers(min_value=0)), + draw(integers(min_value=0)), + draw(integers(min_value=0)), + ) + return ( + schema, + values, + (bound_min, bound_max), + impl, + expl, + default, + optional, + _decoded, + ) + + +class SeqOfMixing(object): + def test_invalid_value_type(self): + with self.assertRaises(InvalidValueType) as err: + self.base_klass(123) + repr(err.exception) + + def test_invalid_values_type(self): + class SeqOf(self.base_klass): + __slots__ = () + schema = Integer() + with self.assertRaises(InvalidValueType) as err: + SeqOf([Integer(123), Boolean(False), Integer(234)]) + repr(err.exception) + + def test_schema_required(self): + with assertRaisesRegex(self, ValueError, "schema must be specified"): + self.base_klass.__mro__[1]() + + @given(booleans(), booleans(), binary(), binary()) + def test_comparison(self, value1, value2, tag1, tag2): + class SeqOf(self.base_klass): + __slots__ = () + schema = Boolean() + obj1 = SeqOf([Boolean(value1)]) + obj2 = SeqOf([Boolean(value2)]) + self.assertEqual(obj1 == obj2, value1 == value2) + self.assertEqual(obj1 == list(obj2), value1 == value2) + self.assertEqual(obj1 == tuple(obj2), value1 == value2) + obj1 = SeqOf([Boolean(value1)], impl=tag1) + obj2 = SeqOf([Boolean(value1)], impl=tag2) + self.assertEqual(obj1 == obj2, tag1 == tag2) + + @given(lists(booleans())) + def test_iter(self, values): + class SeqOf(self.base_klass): + __slots__ = () + schema = Boolean() + obj = SeqOf([Boolean(value) for value in values]) + self.assertEqual(len(obj), len(values)) + for i, value in enumerate(obj): + self.assertEqual(value, values[i]) + + @given(data_strategy()) + def test_ready(self, d): + ready = [Integer(v) for v in d.draw(lists( + integers(), + min_size=1, + max_size=3, + ))] + non_ready = [ + Integer() for _ in + range(d.draw(integers(min_value=1, max_value=5))) + ] + + class SeqOf(self.base_klass): + __slots__ = () + schema = Integer() + values = d.draw(permutations(ready + non_ready)) + seqof = SeqOf() + for value in values: + seqof.append(value) + self.assertFalse(seqof.ready) + repr(seqof) + pprint(seqof) + with self.assertRaises(ObjNotReady) as err: + seqof.encode() + repr(err.exception) + for i, value in enumerate(values): + self.assertEqual(seqof[i], value) + if not seqof[i].ready: + seqof[i] = Integer(i) + self.assertTrue(seqof.ready) + repr(seqof) + pprint(seqof) + + def test_spec_mismatch(self): + class SeqOf(self.base_klass): + __slots__ = () + schema = Integer() + seqof = SeqOf() + seqof.append(Integer(123)) + with self.assertRaises(ValueError): + seqof.append(Boolean(False)) + with self.assertRaises(ValueError): + seqof[0] = Boolean(False) + + @given(data_strategy()) + def test_bounds_satisfied(self, d): + class SeqOf(self.base_klass): + __slots__ = () + schema = Boolean() + bound_min = d.draw(integers(min_value=0, max_value=1 << 7)) + bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7)) + value = [Boolean()] * d.draw(integers(min_value=bound_min, max_value=bound_max)) + SeqOf(value=value, bounds=(bound_min, bound_max)) + + @given(data_strategy()) + def test_bounds_unsatisfied(self, d): + class SeqOf(self.base_klass): + __slots__ = () + schema = Boolean() + bound_min = d.draw(integers(min_value=1, max_value=1 << 7)) + bound_max = d.draw(integers(min_value=bound_min, max_value=1 << 7)) + value = [Boolean()] * d.draw(integers(max_value=bound_min - 1)) + with self.assertRaises(BoundsError) as err: + SeqOf(value=value, bounds=(bound_min, bound_max)) + repr(err.exception) + value = [Boolean()] * d.draw(integers( + min_value=bound_max + 1, + max_value=bound_max + 10, + )) + with self.assertRaises(BoundsError) as err: + SeqOf(value=value, bounds=(bound_min, bound_max)) + repr(err.exception) + + @given(integers(min_value=1, max_value=10)) + def test_out_of_bounds(self, bound_max): + class SeqOf(self.base_klass): + __slots__ = () + schema = Integer() + bounds = (0, bound_max) + seqof = SeqOf() + for _ in range(bound_max): + seqof.append(Integer(123)) + with self.assertRaises(BoundsError): + seqof.append(Integer(123)) + + @given(data_strategy()) + def test_call(self, d): + ( + schema_initial, + value_initial, + bounds_initial, + impl_initial, + expl_initial, + default_initial, + optional_initial, + _decoded_initial, + ) = d.draw(seqof_values_strat()) + + class SeqOf(self.base_klass): + __slots__ = () + schema = schema_initial + obj_initial = SeqOf( + value=value_initial, + bounds=bounds_initial, + impl=impl_initial, + expl=expl_initial, + default=default_initial, + optional=optional_initial or False, + _decoded=_decoded_initial, + ) + ( + _, + value, + bounds, + impl, + expl, + default, + optional, + _decoded, + ) = d.draw(seqof_values_strat( + schema=schema_initial, + do_expl=impl_initial is None, + )) + if (default is None) and (obj_initial.default is not None): + bounds = None + if ( + (bounds is None) and + (value is not None) and + (bounds_initial is not None) and + not (bounds_initial[0] <= len(value) <= bounds_initial[1]) + ): + value = None + if ( + (bounds is None) and + (default is not None) and + (bounds_initial is not None) and + not (bounds_initial[0] <= len(default) <= bounds_initial[1]) + ): + default = None + obj = obj_initial( + value=value, + bounds=bounds, + impl=impl, + expl=expl, + default=default, + optional=optional, + ) + if obj.ready: + value_expected = default if value is None else value + value_expected = ( + default_initial if value_expected is None + else value_expected + ) + value_expected = () if value_expected is None else value_expected + self.assertEqual(obj, value_expected) + self.assertEqual(obj.tag, impl or impl_initial or obj.tag_default) + self.assertEqual(obj.expl_tag, expl or expl_initial) + self.assertEqual( + obj.default, + default_initial if default is None else default, + ) + if obj.default is None: + optional = optional_initial if optional is None else optional + optional = False if optional is None else optional + else: + optional = True + self.assertEqual(obj.optional, optional) + self.assertEqual( + (obj._bound_min, obj._bound_max), + bounds or bounds_initial or (0, float("+inf")), + ) + + @given(seqof_values_strat()) + def test_copy(self, values): + _schema, value, bounds, impl, expl, default, optional, _decoded = values + + class SeqOf(self.base_klass): + __slots__ = () + schema = _schema + obj = SeqOf( + value=value, + bounds=bounds, + impl=impl, + expl=expl, + default=default, + optional=optional or False, + _decoded=_decoded, + ) + obj_copied = obj.copy() + self.assert_copied_basic_fields(obj, obj_copied) + self.assertEqual(obj._bound_min, obj_copied._bound_min) + self.assertEqual(obj._bound_max, obj_copied._bound_max) + self.assertEqual(obj._value, obj_copied._value) + + @given( + lists(binary()), + integers(min_value=1).map(tag_encode), + ) + def test_stripped(self, values, tag_impl): + class SeqOf(self.base_klass): + __slots__ = () + schema = OctetString() + obj = SeqOf([OctetString(v) for v in values], impl=tag_impl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + lists(binary()), + integers(min_value=1).map(tag_ctxc), + ) + def test_stripped_expl(self, values, tag_expl): + class SeqOf(self.base_klass): + __slots__ = () + schema = OctetString() + obj = SeqOf([OctetString(v) for v in values], expl=tag_expl) + with self.assertRaises(NotEnoughData): + obj.decode(obj.encode()[:-1]) + + @given( + integers(min_value=31), + integers(min_value=0), + lists(integers()), + ) + def test_bad_tag(self, tag, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + self.base_klass().decode( + tag_encode(tag)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given( + integers(min_value=128), + integers(min_value=0), + lists(integers()), + ) + def test_bad_len(self, l, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + with self.assertRaises(DecodeError) as err: + self.base_klass().decode( + self.base_klass.tag_default + len_encode(l)[:-1], + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + @given(binary(min_size=1)) + def test_tag_mismatch(self, impl): + assume(impl != self.base_klass.tag_default) + with self.assertRaises(TagMismatch): + self.base_klass(impl=impl).decode(self.base_klass().encode()) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given( + seqof_values_strat(schema=Integer()), + lists(integers().map(Integer)), + integers(min_value=1).map(tag_ctxc), + integers(min_value=0), + ) + def test_symmetric(self, values, value, tag_expl, offset): + _, _, _, _, _, default, optional, _decoded = values + + class SeqOf(self.base_klass): + __slots__ = () + schema = Integer() + obj = SeqOf( + value=value, + default=default, + optional=optional, + _decoded=_decoded, + ) + repr(obj) + pprint(obj) + self.assertFalse(obj.expled) + obj_encoded = obj.encode() + obj_expled = obj(value, expl=tag_expl) + self.assertTrue(obj_expled.expled) + repr(obj_expled) + pprint(obj_expled) + obj_expled_encoded = obj_expled.encode() + obj_decoded, tail = obj_expled.decode(obj_expled_encoded, offset=offset) + repr(obj_decoded) + pprint(obj_decoded) + self.assertEqual(tail, b"") + self._test_symmetric_compare_objs(obj_decoded, obj_expled) + self.assertSequenceEqual(obj_decoded.encode(), obj_expled_encoded) + self.assertSequenceEqual(obj_decoded.expl_tag, tag_expl) + self.assertEqual(obj_decoded.expl_tlen, len(tag_expl)) + self.assertEqual( + obj_decoded.expl_llen, + len(len_encode(len(obj_encoded))), + ) + self.assertEqual(obj_decoded.tlvlen, len(obj_encoded)) + self.assertEqual(obj_decoded.expl_vlen, len(obj_encoded)) + self.assertEqual( + obj_decoded.offset, + offset + obj_decoded.expl_tlen + obj_decoded.expl_llen, + ) + self.assertEqual(obj_decoded.expl_offset, offset) + for obj_inner in obj_decoded: + self.assertIn(obj_inner, obj_decoded) + self.assertSequenceEqual( + obj_inner.encode(), + obj_expled_encoded[ + obj_inner.offset - offset: + obj_inner.offset + obj_inner.tlvlen - offset + ], + ) + + +class TestSequenceOf(SeqOfMixing, CommonMixin, TestCase): + class SeqOf(SequenceOf): + __slots__ = () + schema = "whatever" + base_klass = SeqOf + + def _test_symmetric_compare_objs(self, obj1, obj2): + self.assertEqual(obj1, obj2) + self.assertSequenceEqual(list(obj1), list(obj2)) + + +class TestSetOf(SeqOfMixing, CommonMixin, TestCase): + class SeqOf(SetOf): + __slots__ = () + schema = "whatever" + base_klass = SeqOf + + def _test_symmetric_compare_objs(self, obj1, obj2): + self.assertSetEqual( + set(int(v) for v in obj1), + set(int(v) for v in obj2), + ) + + @settings(max_examples=LONG_TEST_MAX_EXAMPLES) + @given(data_strategy()) + def test_sorted(self, d): + values = [OctetString(v) for v in d.draw(lists(binary()))] + + class Seq(SetOf): + __slots__ = () + schema = OctetString() + seq = Seq(values) + seq_encoded = seq.encode() + seq_decoded, _ = seq.decode(seq_encoded) + self.assertSequenceEqual( + seq_encoded[seq_decoded.tlen + seq_decoded.llen:], + b"".join(sorted([v.encode() for v in values])), + ) + + +class TestGoMarshalVectors(TestCase): + def runTest(self): + self.assertSequenceEqual(Integer(10).encode(), hexdec("02010a")) + self.assertSequenceEqual(Integer(127).encode(), hexdec("02017f")) + self.assertSequenceEqual(Integer(128).encode(), hexdec("02020080")) + self.assertSequenceEqual(Integer(-128).encode(), hexdec("020180")) + self.assertSequenceEqual(Integer(-129).encode(), hexdec("0202ff7f")) + + class Seq(Sequence): + __slots__ = () + schema = ( + ("erste", Integer()), + ("zweite", Integer(optional=True)) + ) + seq = Seq() + seq["erste"] = Integer(64) + self.assertSequenceEqual(seq.encode(), hexdec("3003020140")) + seq["erste"] = Integer(0x123456) + self.assertSequenceEqual(seq.encode(), hexdec("30050203123456")) + seq["erste"] = Integer(64) + seq["zweite"] = Integer(65) + self.assertSequenceEqual(seq.encode(), hexdec("3006020140020141")) + + class NestedSeq(Sequence): + __slots__ = () + schema = ( + ("nest", Seq()), + ) + seq["erste"] = Integer(127) + seq["zweite"] = None + nested = NestedSeq() + nested["nest"] = seq + self.assertSequenceEqual(nested.encode(), hexdec("3005300302017f")) + + self.assertSequenceEqual( + OctetString(b"\x01\x02\x03").encode(), + hexdec("0403010203"), + ) + + class Seq(Sequence): + __slots__ = () + schema = ( + ("erste", Integer(impl=tag_encode(5, klass=TagClassContext))), + ) + seq = Seq() + seq["erste"] = Integer(64) + self.assertSequenceEqual(seq.encode(), hexdec("3003850140")) + + class Seq(Sequence): + __slots__ = () + schema = ( + ("erste", Integer(expl=tag_ctxc(5))), + ) + seq = Seq() + seq["erste"] = Integer(64) + self.assertSequenceEqual(seq.encode(), hexdec("3005a503020140")) + + class Seq(Sequence): + __slots__ = () + schema = ( + ("erste", Null( + impl=tag_encode(0, klass=TagClassContext), + optional=True, + )), + ) + seq = Seq() + seq["erste"] = Null() + self.assertSequenceEqual(seq.encode(), hexdec("30028000")) + seq["erste"] = None + self.assertSequenceEqual(seq.encode(), hexdec("3000")) + + self.assertSequenceEqual( + UTCTime(datetime(1970, 1, 1, 0, 0)).encode(), + hexdec("170d3730303130313030303030305a"), + ) + self.assertSequenceEqual( + UTCTime(datetime(2009, 11, 15, 22, 56, 16)).encode(), + hexdec("170d3039313131353232353631365a"), + ) + self.assertSequenceEqual( + GeneralizedTime(datetime(2100, 4, 5, 12, 1, 1)).encode(), + hexdec("180f32313030303430353132303130315a"), + ) + + class Seq(Sequence): + __slots__ = () + schema = ( + ("erste", GeneralizedTime()), + ) + seq = Seq() + seq["erste"] = GeneralizedTime(datetime(2009, 11, 15, 22, 56, 16)) + self.assertSequenceEqual( + seq.encode(), + hexdec("3011180f32303039313131353232353631365a"), + ) + + self.assertSequenceEqual( + BitString((1, b"\x80")).encode(), + hexdec("03020780"), + ) + self.assertSequenceEqual( + BitString((12, b"\x81\xF0")).encode(), + hexdec("03030481f0"), + ) + + self.assertSequenceEqual( + ObjectIdentifier("1.2.3.4").encode(), + hexdec("06032a0304"), + ) + self.assertSequenceEqual( + ObjectIdentifier("1.2.840.133549.1.1.5").encode(), + hexdec("06092a864888932d010105"), + ) + self.assertSequenceEqual( + ObjectIdentifier("2.100.3").encode(), + hexdec("0603813403"), + ) + + self.assertSequenceEqual( + PrintableString("test").encode(), + hexdec("130474657374"), + ) + self.assertSequenceEqual( + PrintableString("x" * 127).encode(), + hexdec("137F" + "78" * 127), + ) + self.assertSequenceEqual( + PrintableString("x" * 128).encode(), + hexdec("138180" + "78" * 128), + ) + self.assertSequenceEqual(UTF8String("Σ").encode(), hexdec("0c02cea3")) + + class Seq(Sequence): + __slots__ = () + schema = ( + ("erste", IA5String()), + ) + seq = Seq() + seq["erste"] = IA5String("test") + self.assertSequenceEqual(seq.encode(), hexdec("3006160474657374")) + + class Seq(Sequence): + __slots__ = () + schema = ( + ("erste", PrintableString()), + ) + seq = Seq() + seq["erste"] = PrintableString("test") + self.assertSequenceEqual(seq.encode(), hexdec("3006130474657374")) + seq["erste"] = PrintableString("test*") + self.assertSequenceEqual(seq.encode(), hexdec("30071305746573742a")) + + class Seq(Sequence): + __slots__ = () + schema = ( + ("erste", Any(optional=True)), + ("zweite", Integer()), + ) + seq = Seq() + seq["zweite"] = Integer(64) + self.assertSequenceEqual(seq.encode(), hexdec("3003020140")) + + class Seq(SetOf): + __slots__ = () + schema = Integer() + seq = Seq() + seq.append(Integer(10)) + self.assertSequenceEqual(seq.encode(), hexdec("310302010a")) + + class _SeqOf(SequenceOf): + __slots__ = () + schema = PrintableString() + + class SeqOf(SequenceOf): + __slots__ = () + schema = _SeqOf() + _seqof = _SeqOf() + _seqof.append(PrintableString("1")) + seqof = SeqOf() + seqof.append(_seqof) + self.assertSequenceEqual(seqof.encode(), hexdec("30053003130131")) + + class Seq(Sequence): + __slots__ = () + schema = ( + ("erste", Integer(default=1)), + ) + seq = Seq() + seq["erste"] = Integer(0) + self.assertSequenceEqual(seq.encode(), hexdec("3003020100")) + seq["erste"] = Integer(1) + self.assertSequenceEqual(seq.encode(), hexdec("3000")) + seq["erste"] = Integer(2) + self.assertSequenceEqual(seq.encode(), hexdec("3003020102")) + + +class TestPP(TestCase): + @given(data_strategy()) + def test_oid_printing(self, d): + oids = { + str(ObjectIdentifier(k)): v * 2 + for k, v in d.draw(dictionaries(oid_strategy(), text_letters())).items() + } + chosen = d.draw(sampled_from(sorted(oids))) + chosen_id = oids[chosen] + pp = _pp(asn1_type_name=ObjectIdentifier.asn1_type_name, value=chosen) + self.assertNotIn(chosen_id, pp_console_row(pp)) + self.assertIn(chosen_id, pp_console_row(pp, oids=oids))