commit 88f0598eabea782355e29d1b31bc54bb1734df6c Author: Heiko Schaefer Date: Wed Jun 30 22:29:23 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de8d8f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-License-Identifier: CC0-1.0 + +target/ +.idea +Cargo.lock +_test/ +notes/ + diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 0000000..d69cc93 --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,8 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: OpenPGP card +Upstream-Contact: Heiko Schaefer +Source: https://gitlab.com/hkos/openpgp-card + +Files: example/* +Copyright: 2021 Heiko Schaefer +License: CC0-1.0 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..45c5ddb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-License-Identifier: MIT OR Apache-2.0 + + +[workspace] +members = [ + "openpgp-card", + "openpgp-card-sequoia", +] diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..b13cd1c --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +Copyright 2021 Heiko Schäfer + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..a343ccd --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,119 @@ +Creative Commons Legal Code + +CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES +NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE +AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION +ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE +OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS +LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION +OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive +Copyright and Related Rights (defined below) upon the creator and subsequent +owner(s) (each and all, an "owner") of an original work of authorship and/or +a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later claims +of infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further production +of creative, cultural and scientific works, or to gain reputation or greater +distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with +a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or +her Copyright and Related Rights in the Work and the meaning and intended +legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected +by copyright and related or neighboring rights ("Copyright and Related Rights"). +Copyright and Related Rights include, but are not limited to, the following: + +i. the right to reproduce, adapt, distribute, perform, display, communicate, +and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + +iii. publicity and privacy rights pertaining to a person's image or likeness +depicted in a Work; + +iv. rights protecting against unfair competition in regards to a Work, subject +to the limitations in paragraph 4(a), below; + +v. rights protecting the extraction, dissemination, use and reuse of data +in a Work; + +vi. database rights (such as those arising under Directive 96/9/EC of the +European Parliament and of the Council of 11 March 1996 on the legal protection +of databases, and under any national implementation thereof, including any +amended or successor version of such directive); and + +vii. other similar, equivalent or corresponding rights throughout the world +based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time extensions), +(iii) in any current or future medium and for any number of copies, and (iv) +for any purpose whatsoever, including without limitation commercial, advertising +or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the +benefit of each member of the public at large and to the detriment of Affirmer's +heirs and successors, fully intending that such Waiver shall not be subject +to revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account Affirmer's +express Statement of Purpose. In addition, to the extent the Waiver is so +judged Affirmer hereby grants to each affected person a royalty-free, non +transferable, non sublicensable, non exclusive, irrevocable and unconditional +license to exercise Affirmer's Copyright and Related Rights in the Work (i) +in all territories worldwide, (ii) for the maximum duration provided by applicable +law or treaty (including future time extensions), (iii) in any current or +future medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional purposes +(the "License"). The License shall be deemed effective as of the date CC0 +was applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder of +the License, and in such case Affirmer hereby affirms that he or she will +not (i) exercise any of his or her remaining Copyright and Related Rights +in the Work or (ii) assert any associated claims and causes of action with +respect to the Work, in either case contrary to Affirmer's express Statement +of Purpose. + + 4. Limitations and Disclaimers. + +a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, +licensed or otherwise affected by this document. + +b. Affirmer offers the Work as-is and makes no representations or warranties +of any kind concerning the Work, express, implied, statutory or otherwise, +including without limitation warranties of title, merchantability, fitness +for a particular purpose, non infringement, or the absence of latent or other +defects, accuracy, or the present or absence of errors, whether or not discoverable, +all to the greatest extent permissible under applicable law. + +c. Affirmer disclaims responsibility for clearing rights of other persons +that may apply to the Work or any use thereof, including without limitation +any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims +responsibility for obtaining any necessary consents, permissions or other +rights required for any use of the Work. + +d. Affirmer understands and acknowledges that Creative Commons is not a party +to this document and has no duty or obligation with respect to this CC0 or +use of the Work. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..97f25c8 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,19 @@ +The MIT License (MIT) +Copyright (c) 2021 Heiko Schäfer + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2d0dd6 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ + + +**OpenPGP card client library** + +This project implements a client library for the +[OpenPGP card](https://gnupg.org/ftp/specs/OpenPGP-smart-card-application-3.4.1.pdf) +specification, in Rust. + +The project consists of two crates: +- [openpgp-card](https://crates.io/crates/openpgp-card), which offers an + implementation-agnostic OpenPGP card client API. It can be used with any + PGP implementation. +- [openpgp-card-sequoia](https://crates.io/crates/openpgp-card-sequoia), + adds functionality to conveniently use the openpgp-card library with + [Sequoia PGP](https://sequoia-pgp.org/). + +**Acknowledgements** + +This library is based on the +[OpenPGP Card spec](https://gnupg.org/ftp/specs/OpenPGP-smart-card-application-3.4.1.pdf), +version 3.4.1. + +Other helpful resources included: + +The free [Gnuk](https://git.gniibe.org/cgit/gnuk/gnuk.git/) +OpenPGP card implementation by [gniibe](https://www.gniibe.org/). + +The Rust/Sequoia-based OpenPGP card client code in +[kushaldas](https://kushaldas.in/)' project +[johnnycanencrypt](https://github.com/kushaldas/johnnycanencrypt/). + +The [scdaemon](https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=tree;f=scd;hb=refs/heads/master) +client implementation by the [GnuPG](https://gnupg.org/) project. + +The [open-keychain](https://github.com/open-keychain/open-keychain) project, +which implements an OpenPGP card client for Java/Android. + +The Rust/Sequoia-based OpenPGP card client code by +[Robin Krahl](https://git.sr.ht/~ireas/sqsc). \ No newline at end of file diff --git a/example/encrypted_to_25519.asc b/example/encrypted_to_25519.asc new file mode 100644 index 0000000..444822d --- /dev/null +++ b/example/encrypted_to_25519.asc @@ -0,0 +1,9 @@ +-----BEGIN PGP MESSAGE----- + +hF4Dc8fxqe7aw2ASAQdAqTjTObPuwUiRtLQEgEX9hrmCujWNZBLfh9kwCwDR3FEw +ybpUXusFaZUtR7cWbB/csRuMQF0eSgFgYZYuY53TVpdN1hqv5ZRDlUY+zX8vCgke +0kgBxlDCnhLKtI402g8mz36rIxBTQSyGVAa8NekCddl95OUEwLwU84XfxsYk+Ghp +D9XPoW1l7W3bAli91QGriuv9ui+qBbxHoE8= +=nMA1 +-----END PGP MESSAGE----- + diff --git a/example/encrypted_to_nist521.asc b/example/encrypted_to_nist521.asc new file mode 100644 index 0000000..4a7a835 --- /dev/null +++ b/example/encrypted_to_nist521.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP MESSAGE----- + +hMIDE466KzPtyE4SBCMEAamS6kiGRDwualZEDO60OxTL9kSa7UEyu+oTMwlsJIvN +mWEFupa5tu4NaJvP7xgF2QGDco5BjRjyO9oL1OsL3s+rAT2m499RofyUW1H6Navl +s7DXS/mq2HvZ+M6d4AacFhObzgjo+wX0rNrpMHF+RhK/gW+0VnuO0LvhZolG2Zuy +mvfpMM+FZ5oNDZw2VgxfA+zbx8XFiYs1LS8HiWV3cDXl70c46XgvRPEpUsvVewxR +zObptdJIAS/afvidUm3ZxJKZQrqpk8TGlmB2hhabeMqUHWhVUy9KY4eIXbKZT53z +jJup+m0xw9CmT0RWubG8OUwV4WwMpArAmUCqXAmC +=TMDL +-----END PGP MESSAGE----- + diff --git a/example/encrypted_to_rsa2k.asc b/example/encrypted_to_rsa2k.asc new file mode 100644 index 0000000..eb499c1 --- /dev/null +++ b/example/encrypted_to_rsa2k.asc @@ -0,0 +1,12 @@ +-----BEGIN PGP MESSAGE----- + +hQEMA9KP15N3zDJDAQgAkGxBQPRwkFusBio9zFvXgyxPiwwwgrjMlHS5kB8HS/u/ +PaUy5hftiaZsezwil6VAHB1e+fwNljaHg2P6Y/jQfR6vts307PMzolZYffLgjnCT +5JtFZ6liz885WrRZ4ZUmFZZLKJgWKKB839kej+d9jC2PeMDQHbHh9lOmpSZ0rDds +AWB89WjPmtsvrCTerjaQXwQ5VVmscD9HSrgl+cUl0r+xGBTiLMZk6N6qo7oT8xLx +4vCyvheBbBqfpvIQ88j/OPVa/DTTLNfwuybHN5PJiPf3YWbuzSEi9bhfGy5YnjLk +OWNm+NfpiYHqgQ5AtqzC2GuE9QbiAB47NaJF1HbON9JIAa0m9ofL2WarKTLB9fbo +FC4+OA9RvclMccNpm9Wqu9zXClda3wq2LhJIhlux9QEn0Ey0GpIfkn5Rz3kevUr0 +5sP1XNQpz4w8 +=tdV2 +-----END PGP MESSAGE----- diff --git a/example/encrypted_to_rsa4k.asc b/example/encrypted_to_rsa4k.asc new file mode 100644 index 0000000..4352a9b --- /dev/null +++ b/example/encrypted_to_rsa4k.asc @@ -0,0 +1,18 @@ + +-----BEGIN PGP MESSAGE----- + +hQIMAx1NITYxRsy6ARAAhd3a2hAdrcQpkn3Ub80yGlM8b7PRUTMH1Slzco72xWea +i2n5UvIPXYW7fBIrf2F6sotCczbNiLiLQIwgW4MDSCLprjD9/mjm/85slEAn94TS +3omGkjkYjqaHF0wGDBZ/l46nLFcAUyiURmaP664nm2SrEaEGJnVqeEXVp/pmWOpT +bpNwjA7u16XwYr5lEdvG7OGtkYbC9Fjr+/K9CQJ3xy3aUnH211or6RCf6iIotlhD +pvGid9BTQnk6uV/LUyESYJHi+V18OGgHAPnDSHcGTFCfhEiOs/UOZ4ssnSVLamho +1wqWYxSmNtWuPQmQg7eE/cXUQUHUHL2MWanvwGlajcEa5W9OmzwAA/Gruo/pZl+E +dEfrSDB+SPlpIkD28+iM7XcdWwrLbuWGD62sJZZHsS0oTnv2+qZIODyh6xmnx18A +tnhq6anHLVAUtMt319mKssHDCQQj0hKHMP/C8cP9pFvSAAXUPc4/icJe4YwVc0Z/ +EV4AVZ1Cy/q3KyCvIPctntY3KA+OhNN5atsT+8MTIwz7bso5DLV+NzsDunULrHra +M3Bcl6LjlrL/MMks1YnAqDAE+26IoPNoQ9R9kxQSX9IwpHg2mBT51uuYqu0fSfsV +SP9822Uj5uvEo3Wxf0XOFAocpb/ZtRZUmClB2aFnb/+R6VMzScJuAPX7EGv3qxnS +SAFfgz51iCZqciCQ79LUT/YjAK2ohDPg/HmtyjiL4BDCpWZLw0fZeY3RdD3GsjqP +Hw0pQHToHRVGPLjew/rsZ0OmVq8vDd+Uvg== +=Gr7Z +-----END PGP MESSAGE----- diff --git a/example/nist256.sec b/example/nist256.sec new file mode 100644 index 0000000..5f8a571 --- /dev/null +++ b/example/nist256.sec @@ -0,0 +1,16 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lHcEYNExkBMIKoZIzj0DAQcCAwThfAjU6PI2Sn+4R/6NVtPkIt42alFOy7xYkwr3 +K08rmmG/YR+F8nnVRbDil4pexYJPXFtuGLCRCQLMNDWMKlGkAAEA0833239JF/Hv +VZx+qS/bfF7RakQUSnxBttyJmSU28TEQ+bQhTmlzdDI1NiBLZXkgPG5pc3QyNTZA +ZXhhbXBsZS5vcmc+iJAEExMIADgWIQSGmL3TXB0ICfKhhuyfrp9qHgU/FAUCYNEx +kAIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCfrp9qHgU/FE6IAPwM3BHN ++QaOa2TYeMUNIuzhWUFOfwEF1hq4RCmF+SngYgEA18cOFAnl17rSArJ8nU4O7TXH +iFpKyxZSgI2r8OGQbhCcewRg0TGQEggqhkjOPQMBBwIDBCsQOkQZsT1uSJhuXjPa +aY6jkU/j98LO2TdRLyXOYXGyY/1gIVE3PH4AFiHn6CwHUlFp5p+kDtbuDnBWYPOK +V+0DAQgHAAD8DJQWf3nIeSoFDcwunp381Y4f5ipOOzfyxOO2vTLZW+MQ/4h4BBgT +CAAgFiEEhpi901wdCAnyoYbsn66fah4FPxQFAmDRMZACGwwACgkQn66fah4FPxSD +vAD+IHPpFNG3HjIhs25dzwg2/FWkRDo/DSDZwdfnPgyFxewA/iFXxTyFy27PzxSz +49jr9NoTliT2F4bvg/6Ef0yHFBp1 +=/UJg +-----END PGP PRIVATE KEY BLOCK----- diff --git a/example/nist521.sec b/example/nist521.sec new file mode 100644 index 0000000..568b301 --- /dev/null +++ b/example/nist521.sec @@ -0,0 +1,23 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lNkEYNEvwRMFK4EEACMEIwQBUILw0iAs/32QjpkbCGYA1eOeXY+AHJaRBNWpzzDp +vlxTzdPMropRtz2XPYq/QVkDMiGQdB4kDd5tg9NUt54nWCkA9W8JIaXkkn6JebV4 +0SmIoOFAXMUo1Xc5T5Ft266iYajVwBgWQR6KAweAycBoB8R+bUkBApdocPxSJFQl +16ZscM4AAgiGk8JzE4jYYuhDmdwx+SDv/7GlJLw59IgCzXbq+aKZ3k/M/EOhutmu +oyzzyXE+3lrYG8BiR9Wok8SBs07IfXqSMiYUtB5OaXN0IEtleSA8bmlzdDUyMUBl +eGFtcGxlLm9yZz6I0gQTEwoAOBYhBPyXss75biiVRUrwS405Q4HLFqtdBQJg0S/B +AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEI05Q4HLFqtdBHcCCMICD99W +LkQb1LsDZ5W4G6iNpBFiY+jGpTsfDSiIE5ZiZGcteottNn71G45h8j6UBAoA79Zx +QZr6mR6pmIM9SFhIAgY9Ec2CwFUAcPHz4YTHJNBZC0q32IRgHbJtZy7XZDl8rmbi +uPwzVCcuzUQa9nzEntaDXolYS5+6uYmEeZjxtkI9nJzeBGDRL8ESBSuBBAAjBCME +ATDmyeCx6G7ey6CvmF9eOlOEFtZjRX5vWHiXE1+nMF/Fi+M7l9/qLI78m9uSRhBn +j7bo2Tdf2Y9FefdRThn+qRySANiLdYM9pSPrfEpiDf7ugTpw+lzba4Ldodk2o3YM +0CnB8puvzj66wvwd5/nN0pSM6j7y7IUoc6fVVBIp4slAaaSKAwEKCQACCQGGQbdz +CxK7knJLv3/RwvcMbIcWxhUNx6XgUahAeVut1M8TCuIlW1jNR/vtTmAKUuuV7hKR +LWNUxINBfaJvLLYFYx7EiLsEGBMKACAWIQT8l7LO+W4olUVK8EuNOUOByxarXQUC +YNEvwQIbDAAKCRCNOUOByxarXamjAgkBkPfnpZ65+4h+CLC11s3kRpzrdPoZKxGK +kNrwkFSFQ9mNbN1ubROcmsr024Wr2tc8CHDfdLMnNGxQqByn+srC2LYCCOb2tISm +efzwJDZgF8BtdcuLoSOo4JIlgveoXjsx89ngmxtf62SbksH+AX+8xhB4FfKAEewn +Rj6gHQaw0EY0JW3z +=hy8t +-----END PGP PRIVATE KEY BLOCK----- diff --git a/example/test25519.sec b/example/test25519.sec new file mode 100644 index 0000000..c0c89ac --- /dev/null +++ b/example/test25519.sec @@ -0,0 +1,15 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lFgEYNHHwRYJKwYBBAHaRw8BAQdA+kUBD6wJpuvpMNwPHngxDbi4AwcXFbk17ZA0 +z//ycoMAAQD9eZAyGYE/h2Hq4i4HBDAMREDnvOP+CAY4L0O9Y3gTMgzYtCJUZXN0 +IDI1NTE5IDx0ZXN0MjU1MTlAZXhhbXBsZS5vcmc+iJAEExYIADgWIQTykNu/IduG +NDyWFXuHvhW39UjZfAUCYNHHwQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAK +CRCHvhW39UjZfCbMAQCGRz0vSuzKH7QvUb+6huiXv6yrxzVGqQOjZJ5g0xfDPwEA +/MUoT6gDHvhrb7ELAARjl9kHBw6qxCW+x0USJFYX1gycXQRg0cfBEgorBgEEAZdV +AQUBAQdA9YmYawgzp/CymrH54/f1Cq+qbfchvG7+PgrF7gtFoGQDAQgHAAD/ccC7 +CCgN9Aw5sgLvka9AwnZPfwBZbi0zCLaDVUuF30gOPoh4BBgWCAAgFiEE8pDbvyHb +hjQ8lhV7h74Vt/VI2XwFAmDRx8ECGwwACgkQh74Vt/VI2XxZxgEAl8wtZE4QW1AQ +jEVrtJZATjeLXbi6+XNvxQOpsxXwt8EA/irIncB8b08QaEYb5cKj90TUHdVgbBh8 +HR79wFHFjVgF +=HpEs +-----END PGP PRIVATE KEY BLOCK----- diff --git a/example/test2k.sec b/example/test2k.sec new file mode 100644 index 0000000..afedb5d --- /dev/null +++ b/example/test2k.sec @@ -0,0 +1,57 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=c/bE +-----END PGP PRIVATE KEY BLOCK----- diff --git a/example/test3k.sec b/example/test3k.sec new file mode 100644 index 0000000..0e86235 --- /dev/null +++ b/example/test3k.sec @@ -0,0 +1,81 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=1jDp +-----END PGP PRIVATE KEY BLOCK----- diff --git a/example/test4k.sec b/example/test4k.sec new file mode 100644 index 0000000..ac3e1df --- /dev/null +++ b/example/test4k.sec @@ -0,0 +1,105 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=AhOY +-----END PGP PRIVATE KEY BLOCK----- diff --git a/openpgp-card-sequoia/Cargo.toml b/openpgp-card-sequoia/Cargo.toml new file mode 100644 index 0000000..6ddf127 --- /dev/null +++ b/openpgp-card-sequoia/Cargo.toml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-License-Identifier: MIT OR Apache-2.0 + +[package] +name = "openpgp-card-sequoia" +description = "Wrapper of openpgp-card for use with Sequoia PGP" +license = "MIT OR Apache-2.0" +version = "0.0.1" +authors = ["Heiko Schaefer "] +edition = "2018" + +[dependencies] +sequoia-openpgp = "1.3" +openpgp-card = { path = "../openpgp-card" } +chrono = "0.4" +anyhow = "1" +thiserror = "1" +env_logger = "0.8" +log = "0.4" \ No newline at end of file diff --git a/openpgp-card-sequoia/README.md b/openpgp-card-sequoia/README.md new file mode 100644 index 0000000..28c7185 --- /dev/null +++ b/openpgp-card-sequoia/README.md @@ -0,0 +1,33 @@ + + +**OpenPGP card for Sequoia PGP** + +This crate is a thin wrapper for the +[openpgp-card](https://crates.io/crates/openpgp-card) crate. +It offers convenient access to +[OpenPGP card](https://en.wikipedia.org/wiki/OpenPGP_card) +functionality with [Sequoia PGP](https://sequoia-pgp.org/). + +**Example code** + +The program `main.rs` performs a number of functions on an OpenPGP card. +To use it, you need to set an environment variable to the serial number of +the OpenPGP card you want to use. + +NOTE: data on this card will be deleted in the process of running this +program! + +``` +$ export TEST_CARD_SERIAL="01234567" +$ cargo run +``` + +You can see a lot more debugging output by increasing the log-level, +like this: + +``` +$ RUST_LOG=trace cargo run +``` \ No newline at end of file diff --git a/openpgp-card-sequoia/src/decryptor.rs b/openpgp-card-sequoia/src/decryptor.rs new file mode 100644 index 0000000..544f985 --- /dev/null +++ b/openpgp-card-sequoia/src/decryptor.rs @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use anyhow::anyhow; + +use openpgp::Cert; +use openpgp::crypto; +use openpgp::crypto::mpi; +use openpgp::crypto::SessionKey; +use openpgp::packet; +use openpgp::parse::stream::{ + DecryptionHelper, MessageStructure, VerificationHelper, +}; +use openpgp::policy::Policy; +use openpgp::types::{Curve, SymmetricAlgorithm}; +use sequoia_openpgp as openpgp; + +use openpgp_card::{DecryptMe, OpenPGPCardUser}; +use openpgp_card::errors::OpenpgpCardError; + +use crate::PublicKey; + +pub(crate) struct CardDecryptor<'a> { + /// The OpenPGP card (authenticated to allow decryption operations) + ocu: &'a OpenPGPCardUser, + + /// The matching public key for the card's decryption key + public: PublicKey, +} + +impl<'a> CardDecryptor<'a> { + /// Try to create a CardDecryptor. + /// + /// An Error is returned if no match between the card's decryption + /// key and a (sub)key of `cert` can be made. + pub fn new(ocu: &'a OpenPGPCardUser, + cert: &Cert, + policy: &dyn Policy) -> Result, OpenpgpCardError> { + + // Get the fingerprint for the decryption key from the card. + let fps = ocu.get_fingerprints()?; + let fp = fps.decryption(); + + if let Some(fp) = fp { + // Transform into Sequoia Fingerprint + let fp = openpgp::Fingerprint::from_bytes(fp.as_bytes()); + + // Find the matching encryption-capable (sub)key in `cert` + let keys: Vec<_> = + cert.keys() + .with_policy(policy, None) + .for_storage_encryption() + .for_transport_encryption() + .filter(|ka| ka.fingerprint() == fp) + .map(|ka| ka.key()) + .collect(); + + // Exactly one matching (sub)key should be found. If not, fail! + if keys.len() == 1 { + let public = keys[0].clone(); + Ok(Self { ocu, public }) + } else { + Err(OpenpgpCardError::InternalError( + anyhow!("Failed to find a matching (sub)key in cert"))) + } + } else { + Err(OpenpgpCardError::InternalError( + anyhow!("Failed to get the decryption key's Fingerprint \ + from the card"))) + } + } +} + +impl<'a> crypto::Decryptor for CardDecryptor<'a> { + fn public(&self) -> &PublicKey { + &self.public + } + + /// Delegate a decryption operation to the OpenPGP card. + /// + /// This fn prepares the data structures that openpgp-card needs to + /// perform the decryption operation. + /// + /// (7.2.11 PSO: DECIPHER) + fn decrypt( + &mut self, + ciphertext: &mpi::Ciphertext, + _plaintext_len: Option, + ) -> openpgp::Result { + match (ciphertext, self.public.mpis()) { + ( + mpi::Ciphertext::RSA { c: ct }, + mpi::PublicKey::RSA { .. }, + ) => { + let dm = DecryptMe::RSA(ct.value()); + let dec = self.ocu.decrypt(dm)?; + + let sk = openpgp::crypto::SessionKey::from(&dec[..]); + Ok(sk) + } + ( + mpi::Ciphertext::ECDH { ref e, .. }, + mpi::PublicKey::ECDH { ref curve, .. }, + ) => { + let dm = + if curve == &Curve::Cv25519 { + // Ephemeral key without header byte 0x40 + DecryptMe::ECDH(&e.value()[1..]) + } else { + // NIST curves: ephemeral key with header byte + DecryptMe::ECDH(&e.value()) + }; + + // Decryption operation on the card + let dec = self.ocu.decrypt(dm)?; + + #[allow(non_snake_case)] + let S: openpgp::crypto::mem::Protected = dec.into(); + + Ok(crypto::ecdh::decrypt_unwrap(&self.public, &S, ciphertext)?) + } + + (ciphertext, public) => + Err(anyhow!( + "Unsupported combination of ciphertext {:?} \ + and public key {:?} ", ciphertext, public + )), + } + } +} + +impl<'a> DecryptionHelper for CardDecryptor<'a> { + fn decrypt( + &mut self, + pkesks: &[packet::PKESK], + _skesks: &[packet::SKESK], + sym_algo: Option, + mut dec_fn: D, + ) -> openpgp::Result> + where D: FnMut(SymmetricAlgorithm, &SessionKey) -> bool, + { + // Try to decrypt each PKESK, see: + // https://docs.sequoia-pgp.org/src/sequoia_openpgp/packet/pkesk.rs.html#125 + for pkesk in pkesks { + // Only attempt decryption if the KeyIDs match + // (this check is an optimization) + if pkesk.recipient() == &self.public.keyid() { + if pkesk + .decrypt(self, sym_algo) + .map(|(algo, session_key)| dec_fn(algo, &session_key)) + .unwrap_or(false) + { + return Ok(Some(self.public.fingerprint())); + } + } + } + + Ok(None) + } +} + +impl<'a> VerificationHelper for CardDecryptor<'a> { + fn get_certs(&mut self, _ids: &[openpgp::KeyHandle]) -> openpgp::Result> { + Ok(vec![]) + } + fn check(&mut self, _structure: MessageStructure) -> openpgp::Result<()> { + Ok(()) + } +} diff --git a/openpgp-card-sequoia/src/lib.rs b/openpgp-card-sequoia/src/lib.rs new file mode 100644 index 0000000..e4c5aea --- /dev/null +++ b/openpgp-card-sequoia/src/lib.rs @@ -0,0 +1,284 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! This library supports using openpgp-card functionality with +//! sequoia_openpgp data structures. + +use std::error::Error; +use std::io; + +use anyhow::{anyhow, Context, Result}; +use chrono::prelude::*; +use openpgp::armor; +use openpgp::cert::amalgamation::key::ValidErasedKeyAmalgamation; +use openpgp::crypto::mpi; +use openpgp::crypto::mpi::{MPI, ProtectedMPI}; +use openpgp::packet::{Key, key}; +use openpgp::packet::key::{SecretParts, UnspecifiedRole}; +use openpgp::parse::{Parse, stream::DecryptorBuilder}; +use openpgp::policy::StandardPolicy; +use openpgp::serialize::stream::{Message, Signer}; +use sequoia_openpgp as openpgp; + +use openpgp_card::{CardUploadableKey, EccKey, EccType, errors::OpenpgpCardError, + KeyType, OpenPGPCardAdmin, OpenPGPCardUser, PrivateKeyMaterial, + RSAKey}; + +mod decryptor; +mod signer; + +/// Shorthand for public key data +pub(crate) type PublicKey = Key; + + +/// A SequoiaKey represents the private cryptographic key material of an +/// OpenPGP (sub)key to be uploaded to an OpenPGP card. +struct SequoiaKey { + key: openpgp::packet::Key, + public: mpi::PublicKey, + password: Option, +} + +impl SequoiaKey { + /// A `SequoiaKey` wraps a Sequoia PGP private (sub)key data + /// (i.e. a ValidErasedKeyAmalgamation) in a form that can be uploaded + /// by the openpgp-card crate. + fn new(vka: ValidErasedKeyAmalgamation, + password: Option) -> Self { + let public = vka.parts_as_public().mpis().clone(); + + Self { + key: vka.key().clone(), + public, + password, + } + } +} + +/// Implement the `CardUploadableKey` trait that openpgp-card uses to +/// upload (sub)keys to a card. +impl CardUploadableKey for SequoiaKey { + fn get_key(&self) -> Result { + // Decrypt key with password, if set + let key = match &self.password { + None => self.key.clone(), + Some(pw) => self.key.clone() + .decrypt_secret(&openpgp::crypto::Password::from(pw.as_str()))? + }; + + // Get private cryptographic material + let unenc = + if let Some(openpgp::packet::key::SecretKeyMaterial::Unencrypted(ref u)) = key.optional_secret() { + u + } else { + panic!("can't get private key material"); + }; + + let secret_key_material = unenc.map(|mpis| mpis.clone()); + + match (&self.public, secret_key_material) { + (mpi::PublicKey::RSA { e, n }, + mpi::SecretKeyMaterial::RSA { d: _, p, q, u: _ }) => { + let sq_rsa = SqRSA::new(e.clone(), n.clone(), p, q); + + Ok(PrivateKeyMaterial::R(Box::new(sq_rsa))) + } + (mpi::PublicKey::ECDH { curve, .. }, + mpi::SecretKeyMaterial::ECDH { scalar }) => { + let sq_ecc = SqEccKey::new(curve.oid().to_vec(), + scalar, EccType::ECDH); + + Ok(PrivateKeyMaterial::E(Box::new(sq_ecc))) + } + (mpi::PublicKey::ECDSA { curve, .. }, + mpi::SecretKeyMaterial::ECDSA { scalar }) => { + let sq_ecc = SqEccKey::new(curve.oid().to_vec(), + scalar, EccType::ECDSA); + + Ok(PrivateKeyMaterial::E(Box::new(sq_ecc))) + } + (mpi::PublicKey::EdDSA { curve, .. }, + mpi::SecretKeyMaterial::EdDSA { scalar }) => { + let sq_ecc = SqEccKey::new(curve.oid().to_vec(), + scalar, EccType::EdDSA); + + Ok(PrivateKeyMaterial::E(Box::new(sq_ecc))) + } + (p, s) => { + unimplemented!("Unexpected algorithms: public {:?}, \ + secret {:?}", p, s); + } + } + } + + fn get_ts(&self) -> u64 { + let key_creation: DateTime = self.key.creation_time().into(); + key_creation.timestamp() as u64 + } + + fn get_fp(&self) -> Vec { + self.key.fingerprint().as_bytes().to_vec() + } +} + + +/// RSA-specific data-structure to hold private (sub)key material for upload +/// with the `openpgp-card` crate. +struct SqRSA { + e: MPI, + n: MPI, + p: ProtectedMPI, + q: ProtectedMPI, +} + +impl SqRSA { + fn new(e: MPI, n: MPI, p: ProtectedMPI, q: ProtectedMPI) -> Self { + Self { e, n, p, q } + } +} + +impl RSAKey for SqRSA { + fn get_e(&self) -> &[u8] { + self.e.value() + } + + fn get_n(&self) -> &[u8] { + self.n.value() + } + + fn get_p(&self) -> &[u8] { + self.p.value() + } + + fn get_q(&self) -> &[u8] { + self.q.value() + } +} + +/// ECC-specific data-structure to hold private (sub)key material for upload +/// with the `openpgp-card` crate. +struct SqEccKey { + oid: Vec, + scalar: ProtectedMPI, + ecc_type: EccType, +} + +impl SqEccKey { + fn new(oid: Vec, scalar: ProtectedMPI, ecc_type: EccType) -> Self { + SqEccKey { oid, scalar, ecc_type } + } +} + +impl EccKey for SqEccKey { + fn get_oid(&self) -> &[u8] { + &self.oid + } + + fn get_scalar(&self) -> &[u8] { + &self.scalar.value() + } + + fn get_type(&self) -> EccType { + self.ecc_type + } +} + + +/// Convenience fn to select and upload a (sub)key from a Cert, as a given +/// KeyType. If multiple suitable (sub)keys are found, the first one is +/// used. +/// +/// FIXME: picking the (sub)key to upload should probably done with +/// more intent. +pub fn upload_from_cert_yolo( + oca: &OpenPGPCardAdmin, + cert: &sequoia_openpgp::Cert, + key_type: KeyType, + password: Option, +) -> Result<(), Box> { + let policy = StandardPolicy::new(); + + // Find all suitable (sub)keys for key_type. + let mut valid_ka = cert + .keys() + .with_policy(&policy, None) + .secret() + .alive() + .revoked(false); + valid_ka = match key_type { + KeyType::Decryption => valid_ka.for_storage_encryption(), + KeyType::Signing => valid_ka.for_signing(), + KeyType::Authentication => valid_ka.for_authentication(), + _ => return Err(anyhow!("Unexpected KeyType").into()), + }; + + // FIXME: for now, we just pick the first (sub)key from the list + if let Some(vka) = valid_ka.next() { + upload_key(oca, vka, key_type, password).map_err(|e| e.into()) + } else { + Err(anyhow!("No suitable (sub)key found").into()) + } +} + +/// Upload a ValidErasedKeyAmalgamation to the card as a specific KeyType. +/// +/// The caller needs to make sure that `vka` is suitable for `key_type`. +pub fn upload_key( + oca: &OpenPGPCardAdmin, + vka: ValidErasedKeyAmalgamation, + key_type: KeyType, + password: Option, +) -> Result<(), OpenpgpCardError> { + let sqk = SequoiaKey::new(vka, password); + + oca.upload_key(Box::new(sqk), key_type) +} + +pub fn decrypt( + ocu: &OpenPGPCardUser, + cert: &sequoia_openpgp::Cert, + msg: Vec, +) -> Result> { + let mut decrypted = Vec::new(); + { + let reader = io::BufReader::new(&msg[..]); + + let p = StandardPolicy::new(); + let d = decryptor::CardDecryptor::new(ocu, cert, &p)?; + + let db = DecryptorBuilder::from_reader(reader)?; + let mut decryptor = db.with_policy(&p, None, d)?; + + // Read all data from decryptor and store in decrypted + io::copy(&mut decryptor, &mut decrypted)?; + } + + Ok(decrypted) +} + +pub fn sign( + ocu: &OpenPGPCardUser, + cert: &sequoia_openpgp::Cert, + input: &mut dyn io::Read, +) -> Result { + let mut armorer = armor::Writer::new(vec![], armor::Kind::Signature)?; + { + let p = StandardPolicy::new(); + let s = signer::CardSigner::new(ocu, cert, &p)?; + + let message = Message::new(&mut armorer); + let mut message = Signer::new(message, s) + .detached() + .build()?; + + // Process input data, via message + io::copy(input, &mut message)?; + + message.finalize()?; + } + + let buffer = armorer.finalize()?; + + String::from_utf8(buffer) + .context("Failed to convert signature to utf8") +} diff --git a/openpgp-card-sequoia/src/main.rs b/openpgp-card-sequoia/src/main.rs new file mode 100644 index 0000000..e601066 --- /dev/null +++ b/openpgp-card-sequoia/src/main.rs @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::error::Error; +use std::env; + +use anyhow::Result; +use sequoia_openpgp::Cert; +use sequoia_openpgp::parse::Parse; + +use openpgp_card::{KeyType, OpenPGPCard}; + +// Filename of test key and test message to use: + +// const TEST_KEY_PATH: &str = "example/test4k.sec"; +// const TEST_ENC_MSG: &str = "example/encrypted_to_rsa4k.asc"; + +// const TEST_KEY_PATH: &str = "example/nist521.sec"; +// const TEST_ENC_MSG: &str = "example/encrypted_to_nist521.asc"; + +const TEST_KEY_PATH: &str = "example/test25519.sec"; +const TEST_ENC_MSG: &str = "example/encrypted_to_25519.asc"; + + +fn main() -> Result<(), Box> { + env_logger::init(); + + // Serial number of the OpenPGP Card that will be used for tests. + let test_card_serial = env::var("TEST_CARD_SERIAL"); + + if let Ok(test_card_serial) = test_card_serial { + println!("** get card"); + let oc = OpenPGPCard::open_by_serial(&test_card_serial)?; + + // card metadata + + println!("** get aid"); + let app_id = oc.get_aid()?; + + println!("app id: {:x?}\n\n", app_id); + println!(" serial: {:?}\n\n", app_id.serial()); + + let eli = oc.get_extended_length_information()?; + println!("extended_length_info: {:?}\n\n", eli); + + let hist = oc.get_historical()?; + println!("historical {:#x?}", hist); + + let ext = oc.get_extended_capabilities()?; + println!("extended_capabilities {:#x?}", ext); + + // cardholder + + let ch = oc.get_cardholder_related_data()?; + println!("card holder {:x?}", ch); + + // crypto-ish metadata + + let fp = oc.get_fingerprints()?; + println!("fp {:#x?}", fp); + + let sst = oc.get_security_support_template()?; + println!("sst {:x?}", sst); + + let ai = oc.list_supported_algo()?; + println!("ai {:#?}", ai); + + let algo = oc.get_algorithm_attributes(KeyType::Signing)?; + println!("algo sig {:?}", algo); + let algo = oc.get_algorithm_attributes(KeyType::Decryption)?; + println!("algo dec {:?}", algo); + let algo = oc.get_algorithm_attributes(KeyType::Authentication)?; + println!("algo aut {:?}", algo); + + + // --------------------------------------------- + // CAUTION: Write commands ahead! + // Try not to overwrite your production cards. + // --------------------------------------------- + assert_eq!(app_id.serial(), test_card_serial); + + + oc.factory_reset()?; + + match oc.verify_pw3("12345678") { + Ok(oc_admin) => { + println!("pw3 verify ok"); + + let res = oc_admin.set_name("Bar< panic!() + } + + // ----------------------------- + // Open fresh Card for decrypt + // ----------------------------- + + let oc = OpenPGPCard::open_by_serial(&test_card_serial)?; + let app_id = oc.get_aid()?; + + // Check that we're still using the expected card + assert_eq!(app_id.serial(), test_card_serial); + + match oc.verify_pw1_82("123456") { + Ok(oc_user) => { + println!("pw1 82 verify ok"); + + let cert = Cert::from_file(TEST_KEY_PATH)?; + let msg = std::fs::read_to_string + (TEST_ENC_MSG) + .expect("Unable to read file"); + + println!("{:?}", msg); + + let res = openpgp_card_sequoia::decrypt + (&oc_user, &cert, msg.into_bytes())?; + + let plain = String::from_utf8_lossy(&res); + println!("decrypted plaintext: {}", plain); + + assert_eq!(plain, "Hello world!\n"); + } + _ => panic!("verify pw1 failed") + } + + // ----------------------------- + // Open fresh Card for signing + // ----------------------------- + let oc = OpenPGPCard::open_by_serial(&test_card_serial)?; + + // Sign + match oc.verify_pw1_81("123456") { + Ok(oc_user) => { + println!("pw1 81 verify ok"); + + let cert = Cert::from_file(TEST_KEY_PATH)?; + + let text = "Hello world, I am signed."; + let res = openpgp_card_sequoia::sign(&oc_user, &cert, + &mut text.as_bytes()); + + println!("res sign {:?}", res); + + println!("res: {}", res?) + + // FIXME: validate sig + } + _ => panic!("verify pw1 failed") + } + } else { + println!("Please set environment variable TEST_CARD_SERIAL."); + println!(); + + println!("NOTE: the configured card will get overwritten!"); + println!("So do NOT use your production card for testing."); + println!(); + + println!("The following OpenPGP cards are currently connected to \ + your system:"); + + let cards = openpgp_card::OpenPGPCard::list_cards()?; + for c in cards { + println!(" '{}'", c.get_aid()?.serial()); + } + } + + Ok(()) +} diff --git a/openpgp-card-sequoia/src/signer.rs b/openpgp-card-sequoia/src/signer.rs new file mode 100644 index 0000000..8fa74fc --- /dev/null +++ b/openpgp-card-sequoia/src/signer.rs @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::convert::TryInto; + +use anyhow::anyhow; +use openpgp::crypto; +use openpgp::crypto::mpi; +use openpgp::policy::Policy; +use openpgp::types::PublicKeyAlgorithm; +use sequoia_openpgp as openpgp; + +use openpgp_card::Hash; +use openpgp_card::OpenPGPCardUser; +use openpgp_card::errors::OpenpgpCardError; + +use crate::PublicKey; + + +pub(crate) struct CardSigner<'a> { + /// The OpenPGP card (authenticated to allow signing operations) + ocu: &'a OpenPGPCardUser, + + /// The matching public key for the card's signing key + public: PublicKey, +} + +impl<'a> CardSigner<'a> { + /// Try to create a CardSigner. + /// + /// An Error is returned if no match between the card's signing + /// key and a (sub)key of `cert` can be made. + pub fn new(ocu: &'a OpenPGPCardUser, + cert: &openpgp::Cert, + policy: &dyn Policy) + -> Result, OpenpgpCardError> { + + // Get the fingerprint for the signing key from the card. + let fps = ocu.get_fingerprints()?; + let fp = fps.signature(); + + if let Some(fp) = fp { + // Transform into Sequoia Fingerprint + let fp = openpgp::Fingerprint::from_bytes(fp.as_bytes()); + + // Find the matching signing-capable (sub)key in `cert` + let keys: Vec<_> = + cert + .keys() + .with_policy(policy, None) + .alive() + .revoked(false) + .for_signing() + .filter(|ka| ka.fingerprint() == fp) + .map(|ka| ka.key()) + .collect(); + + // Exactly one matching (sub)key should be found. If not, fail! + if keys.len() == 1 { + let public = keys[0].clone(); + + Ok(CardSigner { + ocu, + public: public.role_as_unspecified().clone(), + }) + } else { + Err(OpenpgpCardError::InternalError( + anyhow!("Failed to find a matching (sub)key in cert"))) + } + } else { + Err(OpenpgpCardError::InternalError( + anyhow!("Failed to get the signing key's Fingerprint \ + from the card"))) + } + } +} + +impl<'a> crypto::Signer for CardSigner<'a> { + fn public(&self) -> &PublicKey { + &self.public + } + + /// Delegate a signing operation to the OpenPGP card. + /// + /// This fn prepares the data structures that openpgp-card needs to + /// perform the signing operation. + /// + /// (7.2.10 PSO: COMPUTE DIGITAL SIGNATURE) + fn sign(&mut self, + hash_algo: openpgp::types::HashAlgorithm, + digest: &[u8], + ) -> openpgp::Result { + match (self.public.pk_algo(), self.public.mpis()) { + #[allow(deprecated)] + (PublicKeyAlgorithm::RSASign, mpi::PublicKey::RSA { .. }) | + (PublicKeyAlgorithm::RSAEncryptSign, mpi::PublicKey::RSA { .. }) => { + let sig = match hash_algo { + openpgp::types::HashAlgorithm::SHA256 => { + let hash = Hash::SHA256(digest.try_into() + .map_err(|_| anyhow!("invalid slice length"))?); + self.ocu.signature_for_hash(hash)? + } + openpgp::types::HashAlgorithm::SHA384 => { + let hash = Hash::SHA384(digest.try_into() + .map_err(|_| anyhow!("invalid slice length"))?); + self.ocu.signature_for_hash(hash)? + } + openpgp::types::HashAlgorithm::SHA512 => { + let hash = Hash::SHA512(digest.try_into() + .map_err(|_| anyhow!("invalid slice length"))?); + self.ocu.signature_for_hash(hash)? + } + _ => { + return Err( + anyhow!("Unsupported hash algorithm for RSA {:?}", + hash_algo)); + } + }; + + let mpi = mpi::MPI::new(&sig[..]); + Ok(mpi::Signature::RSA { s: mpi }) + } + (PublicKeyAlgorithm::EdDSA, mpi::PublicKey::EdDSA { .. }) => { + let hash = Hash::EdDSA(digest); + let sig = self.ocu.signature_for_hash(hash)?; + + let r = mpi::MPI::new(&sig[..32]); + let s = mpi::MPI::new(&sig[32..]); + + Ok(mpi::Signature::EdDSA { r, s }) + } + + // FIXME: implement NIST etc + (pk_algo, _) => Err(anyhow!( + "Unsupported combination of algorithm {:?} and pubkey {:?}", + pk_algo, self.public + )), + } + } +} diff --git a/openpgp-card/Cargo.toml b/openpgp-card/Cargo.toml new file mode 100644 index 0000000..001632c --- /dev/null +++ b/openpgp-card/Cargo.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-License-Identifier: MIT OR Apache-2.0 + +[package] +name = "openpgp-card" +description = "A client implementation for the OpenPGP card specification" +license = "MIT OR Apache-2.0" +version = "0.0.1" +authors = ["Heiko Schaefer "] +edition = "2018" + +[dependencies] +pcsc = "2" +nom = "6" +hex-literal = "0.3" +anyhow = "1" +thiserror = "1" +env_logger = "0.8" +log = "0.4" +chrono = "0.4" diff --git a/openpgp-card/README.md b/openpgp-card/README.md new file mode 100644 index 0000000..87c4750 --- /dev/null +++ b/openpgp-card/README.md @@ -0,0 +1,14 @@ + + +**OpenPGP card client library** + +This crate implements a client library for the +[OpenPGP card](https://gnupg.org/ftp/specs/OpenPGP-smart-card-application-3.4.1.pdf) +specification, in Rust. + +This library is OpenPGP implementation-agnostic. Its communication with +the card is based on simple data structures, derived from the formats +defined in the OpenPGP card specification. diff --git a/openpgp-card/src/apdu/command.rs b/openpgp-card/src/apdu/command.rs new file mode 100644 index 0000000..36d8262 --- /dev/null +++ b/openpgp-card/src/apdu/command.rs @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use anyhow::Result; +use crate::apdu::Le; + +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Debug)] +pub struct Command { + // Class byte (CLA) + pub cla: u8, + + // Instruction byte (INS) + pub ins: u8, + + // Parameter bytes (P1/P2) + pub p1: u8, + pub p2: u8, + + pub data: Vec, +} + +impl Command { + pub fn new(cla: u8, ins: u8, p1: u8, p2: u8, data: Vec) -> Self { + Command { cla, ins, p1, p2, data } + } + + pub(crate) fn serialize(&self, ext: Le) -> Result> { + // Set OpenPGP card spec, chapter 7 (pg 47) + + // FIXME: 1) get "ext" information (how long can commands and + // responses be), + // FIXME: 2) decide on long vs. short encoding for both Lc and Le + // (must be the same) + + let data_len = if self.data.len() as u16 > 0xff || ext == Le::Long { + vec![0, + (self.data.len() as u16 >> 8) as u8, + (self.data.len() as u16 & 255) as u8] + } else { + vec![self.data.len() as u8] + }; + + let mut buf = vec![self.cla, self.ins, self.p1, self.p2]; + + if !self.data.is_empty() { + buf.extend_from_slice(&data_len); + buf.extend_from_slice(&self.data[..]); + } + + // Le + match ext { + // FIXME? (from scd/apdu.c): + // /* T=0 does not allow the use of Lc together with L + // e; + // thus disable Le in this case. */ + // if (reader_table[slot].is_t0) + // le = -1; + + Le::None => (), + + Le::Short => buf.push(0), + Le::Long => { + buf.push(0); + buf.push(0); + if self.data.is_empty() { + buf.push(0) + } + } + } + + Ok(buf) + } +} diff --git a/openpgp-card/src/apdu/commands.rs b/openpgp-card/src/apdu/commands.rs new file mode 100644 index 0000000..c8cbbec --- /dev/null +++ b/openpgp-card/src/apdu/commands.rs @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +/// APDU Commands for OpenPGP card operations + +use crate::apdu::command::Command; + +/// Select the OpenPGP applet +pub fn select_openpgp() -> Command { + Command::new( + 0x00, 0xA4, 0x04, 0x00, + vec![0xD2, 0x76, 0x00, 0x01, 0x24, 0x01], + ) +} + +/// Get DO "Application related data" +pub fn get_application_data() -> Command { + Command::new(0x00, 0xCA, 0x00, 0x6E, vec![]) +} + +/// Get DO "Uniform resource locator" +pub fn get_url() -> Command { + Command::new(0x00, 0xCA, 0x5F, 0x50, vec![]) +} + +/// Get DO "Cardholder related data" +pub fn cardholder_related_data() -> Command { + Command::new(0x00, 0xCA, 0x00, 0x65, vec![]) +} + +/// Get DO "Security support template" +pub fn get_security_support_template() -> Command { + Command::new(0x00, 0xCA, 0x00, 0x7A, vec![]) +} + +/// Get DO "List of supported Algorithm attributes" +pub fn get_algo_list() -> Command { + Command::new(0x00, 0xCA, 0x00, 0xFA, vec![]) +} + +/// GET RESPONSE +pub fn get_response() -> Command { + Command::new(0x00, 0xC0, 0x00, 0x00, vec![]) +} + +/// VERIFY pin for PW1 (81) +pub fn verify_pw1_81(pin: Vec) -> Command { + Command::new(0x00, 0x20, 0x00, 0x81, pin) +} + +/// VERIFY pin for PW1 (82) +pub fn verify_pw1_82(pin: Vec) -> Command { + Command::new(0x00, 0x20, 0x00, 0x82, pin) +} + +/// VERIFY pin for PW3 (83) +pub fn verify_pw3(pin: Vec) -> Command { + Command::new(0x00, 0x20, 0x00, 0x83, pin) +} + +/// TERMINATE DF +pub fn terminate_df() -> Command { + Command::new(0x00, 0xe6, 0x00, 0x00, vec![]) +} + +/// ACTIVATE FILE +pub fn activate_file() -> Command { + Command::new(0x00, 0x44, 0x00, 0x00, vec![]) +} + + +/// 7.2.8 PUT DATA, +/// ('tag' must consist of either one or two bytes) +pub fn put_data(tag: &[u8], data: Vec) -> Command { + assert!(!tag.is_empty() && tag.len() <= 2); + + let (p1, p2) = if tag.len() == 2 { + (tag[0], tag[1]) + } else { + (0, tag[0]) + }; + Command::new(0x00, 0xda, p1, p2, data) +} + + +/// PUT DO Name +pub fn put_name(name: Vec) -> Command { + put_data(&[0x5b], name) +} + +/// PUT DO Language preferences +pub fn put_lang(lang: Vec) -> Command { + put_data(&[0x5f, 0x2d], lang) +} + +/// PUT DO Sex +pub fn put_sex(sex: u8) -> Command { + put_data(&[0x5f, 0x35], vec![sex]) +} + +/// PUT DO Uniform resource locator (URL) +pub fn put_url(url: Vec) -> Command { + put_data(&[0x5f, 0x50], url) +} + +/// Change PW1 (user pin). +/// This can be used to reset the counter and set a pin. +pub fn change_pw1(pin: Vec) -> Command { + Command::new(0x00, 0x2C, 0x02, 0x81, pin) +} + +/// Change PW3 (admin pin) +pub fn change_pw3(oldpin: Vec, newpin: Vec) -> Command { + let mut fullpin = oldpin; + fullpin.extend(newpin.iter()); + + Command::new(0x00, 0x24, 0x00, 0x83, fullpin) +} + +/// Creates new APDU for decryption operation +pub fn decryption(data: Vec) -> Command { + Command::new(0x00, 0x2A, 0x80, 0x86, data) +} + +/// Creates new APDU for decryption operation +pub fn signature(data: Vec) -> Command { + Command::new(0x00, 0x2A, 0x9e, 0x9a, data) +} diff --git a/openpgp-card/src/apdu/mod.rs b/openpgp-card/src/apdu/mod.rs new file mode 100644 index 0000000..002d9ad --- /dev/null +++ b/openpgp-card/src/apdu/mod.rs @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +pub mod command; +pub mod commands; +pub mod response; + +use std::convert::TryFrom; +use pcsc::Card; + +use crate::OpenPGPCard; +use crate::apdu::command::Command; +use crate::errors::{OcErrorStatus, OpenpgpCardError, SmartcardError}; +use crate::apdu::response::Response; + +#[derive(Clone, Copy, PartialEq)] +pub(crate) enum Le { None, Short, Long } + +/// Send a Command and return the result as a Response. +/// +/// If the reply is truncated, this fn assembles all the parts and returns +/// them as one aggregated Response. +pub(crate) fn send_command(card: &Card, cmd: Command, ext: Le, + oc: Option<&OpenPGPCard>) + -> Result { + let mut resp = Response::try_from( + send_command_low_level(&card, cmd, ext, oc)?)?; + + while resp.status()[0] == 0x61 { + // More data is available for this command from the card + + log::trace!(" response was truncated, getting more data"); + + // Get additional data + let next = Response::try_from + (send_command_low_level(&card, + commands::get_response(), ext, oc)?)?; + + // FIXME: first check for 0x61xx or 0x9000? + log::trace!(" appending {} bytes to response", next.raw_data().len()); + + // Append new data to resp.data and overwrite status. + resp.raw_mut_data().extend_from_slice(next.raw_data()); + resp.set_status(next.status()); + } + + log::trace!(" final response len: {}", resp.raw_data().len()); + + Ok(resp) +} + +/// Send the given Command (chained, if required) to the card and +/// return the response as a vector of `u8`. +/// +/// If the response is chained, this fn only returns one chunk, the caller +/// needs take care of chained responses +fn send_command_low_level(card: &Card, + cmd: Command, + ext: Le, + oc: Option<&OpenPGPCard>) + -> Result, OpenpgpCardError> { + log::trace!(" -> full APDU command: {:x?}", cmd); + log::trace!(" serialized: {:x?}", cmd.serialize(ext)); + + // default settings + let mut ext_support = false; + let mut chaining_support = false; + let mut chunk_size = 255; + + // Get feature configuration from card metadata + if let Some(oc) = oc { + if let Ok(hist) = oc.get_historical() { + if let Some(cc) = hist.get_card_capabilities() { + chaining_support = cc.get_command_chaining(); + ext_support = cc.get_extended_lc_le(); + } + } + + if let Ok(Some(eli)) = oc.get_extended_length_information() { + chunk_size = eli.max_command_bytes as usize; + } + } + + log::trace!("ext le/lc {}, chaining {}, command chunk size {}", + ext_support, chaining_support, chunk_size); + + // update Le setting to 'long', if we're using a larger chunk size + let ext = match (ext, chunk_size > 0xff) { + (Le::None, _) => Le::None, + (_, true) => Le::Long, + _ => ext + }; + + let buf_size = if !ext_support { + pcsc::MAX_BUFFER_SIZE + } else { + pcsc::MAX_BUFFER_SIZE_EXTENDED + }; + + let mut resp_buffer = vec![0; buf_size]; + + if chaining_support && !cmd.data.is_empty() { + // Send command in chained mode + + log::trace!("chained command mode"); + + // Break up payload into chunks that fit into one command, each + let chunks: Vec<_> = cmd.data.chunks(chunk_size).collect(); + + for (i, d) in chunks.iter().enumerate() { + let last = i == chunks.len() - 1; + let partial = + Command { + cla: if last { 0x00 } else { 0x10 }, + data: d.to_vec(), + ..cmd + }; + + let serialized = partial.serialize(ext). + map_err(OpenpgpCardError::InternalError)?; + log::trace!(" -> chunked APDU command: {:x?}", &serialized); + + let resp = card + .transmit(&serialized, &mut resp_buffer) + .map_err(|e| OpenpgpCardError::Smartcard(SmartcardError::Error( + format!("Transmit failed: {:?}", e))))?; + + log::trace!(" <- APDU chunk response: {:x?}", &resp); + + if resp.len() < 2 { + return Err(OcErrorStatus::ResponseLength(resp.len()).into()); + } + + if !last { + // check that the returned status is ok + let sw1 = resp[resp.len() - 2]; + let sw2 = resp[resp.len() - 1]; + + // ISO: "If SW1-SW2 is set to '6883', then the last + // command of the chain is expected." + if !((sw1 == 0x90 && sw2 == 0x00) + || (sw1 == 0x68 && sw2 == 0x83)) { + + // Unexpected status for a non-final chunked response + return Err(OcErrorStatus::from((sw1, sw2)).into()); + } + + // ISO: "If SW1-SW2 is set to '6884', then command + // chaining is not supported." + } else { + // this is the last Response in the chain -> return + return Ok(resp.to_vec()); + } + } + unreachable!("This state should be unreachable"); + } else { + let serialized = cmd.serialize(ext)?; + + let resp = card + .transmit(&serialized, &mut resp_buffer) + .map_err(|e| OpenpgpCardError::Smartcard(SmartcardError::Error( + format!("Transmit failed: {:?}", e))))?; + + + log::trace!(" <- APDU response: {:x?}", resp); + + Ok(resp.to_vec()) + } +} diff --git a/openpgp-card/src/apdu/response.rs b/openpgp-card/src/apdu/response.rs new file mode 100644 index 0000000..b8e6a9e --- /dev/null +++ b/openpgp-card/src/apdu/response.rs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::errors::OcErrorStatus; +use std::convert::TryFrom; + +/// APDU Response +#[allow(unused)] +#[derive(Clone, Debug)] +pub struct Response { + pub(self) data: Vec, + pub(self) sw1: u8, + pub(self) sw2: u8, +} + +impl Response { + pub fn check_ok(&self) -> Result<(), OcErrorStatus> { + if !self.is_ok() { + Err(OcErrorStatus::from((self.sw1, self.sw2))) + } else { + Ok(()) + } + } + + pub fn data(&self) -> Result<&[u8], OcErrorStatus> { + self.check_ok()?; + Ok(&self.data) + } + + /// access data even if the result status is an error status + pub(crate) fn raw_data(&self) -> &[u8] { + &self.data + } + + pub(crate) fn raw_mut_data(&mut self) -> &mut Vec { + &mut self.data + } + + pub(crate) fn set_status(&mut self, new_status: [u8; 2]) { + self.sw1 = new_status[0]; + self.sw2 = new_status[1]; + } + + pub fn status(&self) -> [u8; 2] { + [self.sw1, self.sw2] + } +} + +impl<'a> TryFrom<&[u8]> for Response { + type Error = OcErrorStatus; + + fn try_from(buf: &[u8]) -> Result { + let n = buf.len(); + if n < 2 { + return Err(OcErrorStatus::ResponseLength(buf.len())); + } + Ok(Response { + data: buf[..n - 2].into(), + sw1: buf[n - 2], + sw2: buf[n - 1], + }) + } +} + +impl TryFrom> for Response { + type Error = OcErrorStatus; + + fn try_from(mut data: Vec) -> Result { + let sw2 = data.pop() + .ok_or_else(|| OcErrorStatus::ResponseLength(data.len()))?; + let sw1 = data.pop() + .ok_or_else(|| OcErrorStatus::ResponseLength(data.len()))?; + + Ok(Response { data, sw1, sw2 }) + } +} + + +impl Response { + /// Is the response (0x90 0x00)? + pub fn is_ok(&self) -> bool { + self.sw1 == 0x90 && self.sw2 == 0x00 + } +} + +#[cfg(test)] +mod tests { + use crate::apdu::response::Response; + use std::convert::TryFrom; + + #[test] + fn test_two_bytes_data_response() { + let res = Response::try_from(vec![0x01, 0x02, 0x90, 0x00]).unwrap(); + assert_eq!(res.is_ok(), true); + assert_eq!(res.data, vec![0x01, 0x02]); + } + + #[test] + fn test_no_data_response() { + let res = Response::try_from(vec![0x90, 0x00]).unwrap(); + assert_eq!(res.is_ok(), true); + assert_eq!(res.data, vec![]); + } + + #[test] + fn test_more_data_response() { + let res = Response::try_from(vec![0xAB, 0x61, 0x02]).unwrap(); + assert_eq!(res.is_ok(), false); + assert_eq!(res.data, vec![0xAB]); + } +} diff --git a/openpgp-card/src/card.rs b/openpgp-card/src/card.rs new file mode 100644 index 0000000..2fe2ad3 --- /dev/null +++ b/openpgp-card/src/card.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use anyhow::Result; +use pcsc::{Card, Context, Scope, ShareMode, Protocols, Error}; + +use crate::errors; + + +pub fn get_cards() -> Result, errors::SmartcardError> { + let ctx = match Context::establish(Scope::User) { + Ok(ctx) => ctx, + Err(err) => return Err(errors::SmartcardError::ContextError(err.to_string())), + }; + + // List available readers. + let mut readers_buf = [0; 2048]; + let readers = match ctx.list_readers(&mut readers_buf) { + Ok(readers) => readers, + Err(err) => { + return Err(errors::SmartcardError::ReaderError(err.to_string())); + } + }; + + let mut found_reader = false; + + let mut cards = vec![]; + + // Find a reader with a SmartCard. + for reader in readers { + // We've seen at least one smartcard reader + found_reader = true; + + // Try connecting to card in this reader + let card = match ctx.connect(reader, ShareMode::Shared, Protocols::ANY) { + Ok(card) => card, + Err(Error::NoSmartcard) => { + continue; // try next reader + } + Err(err) => { + return Err(errors::SmartcardError::SmartCardConnectionError( + err.to_string(), + )); + } + }; + + cards.push(card); + } + + if !found_reader { + Err(errors::SmartcardError::NoReaderFoundError) + } else { + Ok(cards) + } +} diff --git a/openpgp-card/src/errors.rs b/openpgp-card/src/errors.rs new file mode 100644 index 0000000..aeeab60 --- /dev/null +++ b/openpgp-card/src/errors.rs @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum OpenpgpCardError { + #[error("Error interacting with smartcard {0}")] + Smartcard(SmartcardError), + + #[error("OpenPGP card error status {0}")] + OcStatus(OcErrorStatus), + + #[error("Internal error {0}")] + InternalError(anyhow::Error), +} + +impl From for OpenpgpCardError { + fn from(oce: OcErrorStatus) -> Self { + OpenpgpCardError::OcStatus(oce) + } +} + +impl From for OpenpgpCardError { + fn from(ae: anyhow::Error) -> Self { + OpenpgpCardError::InternalError(ae) + } +} + +#[derive(Error, Debug)] +pub enum OcErrorStatus { + #[error("Selected file or DO in termination state")] + TerminationState, + + #[error("Password not checked, {0} allowed retries")] + PasswordNotChecked(u8), + + #[error("Triggering by the card {0}")] + TriggeringByCard(u8), + + #[error("Memory failure")] + MemoryFailure, + + #[error("Security-related issues (reserved for UIF in this application)")] + SecurityRelatedIssues, + + #[error("Wrong length (Lc and/or Le)")] + WrongLength, + + #[error("Logical channel not supported")] + LogicalChannelNotSupported, + + #[error("Secure messaging not supported")] + SecureMessagingNotSupported, + + #[error("Last command of the chain expected")] + LastCommandOfChainExpected, + + #[error("Command chaining not supported")] + CommandChainingUnsupported, + + #[error("Security status not satisfied")] + SecurityStatusNotSatisfied, + + #[error("Authentication method blocked")] + AuthenticationMethodBlocked, + + #[error("Condition of use not satisfied")] + ConditionOfUseNotSatisfied, + + #[error("Expected secure messaging DOs missing (e. g. SM-key)")] + ExpectedSecureMessagingDOsMissing, + + #[error("SM data objects incorrect (e. g. wrong TLV-structure in command data)")] + SMDataObjectsIncorrect, + + #[error("Incorrect parameters in the command data field")] + IncorrectParametersCommandDataField, + + #[error("File or application not found")] + FileOrApplicationNotFound, + + #[error("Referenced data, reference data or DO not found")] + ReferencedDataNotFound, + + #[error("Wrong parameters P1-P2")] + WrongParametersP1P2, + + #[error("Instruction code (INS) not supported or invalid")] + INSNotSupported, + + #[error("Class (CLA) not supported")] + CLANotSupported, + + #[error("No precise diagnosis")] + NoPreciseDiagnosis, + + #[error("Unknown OpenPGP card status: [{0}, {1}]")] + UnknownStatus(u8, u8), + + #[error("Unexpected response length: {0}")] + ResponseLength(usize), + +} + +impl From<(u8, u8)> for OcErrorStatus { + fn from(status: (u8, u8)) -> Self { + match (status.0, status.1) { + (0x62, 0x85) => OcErrorStatus::TerminationState, + (0x63, 0xC0..=0xCF) => + OcErrorStatus::PasswordNotChecked(status.1 & 0xf), + (0x64, 0x02..=0x80) => + OcErrorStatus::TriggeringByCard(status.1), + (0x65, 0x01) => OcErrorStatus::MemoryFailure, + (0x66, 0x00) => OcErrorStatus::SecurityRelatedIssues, + (0x67, 0x00) => OcErrorStatus::WrongLength, + (0x68, 0x81) => OcErrorStatus::LogicalChannelNotSupported, + (0x68, 0x82) => OcErrorStatus::SecureMessagingNotSupported, + (0x68, 0x83) => OcErrorStatus::LastCommandOfChainExpected, + (0x68, 0x84) => OcErrorStatus::CommandChainingUnsupported, + (0x69, 0x82) => OcErrorStatus::SecurityStatusNotSatisfied, + (0x69, 0x83) => OcErrorStatus::AuthenticationMethodBlocked, + (0x69, 0x85) => OcErrorStatus::ConditionOfUseNotSatisfied, + (0x69, 0x87) => OcErrorStatus::ExpectedSecureMessagingDOsMissing, + (0x69, 0x88) => OcErrorStatus::SMDataObjectsIncorrect, + (0x6A, 0x80) => OcErrorStatus::IncorrectParametersCommandDataField, + (0x6A, 0x82) => OcErrorStatus::FileOrApplicationNotFound, + (0x6A, 0x88) => OcErrorStatus::ReferencedDataNotFound, + (0x6B, 0x00) => OcErrorStatus::WrongParametersP1P2, + (0x6D, 0x00) => OcErrorStatus::INSNotSupported, + (0x6E, 0x00) => OcErrorStatus::CLANotSupported, + (0x6F, 0x00) => OcErrorStatus::NoPreciseDiagnosis, + _ => OcErrorStatus::UnknownStatus(status.0, status.1) + } + } +} + +#[derive(Error, Debug)] +pub enum SmartcardError { + #[error("Failed to create a pcsc smartcard context {0}")] + ContextError(String), + + #[error("Failed to list readers: {0}")] + ReaderError(String), + + #[error("No reader found.")] + NoReaderFoundError, + + #[error("The requested card was not found.")] + CardNotFound, + + #[error("Failed to connect to the card: {0}")] + SmartCardConnectionError(String), + + #[error("Generic SmartCard Error: {0}")] + Error(String), +} diff --git a/openpgp-card/src/key_upload.rs b/openpgp-card/src/key_upload.rs new file mode 100644 index 0000000..8e90aa9 --- /dev/null +++ b/openpgp-card/src/key_upload.rs @@ -0,0 +1,383 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use anyhow::{anyhow, Result}; + +use crate::{KeyType, OpenPGPCardAdmin, tlv, + CardUploadableKey, EccKey, RSAKey, EccType, PrivateKeyMaterial}; +use crate::apdu; +use crate::apdu::commands; +use crate::apdu::command::Command; +use crate::apdu::Le; +use crate::parse::algo_attrs::{Algo, RsaAttrs}; +use crate::parse::algo_info::AlgoInfo; +use crate::tlv::{Tlv, TlvEntry, tag::Tag}; +use crate::errors::OpenpgpCardError; + + +/// Upload an explicitly selected Key to the card as a specific KeyType. +/// +/// The client needs to make sure that the key is suitable for `key_type`. +pub(crate) fn upload_key( + oca: &OpenPGPCardAdmin, + key: Box, + key_type: KeyType, +) -> Result<(), OpenpgpCardError> { + // FIXME: the list of algorithms is retrieved multiple times, it should + // be cached + let algo_list = oca.list_supported_algo()?; + + let (algo_cmd, key_cmd) = + match key.get_key()? { + PrivateKeyMaterial::R(rsa_key) => { + // RSA bitsize + // (round up to 4-bytes, in case the key has 8+ leading zeros) + let rsa_bits = + (((rsa_key.get_n().len() * 8 + 31) / 32) * 32) as u16; + + // FIXME: deal with absence of algo list (unwrap!) + // Get suitable algorithm from card's list + let algo = get_card_algo_rsa(algo_list.unwrap(), key_type, rsa_bits); + + let algo_cmd = rsa_algo_attrs_cmd(key_type, rsa_bits, &algo)?; + let key_cmd = rsa_key_cmd(key_type, rsa_key, &algo)?; + + // Return commands + (algo_cmd, key_cmd) + } + PrivateKeyMaterial::E(ecc_key) => { + // Initially there were checks of the following form, here. + // However, some cards seem to report erroneous + // information about supported algorithms. + // (e.g. Yk5 reports support of EdDSA over Cv25519/Ed25519, + // but not ECDH). + + // if !check_card_algo_*(algo_list.unwrap(), + // key_type, ecc_key.get_oid()) { + // // Error + // } + + let algo_cmd = ecc_algo_attrs_cmd(key_type, + ecc_key.get_oid(), + ecc_key.get_type()); + + let key_cmd = ecc_key_cmd(ecc_key, key_type)?; + + (algo_cmd, key_cmd) + } + }; + + copy_key_to_card(oca, key_type, key.get_ts(), key.get_fp(), algo_cmd, + key_cmd)?; + + Ok(()) +} + +// FIXME: refactor, these checks currently pointlessly duplicate code +fn get_card_algo_rsa(algo_list: AlgoInfo, key_type: KeyType, rsa_bits: u16) + -> RsaAttrs { + + // Find suitable algorithm parameters (from card's list of algorithms). + // FIXME: handle "no list available" (older cards?) + // (Current algo parameters of the key slot should be used, then (?)) + + // Get Algos for this keytype + let keytype_algos: Vec<_> = algo_list.get_by_keytype(key_type); + // Get RSA algo attributes + let rsa_algos: Vec<_> = keytype_algos.iter() + .map(|a| + { + if let Algo::Rsa(r) = a { + Some(r) + } else { + None + } + }) + .flatten() + .collect(); + + // Filter card algorithms by rsa bitlength of the key we want to upload + let algo: Vec<_> = rsa_algos.iter() + .filter(|&a| a.len_n == rsa_bits) + .collect(); + + // FIXME: handle error if no algo found + let algo = *algo[0]; + + algo.clone() +} + +// FIXME: refactor, these checks currently pointlessly duplicate code +fn check_card_algo_ecdh(algo_list: AlgoInfo, key_type: KeyType, oid: &[u8]) -> bool { + // Find suitable algorithm parameters (from card's list of algorithms). + // FIXME: handle "no list available" (older cards?) + // (Current algo parameters of the key slot should be used, then (?)) + + // Get Algos for this keytype + let keytype_algos: Vec<_> = algo_list.get_by_keytype(key_type); + + // Get attributes + let ecdh_algos: Vec<_> = keytype_algos.iter() + .map(|a| + { + if let Algo::Ecdh(e) = a { + Some(e) + } else { + None + } + }) + .flatten() + .collect(); + + // Check if this OID exists in the supported algorithms + ecdh_algos.iter().any(|e| e.oid == oid) +} + +// FIXME: refactor, these checks currently pointlessly duplicate code +fn check_card_algo_ecdsa(algo_list: AlgoInfo, + key_type: KeyType, oid: &[u8]) -> bool { + // Find suitable algorithm parameters (from card's list of algorithms). + // FIXME: handle "no list available" (older cards?) + // (Current algo parameters of the key slot should be used, then (?)) + + // Get Algos for this keytype + let keytype_algos: Vec<_> = algo_list.get_by_keytype(key_type); + + // Get attributes + let ecdsa_algos: Vec<_> = keytype_algos.iter() + .map(|a| + { + if let Algo::Ecdsa(e) = a { + Some(e) + } else { + None + } + }) + .flatten() + .collect(); + + // Check if this OID exists in the supported algorithms + ecdsa_algos.iter().any(|e| e.oid == oid) +} + +// FIXME: refactor, these checks currently pointlessly duplicate code +fn check_card_algo_eddsa(algo_list: AlgoInfo, + key_type: KeyType, oid: &[u8]) -> bool { + // Find suitable algorithm parameters (from card's list of algorithms). + // FIXME: handle "no list available" (older cards?) + // (Current algo parameters of the key slot should be used, then (?)) + + // Get Algos for this keytype + let keytype_algos: Vec<_> = algo_list.get_by_keytype(key_type); + + // Get attributes + let eddsa_algos: Vec<_> = keytype_algos.iter() + .map(|a| + { + if let Algo::Eddsa(e) = a { + Some(e) + } else { + None + } + }) + .flatten() + .collect(); + + // Check if this OID exists in the supported algorithms + eddsa_algos.iter().any(|e| e.oid == oid) +} + +fn ecc_key_cmd(ecc_key: Box, key_type: KeyType) + -> Result { + let scalar_data = ecc_key.get_scalar(); + let scalar_len = scalar_data.len() as u8; + + // 1) "Control Reference Template" + let crt = get_crt(key_type)?; + + // 2) "Cardholder private key template" (7F48) + let cpkt = Tlv(Tag(vec![0x7F, 0x48]), + TlvEntry::S(vec![0x92, scalar_len])); + + // 3) "Cardholder private key" (5F48) + let cpk = Tlv(Tag(vec![0x5F, 0x48]), TlvEntry::S(scalar_data.to_vec())); + + + // "Extended header list (DO 4D)" (contains the three inner TLV) + let ehl = Tlv(Tag(vec![0x4d]), + TlvEntry::C(vec![crt, cpkt, cpk])); + + + // The key import uses a PUT DATA command with odd INS (DB) and an + // Extended header list (DO 4D) as described in ISO 7816-8 + + Ok(Command::new(0x00, 0xDB, 0x3F, 0xFF, + ehl.serialize().to_vec())) +} + +fn get_crt(key_type: KeyType) -> Result { + // "Control Reference Template" (0xB8 | 0xB6 | 0xA4) + let tag = match key_type { + KeyType::Decryption => 0xB8, + KeyType::Signing => 0xB6, + KeyType::Authentication => 0xA4, + _ => return Err(OpenpgpCardError::InternalError + (anyhow!("Unexpected KeyType"))) + }; + Ok(Tlv(Tag(vec![tag]), TlvEntry::S(vec![]))) +} + +fn rsa_key_cmd(key_type: KeyType, + rsa_key: Box, + algo_attrs: &RsaAttrs) -> Result { + + // Assemble key command, which contains three sub-TLV: + + // 1) "Control Reference Template" + let crt = get_crt(key_type)?; + + + // 2) "Cardholder private key template" (7F48) + // "describes the input and the length of the content of the following DO" + + // collect the value for this DO + let mut value = vec![]; + + // Length of e in bytes, rounding up from the bit value in algo. + let len_e_bytes = ((algo_attrs.len_e + 7) / 8) as u8; + + value.push(0x91); + // len_e in bytes has a value of 3-4, it doesn't need TLV encoding + value.push(len_e_bytes); + + // len_p and len_q are len_n/2 (value from card algorithm list). + // transform unit from bits to bytes. + let len_p_bytes: u16 = algo_attrs.len_n / 2 / 8; + let len_q_bytes: u16 = algo_attrs.len_n / 2 / 8; + + value.push(0x92); + // len p in bytes, TLV-encoded + value.extend_from_slice(&tlv::tlv_encode_length(len_p_bytes)); + + value.push(0x93); + // len q in bytes, TLV-encoded + value.extend_from_slice(&tlv::tlv_encode_length(len_q_bytes)); + + let cpkt = Tlv(Tag(vec![0x7F, 0x48]), TlvEntry::S(value)); + + + // 3) "Cardholder private key" (5F48) + // + // "represents a concatenation of the key data elements according to + // the definitions in DO '7F48'." + let mut keydata = Vec::new(); + + let e_as_bytes = rsa_key.get_e(); + + // Push e, padded to length with zero bytes from the left + for _ in e_as_bytes.len()..(len_e_bytes as usize) { + keydata.push(0); + } + keydata.extend(e_as_bytes); + + // FIXME: do p/q need to be padded from the left when many leading + // bits are zero? + keydata.extend(rsa_key.get_p().iter()); + keydata.extend(rsa_key.get_q().iter()); + + let cpk = Tlv(Tag(vec![0x5F, 0x48]), TlvEntry::S(keydata)); + + + // "Extended header list (DO 4D)" + let ehl = Tlv(Tag(vec![0x4d]), TlvEntry::C(vec![crt, cpkt, cpk])); + + + // The key import uses a PUT DATA command with odd INS (DB) and an + // Extended header list (DO 4D) as described in ISO 7816-8 + + Ok(Command::new(0x00, 0xDB, 0x3F, 0xFF, + ehl.serialize().to_vec())) +} + +/// Set algorithm attributes [4.4.3.9 Algorithm Attributes] +fn rsa_algo_attrs_cmd(key_type: KeyType, + rsa_bits: u16, + algo_attrs: &RsaAttrs) -> + Result { + + // Algorithm ID (01 = RSA (Encrypt or Sign)) + let mut algo_attributes = vec![0x01]; + + // Length of modulus n in bit + algo_attributes.extend(rsa_bits.to_be_bytes()); + + // Length of public exponent e in bit + algo_attributes.push(0x00); + algo_attributes.push(algo_attrs.len_e as u8); + + // Import-Format of private key + // (This fn currently assumes import_format "00 = standard (e, p, q)") + if algo_attrs.import_format != 0 { + return Err( + anyhow!("Unexpected RSA input format (only 0 is supported)")); + } + + algo_attributes.push(algo_attrs.import_format); + + // Command to PUT the algorithm attributes + Ok(commands::put_data(&[key_type.get_algorithm_tag()], algo_attributes)) +} + +/// Set algorithm attributes [4.4.3.9 Algorithm Attributes] +fn ecc_algo_attrs_cmd(key_type: KeyType, oid: &[u8], ecc_type: EccType) + -> Command { + let algo_id = match ecc_type { + EccType::EdDSA => 0x16, + EccType::ECDH => 0x12, + EccType::ECDSA => 0x13, + }; + + let mut algo_attributes = vec![algo_id]; + algo_attributes.extend(oid); + // Leave Import-Format unset, for default (pg. 35) + + // Command to PUT the algorithm attributes + commands::put_data(&[key_type.get_algorithm_tag()], algo_attributes) +} + +fn copy_key_to_card(oca: &OpenPGPCardAdmin, + key_type: KeyType, + ts: u64, + fp: Vec, + algo_cmd: Command, + key_cmd: Command) + -> Result<(), OpenpgpCardError> { + let fp_cmd = + commands::put_data(&[key_type.get_fingerprint_put_tag()], fp); + + // Timestamp update + let time_value: Vec = ts + .to_be_bytes() + .iter() + .skip_while(|&&e| e == 0) + .copied() + .collect(); + + let time_cmd = + commands::put_data(&[key_type.get_timestamp_put_tag()], time_value); + + + // Send all the commands + + let ext = Le::None; // FIXME?! + + // FIXME: Only write algo attributes to the card if "extended + // capabilities" show that they are changeable! + apdu::send_command(oca.card(), algo_cmd, ext, Some(oca))?.check_ok()?; + + apdu::send_command(oca.card(), key_cmd, ext, Some(oca))?.check_ok()?; + apdu::send_command(oca.card(), fp_cmd, ext, Some(oca))?.check_ok()?; + apdu::send_command(oca.card(), time_cmd, ext, Some(oca))?.check_ok()?; + + Ok(()) +} diff --git a/openpgp-card/src/lib.rs b/openpgp-card/src/lib.rs new file mode 100644 index 0000000..8ef1a4c --- /dev/null +++ b/openpgp-card/src/lib.rs @@ -0,0 +1,762 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::convert::TryFrom; + +use anyhow::{anyhow, Result}; +use pcsc::*; + +use apdu::{commands, Le, response::Response}; +use parse::{algo_attrs::Algo, + algo_info::AlgoInfo, + application_id::ApplicationId, + cardholder::CardHolder, + extended_cap::ExtendedCap, + extended_cap::Features, + extended_length_info::ExtendedLengthInfo, + fingerprint, + historical::Historical, + KeySet}; +use tlv::Tlv; + +use crate::errors::{OpenpgpCardError, SmartcardError}; +use crate::tlv::tag::Tag; +use crate::tlv::TlvEntry; +use std::ops::Deref; + +pub mod errors; +mod apdu; +mod card; +mod key_upload; +mod parse; +mod tlv; + + +pub enum Hash<'a> { + SHA256([u8; 0x20]), + SHA384([u8; 0x30]), + SHA512([u8; 0x40]), + EdDSA(&'a [u8]), // FIXME? +} + +impl Hash<'_> { + fn oid(&self) -> Option<&'static [u8]> { + match self { + Self::SHA256(_) => + Some(&[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]), + Self::SHA384(_) => + Some(&[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02]), + Self::SHA512(_) => + Some(&[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]), + Self::EdDSA(_) => None + } + } + + fn digest(&self) -> &[u8] { + match self { + Self::SHA256(d) => &d[..], + Self::SHA384(d) => &d[..], + Self::SHA512(d) => &d[..], + Self::EdDSA(d) => d + } + } +} + + +/// A PGP-implementation-agnostic wrapper for private key data, to upload +/// to an OpenPGP card +pub trait CardUploadableKey { + /// private key data + fn get_key(&self) -> Result; + + /// timestamp of (sub)key creation + fn get_ts(&self) -> u64; + + /// fingerprint + fn get_fp(&self) -> Vec; +} + +/// Algorithm-independent container for private key material to upload to +/// an OpenPGP card +pub enum PrivateKeyMaterial { + R(Box), + E(Box), +} + +/// RSA-specific container for private key material to upload to an OpenPGP +/// card. +pub trait RSAKey { + fn get_e(&self) -> &[u8]; + fn get_n(&self) -> &[u8]; + fn get_p(&self) -> &[u8]; + fn get_q(&self) -> &[u8]; +} + +/// ECC-specific container for private key material to upload to an OpenPGP +/// card. +pub trait EccKey { + fn get_oid(&self) -> &[u8]; + fn get_scalar(&self) -> &[u8]; + fn get_type(&self) -> EccType; +} + +#[derive(Clone, Copy)] +pub enum EccType { + ECDH, + EdDSA, + ECDSA, +} + +/// Container for data to be decrypted on an OpenPGP card. +pub enum DecryptMe<'a> { + // message/ciphertext + RSA(&'a [u8]), + + // ephemeral + ECDH(&'a [u8]), +} + + +#[derive(Debug)] +pub enum Sex { NotKnown, Male, Female, NotApplicable } + +impl Sex { + pub fn as_u8(&self) -> u8 { + match self { + Sex::NotKnown => 0x30, + Sex::Male => 0x31, + Sex::Female => 0x32, + Sex::NotApplicable => 0x39, + } + } +} + +impl From for Sex { + fn from(s: u8) -> Self { + match s { + 31 => Sex::Male, + 32 => Sex::Female, + 39 => Sex::NotApplicable, + _ => Sex::NotKnown, + } + } +} + + +/// Enum to identify one of the Key-slots on an OpenPGP card +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum KeyType { + // Algorithm attributes signature (C1) + Signing, + + // Algorithm attributes decryption (C2) + Decryption, + + // Algorithm attributes authentication (C3) + Authentication, + + // Algorithm attributes Attestation key (DA, Yubico) + Attestation, +} + +impl KeyType { + /// Get C1/C2/C3/DA values for this KeyTypes, to use as Tag + pub fn get_algorithm_tag(&self) -> u8 { + use KeyType::*; + + match self { + Signing => 0xC1, + Decryption => 0xC2, + Authentication => 0xC3, + Attestation => 0xDA, + } + } + + /// Get C7/C8/C9/DB values for this KeyTypes, to use as Tag. + /// + /// (NOTE: these Tags are only used for "PUT DO", but GETting + /// fingerprint information from the card uses the combined Tag C5) + pub fn get_fingerprint_put_tag(&self) -> u8 { + use KeyType::*; + + match self { + Signing => 0xC7, + Decryption => 0xC8, + Authentication => 0xC9, + Attestation => 0xDB, + } + } + + /// Get CE/CF/D0/DD values for this KeyTypes, to use as Tag. + /// + /// (NOTE: these Tags are only used for "PUT DO", but GETting + /// timestamp information from the card uses the combined Tag CD) + pub fn get_timestamp_put_tag(&self) -> u8 { + use KeyType::*; + + match self { + Signing => 0xCE, + Decryption => 0xCF, + Authentication => 0xD0, + Attestation => 0xDD, + } + } +} + +/// Representation of an opened OpenPGP card, with default privileges (i.e. +/// no passwords have been verified) +pub struct OpenPGPCard { + card: Card, + + // Cache of "application related data". + // + // FIXME: Should be invalidated when changing data on the card! + // (e.g. uploading keys, etc) + ard: Tlv, +} + +impl OpenPGPCard { + /// Get all cards that can be opened as an OpenPGP card applet + pub fn list_cards() -> Result> { + let cards = card::get_cards().map_err(|err| anyhow!(err))?; + let ocs: Vec<_> = cards.into_iter().map(Self::open_card) + .map(|oc| oc.ok()).flatten().collect(); + + Ok(ocs) + } + + /// Find an OpenPGP card by serial number and return it. + pub fn open_by_serial(serial: &str) -> Result { + let cards = card::get_cards() + .map_err(|e| OpenpgpCardError::Smartcard( + SmartcardError::Error(format!("{:?}", e))))?; + + for card in cards { + let res = Self::open_card(card); + if let Ok(opened_card) = res { + let res = opened_card.get_aid(); + if let Ok(aid) = res { + if aid.serial() == serial { + return Ok(opened_card); + } + } + } + } + + Err(OpenpgpCardError::Smartcard(SmartcardError::CardNotFound)) + } + + /// Open connection to some card and select the openpgp applet + pub fn open_yolo() -> Result { + let mut cards = card::get_cards() + .map_err(|e| OpenpgpCardError::Smartcard( + SmartcardError::Error(format!("{:?}", e))))?; + + // randomly use the first card in the list + let card = cards.swap_remove(0); + + Self::open_card(card) + } + + /// Open connection to a specific card and select the openpgp applet + fn open_card(card: Card) -> Result { + let select_openpgp = commands::select_openpgp(); + + let resp = + apdu::send_command(&card, select_openpgp, Le::Short, None)?; + + if resp.is_ok() { + // read and cache "application related data" + let ard = Self::get_app_data(&card)?; + + Ok(Self { card, ard }) + } else { + Err(anyhow!("Couldn't open OpenPGP application").into()) + } + } + + // --- application data --- + + /// Load "application related data". + /// + /// This is done once, after opening the OpenPGP card applet + /// (the data is stored in the OpenPGPCard object). + fn get_app_data(card: &Card) -> Result { + let ad = commands::get_application_data(); + let resp = apdu::send_command(card, ad, Le::Short, None)?; + let entry = TlvEntry::from(resp.data()?, true)?; + + log::trace!(" App data TlvEntry: {:x?}", entry); + + Ok(Tlv(Tag::from([0x6E]), entry)) + } + + pub fn get_aid(&self) -> Result { + // get from cached "application related data" + let aid = self.ard.find(&Tag::from([0x4F])); + + if let Some(aid) = aid { + Ok(ApplicationId::try_from(&aid.serialize()[..])?) + } else { + Err(anyhow!("Couldn't get Application ID.").into()) + } + } + + pub fn get_historical(&self) -> Result { + // get from cached "application related data" + let hist = self.ard.find(&Tag::from([0x5F, 0x52])); + + if let Some(hist) = hist { + log::debug!("Historical bytes: {:x?}", hist); + Historical::from(&hist.serialize()) + } else { + Err(anyhow!("Failed to get historical bytes.").into()) + } + } + + pub fn get_extended_length_information(&self) -> Result> { + // get from cached "application related data" + let eli = self.ard.find(&Tag::from([0x7F, 0x66])); + + log::debug!("Extended length information: {:x?}", eli); + + if let Some(eli) = eli { + // The card has returned extended length information + Ok(Some(ExtendedLengthInfo::from(&eli.serialize()[..])?)) + } else { + // The card didn't return this (optional) DO. That is ok. + Ok(None) + } + } + + pub fn get_general_feature_management() -> Option { + unimplemented!() + } + + pub fn get_discretionary_data_objects() { + unimplemented!() + } + + pub fn get_extended_capabilities(&self) -> Result { + // get from cached "application related data" + let ecap = self.ard.find(&Tag::from([0xc0])); + + if let Some(ecap) = ecap { + Ok(ExtendedCap::try_from(&ecap.serialize()[..])?) + } else { + Err(anyhow!("Failed to get extended capabilities.").into()) + } + } + + pub fn get_algorithm_attributes(&self, key_type: KeyType) -> Result { + // get from cached "application related data" + let aa = self.ard.find(&Tag::from([key_type.get_algorithm_tag()])); + + if let Some(aa) = aa { + Algo::try_from(&aa.serialize()[..]) + } else { + Err(anyhow!("Failed to get algorithm attributes for {:?}.", + key_type)) + } + } + + pub fn get_pw_status_bytes() { + unimplemented!() + } + + pub fn get_fingerprints(&self) -> Result, OpenpgpCardError> { + // Get from cached "application related data" + let fp = self.ard.find(&Tag::from([0xc5])); + + if let Some(fp) = fp { + let fp = fingerprint::from(&fp.serialize())?; + + log::debug!("Fp: {:x?}", fp); + + Ok(fp) + } else { + Err(anyhow!("Failed to get fingerprints.").into()) + } + } + + pub fn get_ca_fingerprints(&self) { + unimplemented!() + } + + pub fn get_key_generation_times() { + unimplemented!() + } + + pub fn get_key_information() { + unimplemented!() + } + + pub fn get_uif_pso_cds() { + unimplemented!() + } + + pub fn get_uif_pso_dec() { + unimplemented!() + } + + pub fn get_uif_pso_aut() { + unimplemented!() + } + pub fn get_uif_attestation() { + unimplemented!() + } + + // --- optional private DOs (0101 - 0104) --- + + // --- login data (5e) --- + + // --- URL (5f50) --- + + pub fn get_url(&self) -> Result { + let _eli = self.get_extended_length_information()?; + + // FIXME: figure out Le + let resp = apdu::send_command(&self.card, commands::get_url(), Le::Long, Some(self))?; + + log::trace!(" final response: {:x?}, data len {}", + resp, resp.raw_data().len()); + + Ok(String::from_utf8_lossy(resp.data()?).to_string()) + } + + // --- cardholder related data (65) --- + pub fn get_cardholder_related_data(&self) -> Result { + let crd = commands::cardholder_related_data(); + let resp = apdu::send_command(&self.card, crd, Le::Short, Some(self))?; + resp.check_ok()?; + + CardHolder::try_from(resp.data()?) + } + + // --- security support template (7a) --- + pub fn get_security_support_template(&self) -> Result { + let sst = commands::get_security_support_template(); + + let ext = self.get_extended_length_information()?.is_some(); + let ext = if ext { Le::Long } else { Le::Short }; + + let resp = apdu::send_command(&self.card, sst, ext, Some(self))?; + resp.check_ok()?; + + log::trace!(" final response: {:x?}, data len {}", + resp, resp.data()?.len()); + + Tlv::try_from(resp.data()?) + } + + // DO "Algorithm Information" (0xFA) + pub fn list_supported_algo(&self) -> Result> { + // The DO "Algorithm Information" (Tag FA) shall be present if + // Algorithm attributes can be changed + let ec = self.get_extended_capabilities()?; + if !ec.features.contains(&Features::AlgoAttrsChangeable) { + // Algorithm attributes can not be changed, + // list_supported_algo is not supported + return Ok(None); + } + + // FIXME: this is a temporary hack! + let eli = self.get_extended_length_information()?; + let ext = eli.is_some() && ec.max_len_special_do > 255; + let ext = if ext { Le::Long } else { Le::Short }; + + let resp = apdu::send_command(&self.card, commands::get_algo_list(), ext, Some(self))?; + resp.check_ok()?; + + let ai = AlgoInfo::try_from(resp.data()?)?; + Ok(Some(ai)) + } + + // ---------- + + /// Delete all state on this OpenPGP card + pub fn factory_reset(&self) -> Result<()> { + // send 4 bad requests to verify pw1 + // [apdu 00 20 00 81 08 40 40 40 40 40 40 40 40] + for _ in 0..4 { + let verify = commands::verify_pw1_81([0x40; 8].to_vec()); + let resp = apdu::send_command(&self.card, verify, Le::None, Some(self))?; + if !(resp.status() == [0x69, 0x82] + || resp.status() == [0x69, 0x83]) { + return Err(anyhow!("Unexpected status for reset, at pw1.")); + } + } + + // send 4 bad requests to verify pw3 + // [apdu 00 20 00 83 08 40 40 40 40 40 40 40 40] + for _ in 0..4 { + let verify = commands::verify_pw3([0x40; 8].to_vec()); + let resp = apdu::send_command(&self.card, verify, Le::None, Some(self))?; + + if !(resp.status() == [0x69, 0x82] + || resp.status() == [0x69, 0x83]) { + return Err(anyhow!("Unexpected status for reset, at pw3.")); + } + } + + // terminate_df [apdu 00 e6 00 00] + let term = commands::terminate_df(); + let resp = apdu::send_command(&self.card, term, Le::None, Some(self))?; + resp.check_ok()?; + + // activate_file [apdu 00 44 00 00] + let act = commands::activate_file(); + let resp = apdu::send_command(&self.card, act, Le::None, Some(self))?; + resp.check_ok()?; + + // FIXME: does the connection need to be re-opened on some cards, + // after reset?! + + Ok(()) + } + + pub fn verify_pw1_81(self, pin: &str) + -> Result { + assert!(pin.len() >= 6); // FIXME: Err + + let verify = commands::verify_pw1_81(pin.as_bytes().to_vec()); + let res = + apdu::send_command(&self.card, verify, Le::None, Some(&self)); + + if let Ok(resp) = res { + if resp.is_ok() { + return Ok(OpenPGPCardUser { oc: self }); + } + } + + Err(self) + } + + pub fn verify_pw1_82(self, pin: &str) + -> Result { + assert!(pin.len() >= 6); // FIXME: Err + + let verify = commands::verify_pw1_82(pin.as_bytes().to_vec()); + let res = + apdu::send_command(&self.card, verify, Le::None, Some(&self)); + + if let Ok(resp) = res { + if resp.is_ok() { + return Ok(OpenPGPCardUser { oc: self }); + } + } + + Err(self) + } + + pub fn verify_pw3(self, pin: &str) -> Result { + assert!(pin.len() >= 8); // FIXME: Err + + let verify = commands::verify_pw3(pin.as_bytes().to_vec()); + let res = + apdu::send_command(&self.card, verify, Le::None, Some(&self)); + + if let Ok(resp) = res { + if resp.is_ok() { + return Ok(OpenPGPCardAdmin { oc: self }); + } + } + + Err(self) + } +} + + +/// An OpenPGP card after successful verification of PW1 (needs to be split +/// further to model authentication for signing) +pub struct OpenPGPCardUser { + oc: OpenPGPCard, +} + +/// Allow access to fn of OpenPGPCard, through OpenPGPCardUser. +impl Deref for OpenPGPCardUser { + type Target = OpenPGPCard; + + fn deref(&self) -> &Self::Target { + &self.oc + } +} + +impl OpenPGPCardUser { + /// Decrypt the ciphertext in `dm`, on the card. + pub fn decrypt(&self, dm: DecryptMe) + -> Result, OpenpgpCardError> { + match dm { + DecryptMe::RSA(message) => { + let mut data = vec![0x0]; + data.extend_from_slice(message); + + // Call the card to decrypt `data` + self.pso_decipher(data) + } + DecryptMe::ECDH(eph) => { + // External Public Key + let epk = Tlv(Tag(vec![0x86]), + TlvEntry::S(eph.to_vec())); + + // Public Key DO + let pkdo = Tlv(Tag(vec![0x7f, 0x49]), + TlvEntry::C(vec![epk])); + + // Cipher DO + let cdo = Tlv(Tag(vec![0xa6]), + TlvEntry::C(vec![pkdo])); + + self.pso_decipher(cdo.serialize()) + } + } + } + + /// Run decryption operation on the smartcard + /// (7.2.11 PSO: DECIPHER) + pub(crate) fn pso_decipher(&self, data: Vec) + -> Result, OpenpgpCardError> { + // The OpenPGP card is already connected and PW1 82 has been verified + let dec_cmd = commands::decryption(data); + let resp = apdu::send_command(&self.card, dec_cmd, Le::Short, Some(self))?; + resp.check_ok()?; + + Ok(resp.data().map(|d| d.to_vec())?) + } + + + /// Sign the message in `hash`, on the card. + pub fn signature_for_hash(&self, hash: Hash) + -> Result, OpenpgpCardError> { + match hash { + Hash::SHA256(_) | Hash::SHA384(_) | Hash::SHA512(_) => { + let tlv = Tlv(Tag(vec![0x30]), + TlvEntry::C( + vec![Tlv(Tag(vec![0x30]), + TlvEntry::C( + vec![Tlv(Tag(vec![0x06]), + // unwrapping is + // ok, for SHA* + TlvEntry::S(hash.oid().unwrap().to_vec())), + Tlv(Tag(vec![0x05]), TlvEntry::S(vec![])) + ])), + Tlv(Tag(vec!(0x04)), TlvEntry::S(hash.digest().to_vec())) + ] + )); + + Ok(self.compute_digital_signature(tlv.serialize())?) + } + Hash::EdDSA(d) => { + Ok(self.compute_digital_signature(d.to_vec())?) + } + } + } + + /// Run signing operation on the smartcard + /// (7.2.10 PSO: COMPUTE DIGITAL SIGNATURE) + pub(crate) fn compute_digital_signature(&self, data: Vec) + -> Result, OpenpgpCardError> { + let dec_cmd = commands::signature(data); + + let resp = apdu::send_command(&self.card, dec_cmd, Le::Short, Some(self))?; + + Ok(resp.data().map(|d| d.to_vec())?) + } +} + + +/// An OpenPGP card after successful verification of PW3 +pub struct OpenPGPCardAdmin { + oc: OpenPGPCard, +} + +/// Allow access to fn of OpenPGPCard, through OpenPGPCardAdmin. +impl Deref for OpenPGPCardAdmin { + type Target = OpenPGPCard; + + fn deref(&self) -> &Self::Target { + &self.oc + } +} + +impl OpenPGPCardAdmin { + pub(crate) fn card(&self) -> &Card { + &self.card + } + + pub fn set_name(&self, name: &str) -> Result { + if name.len() >= 40 { + return Err(anyhow!("name too long").into()); + } + + // All chars must be in ASCII7 + if name.chars().any(|c| !c.is_ascii()) { + return Err(anyhow!("Invalid char in name").into()); + }; + + let put_name = commands::put_name(name.as_bytes().to_vec()); + apdu::send_command(self.card(), put_name, Le::Short, Some(self)) + } + + pub fn set_lang(&self, lang: &str) -> Result { + if lang.len() > 8 { + return Err(anyhow!("lang too long").into()); + } + + let put_lang = commands::put_lang(lang.as_bytes().to_vec()); + apdu::send_command(self.card(), put_lang, Le::Short, Some(self)) + } + + pub fn set_sex(&self, sex: Sex) -> Result { + let put_sex = commands::put_sex(sex.as_u8()); + apdu::send_command(self.card(), put_sex, Le::Short, Some(self)) + } + + pub fn set_url(&self, url: &str) -> Result { + if url.chars().any(|c| !c.is_ascii()) { + return Err(anyhow!("Invalid char in url").into()); + } + + // Check for max len + let ec = self.get_extended_capabilities()?; + + if url.len() < ec.max_len_special_do as usize { + let put_url = commands::put_url(url.as_bytes().to_vec()); + apdu::send_command(self.card(), put_url, Le::Short, Some(self)) + } else { + Err(anyhow!("URL too long").into()) + } + } + + pub fn upload_key( + &self, + key: Box, + key_type: KeyType, + ) -> Result<(), OpenpgpCardError> { + key_upload::upload_key(self, key, key_type) + } +} + + +#[cfg(test)] +mod test { + use super::tlv::{Tlv, TlvEntry}; + use super::tlv::tag::Tag; + + #[test] + fn test_tlv() { + let cpkt = + Tlv(Tag(vec![0x7F, 0x48]), + TlvEntry::S(vec![0x91, 0x03, + 0x92, 0x82, 0x01, 0x00, + 0x93, 0x82, 0x01, 0x00])); + + assert_eq!(cpkt.serialize(), + vec![0x7F, 0x48, + 0x0A, + 0x91, 0x03, + 0x92, 0x82, 0x01, 0x00, + 0x93, 0x82, 0x01, 0x00, + ]); + } +} diff --git a/openpgp-card/src/parse/algo_attrs.rs b/openpgp-card/src/parse/algo_attrs.rs new file mode 100644 index 0000000..b0b6325 --- /dev/null +++ b/openpgp-card/src/parse/algo_attrs.rs @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::convert::TryFrom; + +use anyhow::Result; +use nom::{branch, bytes::complete as bytes, number::complete as number}; +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::combinator::map; + +use crate::parse; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Algo { + Rsa(RsaAttrs), + Ecdsa(EcdsaAttrs), + Eddsa(EddsaAttrs), + Ecdh(EcdhAttrs), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RsaAttrs { + pub len_n: u16, + pub len_e: u16, + pub import_format: u8, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct EcdsaAttrs { + pub curve: Curve, + pub oid: Vec, + pub import_format: Option, +} + +impl EcdsaAttrs { + pub fn new(curve: Curve, import_format: Option) -> Self { + Self { curve, oid: curve.oid().to_vec(), import_format } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct EddsaAttrs { + pub curve: Curve, + pub oid: Vec, + pub import_format: Option, +} + +impl EddsaAttrs { + pub fn new(curve: Curve, import_format: Option) -> Self { + Self { curve, oid: curve.oid().to_vec(), import_format } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct EcdhAttrs { + pub curve: Curve, + pub oid: Vec, + pub import_format: Option, +} + +impl EcdhAttrs { + pub fn new(curve: Curve, import_format: Option) -> Self { + Self { curve, oid: curve.oid().to_vec(), import_format } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Curve { + NistP256r1, + NistP384r1, + NistP521r1, + BrainpoolP256r1, + BrainpoolP384r1, + BrainpoolP512r1, + Secp256k1, + Ed25519, + Cv25519, +} + +impl Curve { + pub fn oid(&self) -> &[u8] { + use Curve::*; + match self { + NistP256r1 => &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07], + NistP384r1 => &[0x2B, 0x81, 0x04, 0x00, 0x22], + NistP521r1 => &[0x2B, 0x81, 0x04, 0x00, 0x23], + BrainpoolP256r1 => + &[0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07], + BrainpoolP384r1 => + &[0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0b], + BrainpoolP512r1 => + &[0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0d], + Secp256k1 => &[0x2B, 0x81, 0x04, 0x00, 0x0A], + Ed25519 => + &[0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01], + Cv25519 => + &[0x2b, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01] + } + } +} + + +fn parse_oid_cv25519(input: &[u8]) -> nom::IResult<&[u8], Curve> { + map(tag(Curve::Cv25519.oid()), |_| Curve::Cv25519)(input) +} + +fn parse_oid_ed25519(input: &[u8]) -> nom::IResult<&[u8], Curve> { + map(tag(Curve::Ed25519.oid()), |_| Curve::Ed25519)(input) +} + +fn parse_oid_secp256k1(input: &[u8]) -> nom::IResult<&[u8], Curve> { + map(tag(Curve::Secp256k1.oid()), |_| Curve::Secp256k1)(input) +} + +fn parse_oid_nist256(input: &[u8]) -> nom::IResult<&[u8], Curve> { + map(tag(Curve::NistP256r1.oid()), |_| Curve::NistP256r1)(input) +} + +fn parse_oid_nist384(input: &[u8]) -> nom::IResult<&[u8], Curve> { + map(tag(Curve::NistP384r1.oid()), |_| Curve::NistP384r1)(input) +} + +fn parse_oid_nist521(input: &[u8]) -> nom::IResult<&[u8], Curve> { + map(tag(Curve::NistP521r1.oid()), |_| Curve::NistP521r1)(input) +} + +fn parse_oid_brainpool_p256r1(input: &[u8]) -> nom::IResult<&[u8], Curve> { + map(tag(Curve::BrainpoolP256r1.oid()), |_| Curve::BrainpoolP256r1)(input) +} + +fn parse_oid_brainpool_p384r1(input: &[u8]) -> nom::IResult<&[u8], Curve> { + map(tag(Curve::BrainpoolP384r1.oid()), |_| Curve::BrainpoolP384r1)(input) +} + +fn parse_oid_brainpool_p512r1(input: &[u8]) -> nom::IResult<&[u8], Curve> { + map(tag(Curve::BrainpoolP512r1.oid()), |_| Curve::BrainpoolP512r1)(input) +} + +fn parse_oid(input: &[u8]) -> nom::IResult<&[u8], Curve> { + alt((parse_oid_nist256, parse_oid_nist384, parse_oid_nist521, + parse_oid_brainpool_p256r1, parse_oid_brainpool_p384r1, + parse_oid_brainpool_p512r1, + parse_oid_secp256k1, + parse_oid_ed25519, parse_oid_cv25519))(input) +} + +fn parse_rsa(input: &[u8]) -> nom::IResult<&[u8], Algo> { + let (input, _) = bytes::tag([0x01])(input)?; + + let (input, len_n) = number::be_u16(input)?; + let (input, len_e) = number::be_u16(input)?; + let (input, import_format) = number::u8(input)?; + + Ok((input, Algo::Rsa(RsaAttrs { len_n, len_e, import_format }))) +} + +fn parse_import_format(input: &[u8]) -> nom::IResult<&[u8], Option> { + let (input, b) = bytes::take(1usize)(input)?; + Ok((input, Some(b[0]))) +} + +fn default_import_format(input: &[u8]) -> nom::IResult<&[u8], Option> { + Ok((input, None)) +} + +fn parse_ecdh(input: &[u8]) -> nom::IResult<&[u8], Algo> { + let (input, _) = bytes::tag([0x12])(input)?; + let (input, curve) = parse_oid(input)?; + + let (input, import_format) = + alt((parse_import_format, default_import_format))(input)?; + + Ok((input, Algo::Ecdh(EcdhAttrs::new(curve, import_format)))) +} + +fn parse_ecdsa(input: &[u8]) -> nom::IResult<&[u8], Algo> { + let (input, _) = bytes::tag([0x13])(input)?; + let (input, curve) = parse_oid(input)?; + + let (input, import_format) = + alt((parse_import_format, default_import_format))(input)?; + + Ok((input, Algo::Ecdsa(EcdsaAttrs::new(curve, import_format)))) +} + +fn parse_eddsa(input: &[u8]) -> nom::IResult<&[u8], Algo> { + let (input, _) = bytes::tag([0x16])(input)?; + let (input, curve) = parse_oid(input)?; + + let (input, import_format) = + alt((parse_import_format, default_import_format))(input)?; + + Ok((input, Algo::Eddsa(EddsaAttrs::new(curve, import_format)))) +} + +pub(crate) fn parse(input: &[u8]) -> nom::IResult<&[u8], Algo> { + branch::alt( + (parse_rsa, parse_ecdsa, parse_eddsa, parse_ecdh) + )(input) +} + +impl TryFrom<&[u8]> for Algo { + type Error = anyhow::Error; + + fn try_from(data: &[u8]) -> Result { + parse::complete(parse(data)) + } +} diff --git a/openpgp-card/src/parse/algo_info.rs b/openpgp-card/src/parse/algo_info.rs new file mode 100644 index 0000000..88ab2ee --- /dev/null +++ b/openpgp-card/src/parse/algo_info.rs @@ -0,0 +1,307 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::convert::TryFrom; + +use anyhow::Result; +use nom::{branch, bytes::complete as bytes, combinator, multi, sequence}; +use nom::branch::alt; +use nom::combinator::map; + +use crate::KeyType; +use crate::parse::algo_attrs; +use crate::parse::algo_attrs::Algo; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct AlgoInfo( + Vec<(KeyType, Algo)> +); + +impl AlgoInfo { + pub fn get_by_keytype(&self, kt: KeyType) -> Vec<&Algo> { + self.0 + .iter() + .filter(|(k, _)| *k == kt) + .map(|(_, a)| a) + .collect() + } +} + +fn key_type(input: &[u8]) -> nom::IResult<&[u8], KeyType> { + alt(( + map(bytes::tag([0xc1]), |_| KeyType::Signing), + map(bytes::tag([0xc2]), |_| KeyType::Decryption), + map(bytes::tag([0xc3]), |_| KeyType::Authentication), + map(bytes::tag([0xda]), |_| KeyType::Attestation), + ))(input) +} + +fn parse_one(input: &[u8]) -> nom::IResult<&[u8], Algo> { + let (x, a) = combinator::map( + combinator::flat_map(crate::tlv::length::length, bytes::take), + |i| combinator::all_consuming(algo_attrs::parse)(i), + )(input)?; + + Ok((x, a?.1)) +} + +fn parse_list(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, Algo)>> { + multi::many0(sequence::pair(key_type, parse_one))(input) +} + +fn parse_tl_list(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, Algo)>> { + let (input, (_, _, list)) = + sequence::tuple(( + bytes::tag([0xfa]), + crate::tlv::length::length, + parse_list))(input)?; + + Ok((input, list)) +} + +pub(self) fn parse(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, Algo)>> { + // Handle two variations of input format: + // a) TLV format (e.g. Yubikey 5) + // b) Plain list (e.g. Gnuk, FOSS-Store Smartcard 3.4) + + // -- Gnuk: do_alg_info (uint16_t tag, int with_tag) + + branch::alt( + (combinator::all_consuming(parse_list), + combinator::all_consuming(parse_tl_list)) + )(input) +} + +impl TryFrom<&[u8]> for AlgoInfo { + type Error = anyhow::Error; + + fn try_from(input: &[u8]) -> Result { + Ok(AlgoInfo(crate::parse::complete(parse(input))?)) + } +} + + +// test + + +#[cfg(test)] +mod test { + use std::convert::TryFrom; + + use crate::KeyType::*; + use crate::parse::algo_attrs::*; + use crate::parse::algo_attrs::Algo::*; + use crate::parse::algo_attrs::Curve::*; + use crate::parse::algo_info::AlgoInfo; + + #[test] + fn test_gnuk() { + let data = [0xc1, 0x6, 0x1, 0x8, 0x0, 0x0, 0x20, 0x0, 0xc1, 0x6, + 0x1, 0x10, 0x0, 0x0, 0x20, 0x0, 0xc1, 0x9, 0x13, 0x2a, 0x86, + 0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0xc1, 0x6, 0x13, 0x2b, 0x81, + 0x4, 0x0, 0xa, 0xc1, 0xa, 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, + 0xda, 0x47, 0xf, 0x1, 0xc2, 0x6, 0x1, 0x8, 0x0, 0x0, 0x20, + 0x0, 0xc2, 0x6, 0x1, 0x10, 0x0, 0x0, 0x20, 0x0, 0xc2, 0x9, + 0x13, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0xc2, 0x6, + 0x13, 0x2b, 0x81, 0x4, 0x0, 0xa, 0xc2, 0xb, 0x12, 0x2b, 0x6, + 0x1, 0x4, 0x1, 0x97, 0x55, 0x1, 0x5, 0x1, 0xc3, 0x6, 0x1, 0x8, + 0x0, 0x0, 0x20, 0x0, 0xc3, 0x6, 0x1, 0x10, 0x0, 0x0, 0x20, + 0x0, 0xc3, 0x9, 0x13, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x3, 0x1, + 0x7, 0xc3, 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, 0xa, 0xc3, 0xa, + 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0xda, 0x47, 0xf, 0x1]; + + let ai = AlgoInfo::try_from(data.to_vec()).unwrap(); + + assert_eq!( + ai, AlgoInfo( + vec![ + (Signing, Rsa(RsaAttrs { len_n: 2048, len_e: 32, import_format: 0 })), + (Signing, Rsa(RsaAttrs { len_n: 4096, len_e: 32, import_format: 0 })), + (Signing, Ecdsa(EcdsaAttrs::new(NistP256r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(Secp256k1, None))), + (Signing, Eddsa(EddsaAttrs::new(Ed25519, None))), + (Decryption, Rsa(RsaAttrs { len_n: 2048, len_e: 32, import_format: 0 })), + (Decryption, Rsa(RsaAttrs { len_n: 4096, len_e: 32, import_format: 0 })), + (Decryption, Ecdsa(EcdsaAttrs::new(NistP256r1, None))), + (Decryption, Ecdsa(EcdsaAttrs::new(Secp256k1, None))), + (Decryption, Ecdh(EcdhAttrs::new(Cv25519, None))), + (Authentication, Rsa(RsaAttrs { len_n: 2048, len_e: 32, import_format: 0 })), + (Authentication, Rsa(RsaAttrs { len_n: 4096, len_e: 32, import_format: 0 })), + (Authentication, Ecdsa(EcdsaAttrs::new(NistP256r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(Secp256k1, None))), + (Authentication, Eddsa(EddsaAttrs::new(Ed25519, None))) + ] + ) + ); + } + + #[test] + fn test_opgp_card_34() { + let data = [0xc1, 0x6, 0x1, 0x8, 0x0, 0x0, 0x20, 0x0, 0xc1, 0x6, + 0x1, 0xc, 0x0, 0x0, 0x20, 0x0, 0xc1, 0x6, 0x1, 0x10, 0x0, + 0x0, 0x20, 0x0, 0xc1, 0x9, 0x13, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x3, 0x1, 0x7, 0xc1, 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, + 0x22, 0xc1, 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, 0x23, 0xc1, 0xa, + 0x13, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0x7, 0xc1, + 0xa, 0x13, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xb, + 0xc1, 0xa, 0x13, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, + 0xd, 0xc2, 0x6, 0x1, 0x8, 0x0, 0x0, 0x20, 0x0, 0xc2, 0x6, + 0x1, 0xc, 0x0, 0x0, 0x20, 0x0, 0xc2, 0x6, 0x1, 0x10, 0x0, + 0x0, 0x20, 0x0, 0xc2, 0x9, 0x12, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x3, 0x1, 0x7, 0xc2, 0x6, 0x12, 0x2b, 0x81, 0x4, 0x0, + 0x22, 0xc2, 0x6, 0x12, 0x2b, 0x81, 0x4, 0x0, 0x23, 0xc2, 0xa, + 0x12, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0x7, 0xc2, + 0xa, 0x12, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xb, + 0xc2, 0xa, 0x12, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, + 0xd, 0xc3, 0x6, 0x1, 0x8, 0x0, 0x0, 0x20, 0x0, 0xc3, 0x6, 0x1, + 0xc, 0x0, 0x0, 0x20, 0x0, 0xc3, 0x6, 0x1, 0x10, 0x0, 0x0, + 0x20, 0x0, 0xc3, 0x9, 0x13, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x3, + 0x1, 0x7, 0xc3, 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, 0x22, 0xc3, + 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, 0x23, 0xc3, 0xa, 0x13, 0x2b, + 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0x7, 0xc3, 0xa, 0x13, + 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xb, 0xc3, 0xa, + 0x13, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xd]; + + let ai = AlgoInfo::try_from(data.to_vec()).unwrap(); + + assert_eq!( + ai, AlgoInfo( + vec![ + (Signing, Rsa(RsaAttrs { len_n: 2048, len_e: 32, import_format: 0 })), + (Signing, Rsa(RsaAttrs { len_n: 3072, len_e: 32, import_format: 0 })), + (Signing, Rsa(RsaAttrs { len_n: 4096, len_e: 32, import_format: 0 })), + (Signing, Ecdsa(EcdsaAttrs::new(NistP256r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(NistP384r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(NistP521r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(BrainpoolP256r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(BrainpoolP384r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(BrainpoolP512r1, None))), + (Decryption, Rsa(RsaAttrs { len_n: 2048, len_e: 32, import_format: 0 })), + (Decryption, Rsa(RsaAttrs { len_n: 3072, len_e: 32, import_format: 0 })), + (Decryption, Rsa(RsaAttrs { len_n: 4096, len_e: 32, import_format: 0 })), + (Decryption, Ecdh(EcdhAttrs::new(NistP256r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(NistP384r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(NistP521r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(BrainpoolP256r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(BrainpoolP384r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(BrainpoolP512r1, None))), + (Authentication, Rsa(RsaAttrs { len_n: 2048, len_e: 32, import_format: 0 })), + (Authentication, Rsa(RsaAttrs { len_n: 3072, len_e: 32, import_format: 0 })), + (Authentication, Rsa(RsaAttrs { len_n: 4096, len_e: 32, import_format: 0 })), + (Authentication, Ecdsa(EcdsaAttrs::new(NistP256r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(NistP384r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(NistP521r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(BrainpoolP256r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(BrainpoolP384r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(BrainpoolP512r1, None))) + ] + ) + ); + } + + + #[test] + fn test_yk5() { + let data = [0xfa, 0x82, 0x1, 0xe2, 0xc1, 0x6, 0x1, 0x8, 0x0, 0x0, + 0x11, 0x0, 0xc1, 0x6, 0x1, 0xc, 0x0, 0x0, 0x11, 0x0, 0xc1, + 0x6, 0x1, 0x10, 0x0, 0x0, 0x11, 0x0, 0xc1, 0x9, 0x13, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0xc1, 0x6, 0x13, 0x2b, + 0x81, 0x4, 0x0, 0x22, 0xc1, 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, + 0x23, 0xc1, 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, 0xa, 0xc1, 0xa, + 0x13, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0x7, 0xc1, + 0xa, 0x13, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xb, + 0xc1, 0xa, 0x13, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, + 0xd, 0xc1, 0xa, 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0xda, 0x47, + 0xf, 0x1, 0xc1, 0xb, 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x97, + 0x55, 0x1, 0x5, 0x1, 0xc2, 0x6, 0x1, 0x8, 0x0, 0x0, 0x11, + 0x0, 0xc2, 0x6, 0x1, 0xc, 0x0, 0x0, 0x11, 0x0, 0xc2, 0x6, + 0x1, 0x10, 0x0, 0x0, 0x11, 0x0, 0xc2, 0x9, 0x12, 0x2a, 0x86, + 0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0xc2, 0x6, 0x12, 0x2b, 0x81, + 0x4, 0x0, 0x22, 0xc2, 0x6, 0x12, 0x2b, 0x81, 0x4, 0x0, 0x23, + 0xc2, 0x6, 0x12, 0x2b, 0x81, 0x4, 0x0, 0xa, 0xc2, 0xa, 0x12, + 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0x7, 0xc2, 0xa, + 0x12, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xb, 0xc2, + 0xa, 0x12, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xd, + 0xc2, 0xa, 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0xda, 0x47, 0xf, + 0x1, 0xc2, 0xb, 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x97, 0x55, + 0x1, 0x5, 0x1, 0xc3, 0x6, 0x1, 0x8, 0x0, 0x0, 0x11, 0x0, + 0xc3, 0x6, 0x1, 0xc, 0x0, 0x0, 0x11, 0x0, 0xc3, 0x6, 0x1, + 0x10, 0x0, 0x0, 0x11, 0x0, 0xc3, 0x9, 0x13, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x3, 0x1, 0x7, 0xc3, 0x6, 0x13, 0x2b, 0x81, 0x4, + 0x0, 0x22, 0xc3, 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, 0x23, 0xc3, + 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, 0xa, 0xc3, 0xa, 0x13, 0x2b, + 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0x7, 0xc3, 0xa, 0x13, + 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xb, 0xc3, 0xa, + 0x13, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xd, 0xc3, + 0xa, 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0xda, 0x47, 0xf, 0x1, + 0xc3, 0xb, 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x97, 0x55, 0x1, + 0x5, 0x1, 0xda, 0x6, 0x1, 0x8, 0x0, 0x0, 0x11, 0x0, 0xda, + 0x6, 0x1, 0xc, 0x0, 0x0, 0x11, 0x0, 0xda, 0x6, 0x1, 0x10, + 0x0, 0x0, 0x11, 0x0, 0xda, 0x9, 0x13, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x3, 0x1, 0x7, 0xda, 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, + 0x22, 0xda, 0x6, 0x13, 0x2b, 0x81, 0x4, 0x0, 0x23, 0xda, 0x6, + 0x13, 0x2b, 0x81, 0x4, 0x0, 0xa, 0xda, 0xa, 0x13, 0x2b, 0x24, + 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0x7, 0xda, 0xa, 0x13, 0x2b, + 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xb, 0xda, 0xa, 0x13, + 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xd, 0xda, 0xa, + 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0xda, 0x47, 0xf, 0x1, 0xda, + 0xb, 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x97, 0x55, 0x1, 0x5, 0x1]; + + let ai = AlgoInfo::try_from(data.to_vec()).unwrap(); + + assert_eq!( + ai, AlgoInfo( + vec![ + (Signing, Rsa(RsaAttrs { len_n: 2048, len_e: 17, import_format: 0 })), + (Signing, Rsa(RsaAttrs { len_n: 3072, len_e: 17, import_format: 0 })), + (Signing, Rsa(RsaAttrs { len_n: 4096, len_e: 17, import_format: 0 })), + (Signing, Ecdsa(EcdsaAttrs::new(NistP256r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(NistP384r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(NistP521r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(Secp256k1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(BrainpoolP256r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(BrainpoolP384r1, None))), + (Signing, Ecdsa(EcdsaAttrs::new(BrainpoolP512r1, None))), + (Signing, Eddsa(EddsaAttrs::new(Ed25519, None))), + (Signing, Eddsa(EddsaAttrs::new(Cv25519, None))), + (Decryption, Rsa(RsaAttrs { len_n: 2048, len_e: 17, import_format: 0 })), + (Decryption, Rsa(RsaAttrs { len_n: 3072, len_e: 17, import_format: 0 })), + (Decryption, Rsa(RsaAttrs { len_n: 4096, len_e: 17, import_format: 0 })), + (Decryption, Ecdh(EcdhAttrs::new(NistP256r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(NistP384r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(NistP521r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(Secp256k1, None))), + (Decryption, Ecdh(EcdhAttrs::new(BrainpoolP256r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(BrainpoolP384r1, None))), + (Decryption, Ecdh(EcdhAttrs::new(BrainpoolP512r1, None))), + (Decryption, Eddsa(EddsaAttrs::new(Ed25519, None))), + (Decryption, Eddsa(EddsaAttrs::new(Cv25519, None))), + (Authentication, Rsa(RsaAttrs { len_n: 2048, len_e: 17, import_format: 0 })), + (Authentication, Rsa(RsaAttrs { len_n: 3072, len_e: 17, import_format: 0 })), + (Authentication, Rsa(RsaAttrs { len_n: 4096, len_e: 17, import_format: 0 })), + (Authentication, Ecdsa(EcdsaAttrs::new(NistP256r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(NistP384r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(NistP521r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(Secp256k1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(BrainpoolP256r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(BrainpoolP384r1, None))), + (Authentication, Ecdsa(EcdsaAttrs::new(BrainpoolP512r1, None))), + (Authentication, Eddsa(EddsaAttrs::new(Ed25519, None))), + (Authentication, Eddsa(EddsaAttrs::new(Cv25519, None))), + (Attestation, Rsa(RsaAttrs { len_n: 2048, len_e: 17, import_format: 0 })), + (Attestation, Rsa(RsaAttrs { len_n: 3072, len_e: 17, import_format: 0 })), + (Attestation, Rsa(RsaAttrs { len_n: 4096, len_e: 17, import_format: 0 })), + (Attestation, Ecdsa(EcdsaAttrs::new(NistP256r1, None))), + (Attestation, Ecdsa(EcdsaAttrs::new(NistP384r1, None))), + (Attestation, Ecdsa(EcdsaAttrs::new(NistP521r1, None))), + (Attestation, Ecdsa(EcdsaAttrs::new(Secp256k1, None))), + (Attestation, Ecdsa(EcdsaAttrs::new(BrainpoolP256r1, None))), + (Attestation, Ecdsa(EcdsaAttrs::new(BrainpoolP384r1, None))), + (Attestation, Ecdsa(EcdsaAttrs::new(BrainpoolP512r1, None))), + (Attestation, Eddsa(EddsaAttrs::new(Ed25519, None))), + (Attestation, Eddsa(EddsaAttrs::new(Cv25519, None))) + ] + ) + ); + } +} diff --git a/openpgp-card/src/parse/application_id.rs b/openpgp-card/src/parse/application_id.rs new file mode 100644 index 0000000..afa72aa --- /dev/null +++ b/openpgp-card/src/parse/application_id.rs @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use nom::{bytes::complete as bytes, number::complete as number}; +use anyhow::Result; +use std::convert::TryFrom; + +use crate::parse; + +#[derive(Debug, Eq, PartialEq)] +pub struct ApplicationId { + pub application: u8, + + // GnuPG says: + // if (app->appversion >= 0x0200) + // app->app_local->extcap.is_v2 = 1; + // + // if (app->appversion >= 0x0300) + // app->app_local->extcap.is_v3 = 1; + pub version: u16, + + pub manufacturer: u16, + + pub serial: u32, +} + +fn parse(input: &[u8]) + -> nom::IResult<&[u8], ApplicationId> { + let (input, _) = bytes::tag([0xd2, 0x76, 0x0, 0x1, 0x24])(input)?; + + let (input, application) = number::u8(input)?; + let (input, version) = number::be_u16(input)?; + let (input, manufacturer) = number::be_u16(input)?; + let (input, serial) = number::be_u32(input)?; + + let (input, _) = + nom::combinator::all_consuming(bytes::tag([0x0, 0x0]))(input)?; + + Ok((input, + ApplicationId { application, version, manufacturer, serial })) +} + +impl TryFrom<&[u8]> for ApplicationId { + type Error = anyhow::Error; + + fn try_from(data: &[u8]) -> Result { + parse::complete(parse(data)) + } +} + +impl ApplicationId { + pub fn serial(&self) -> String { + format!("{:08X}", self.serial) + } +} + diff --git a/openpgp-card/src/parse/cardholder.rs b/openpgp-card/src/parse/cardholder.rs new file mode 100644 index 0000000..784b52d --- /dev/null +++ b/openpgp-card/src/parse/cardholder.rs @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::convert::TryFrom; + +use anyhow::Result; + +use crate::Sex; +use crate::tlv::tag::Tag; +use crate::tlv::{TlvEntry, Tlv}; + +#[derive(Debug)] +pub struct CardHolder { + pub name: Option, + pub lang: Option>, + pub sex: Option, +} + +impl TryFrom<&[u8]> for CardHolder { + type Error = anyhow::Error; + + fn try_from(data: &[u8]) -> Result { + let entry = TlvEntry::from(&data, true)?; + let tlv = Tlv(Tag(vec![0x65]), entry); + + let name: Option = tlv.find(&Tag::from(&[0x5b][..])) + .map(|v| String::from_utf8_lossy(&v.serialize()).to_string()); + + let lang: Option> = tlv + .find(&Tag::from(&[0x5f, 0x2d][..])) + .map(|v| v.serialize().chunks(2) + .map(|c| [c[0] as char, c[1] as char]).collect() + ); + + let sex = tlv + .find(&Tag::from(&[0x5f, 0x35][..])) + .map(|v| v.serialize()) + .filter(|v| v.len() == 1) + .map(|v| Sex::from(v[0])); + + Ok(CardHolder { name, lang, sex }) + } +} diff --git a/openpgp-card/src/parse/extended_cap.rs b/openpgp-card/src/parse/extended_cap.rs new file mode 100644 index 0000000..c30db10 --- /dev/null +++ b/openpgp-card/src/parse/extended_cap.rs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use nom::{number::complete as number, combinator, sequence}; +use anyhow::Result; +use std::collections::HashSet; +use crate::parse; +use crate::errors::OpenpgpCardError; +use std::convert::TryFrom; + +#[derive(Debug, Eq, PartialEq)] +pub struct ExtendedCap { + pub features: HashSet, + sm: u8, + max_len_challenge: u16, + max_len_cardholder_cert: u16, + pub max_len_special_do: u16, + pin_2_format: bool, + mse_command: bool, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum Features { + SecureMessaging, + GetChallenge, + KeyImport, + PwStatusChange, + PrivateUseDOs, + AlgoAttrsChangeable, + Aes, + KdfDo, +} + +fn features(input: &[u8]) -> nom::IResult<&[u8], HashSet> { + combinator::map(number::u8, |b| { + let mut f = HashSet::new(); + + if b & 0x80 != 0 { f.insert(Features::SecureMessaging); } + if b & 0x40 != 0 { f.insert(Features::GetChallenge); } + if b & 0x20 != 0 { f.insert(Features::KeyImport); } + if b & 0x10 != 0 { f.insert(Features::PwStatusChange); } + if b & 0x08 != 0 { f.insert(Features::PrivateUseDOs); } + if b & 0x04 != 0 { f.insert(Features::AlgoAttrsChangeable); } + if b & 0x02 != 0 { f.insert(Features::Aes); } + if b & 0x01 != 0 { f.insert(Features::KdfDo); } + + f + })(input) +} + +fn parse(input: &[u8]) + -> nom::IResult<&[u8], (HashSet, u8, u16, u16, u16, u8, u8)> { + nom::combinator::all_consuming(sequence::tuple(( + features, + number::u8, + number::be_u16, + number::be_u16, + number::be_u16, + number::u8, + number::u8) + ))(input) +} + + +impl TryFrom<&[u8]> for ExtendedCap { + type Error = OpenpgpCardError; + + fn try_from(input: &[u8]) -> Result { + let ec = parse::complete(parse(input))?; + + Ok(Self { + features: ec.0, + sm: ec.1, + max_len_challenge: ec.2, + max_len_cardholder_cert: ec.3, + max_len_special_do: ec.4, + pin_2_format: ec.5 == 1, // FIXME: error if != 0|1 + mse_command: ec.6 == 1, // FIXME: error if != 0|1 + }) + } +} + + +#[cfg(test)] +mod test { + use hex_literal::hex; + use crate::parse::extended_cap::{ExtendedCap, Features}; + use std::collections::HashSet; + use std::iter::FromIterator; + + #[test] + fn test_ec() { + let data = hex!("7d 00 0b fe 08 00 00 ff 00 00"); + let ec = ExtendedCap::from(&data).unwrap(); + + assert_eq!( + ec, ExtendedCap { + features: HashSet::from_iter( + vec![Features::GetChallenge, Features::KeyImport, + Features::PwStatusChange, Features::PrivateUseDOs, + Features::AlgoAttrsChangeable, Features::KdfDo]), + sm: 0x0, + max_len_challenge: 0xbfe, + max_len_cardholder_cert: 0x800, + max_len_special_do: 0xff, + pin_2_format: false, + mse_command: false, + } + ); + } +} diff --git a/openpgp-card/src/parse/extended_length_info.rs b/openpgp-card/src/parse/extended_length_info.rs new file mode 100644 index 0000000..b7fb629 --- /dev/null +++ b/openpgp-card/src/parse/extended_length_info.rs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use nom::{number::complete as number, sequence, bytes::complete::tag}; +use anyhow::Result; +use crate::parse; + +#[derive(Debug, Eq, PartialEq)] +pub struct ExtendedLengthInfo { + pub max_command_bytes: u16, + pub max_response_bytes: u16, +} + + +fn parse(input: &[u8]) -> nom::IResult<&[u8], (u16, u16)> { + let (input, (_, cmd, _, resp)) = nom::combinator::all_consuming + (sequence::tuple(( + tag([0x2, 0x2]), + number::be_u16, + tag([0x2, 0x2]), + number::be_u16) + ))(input)?; + + Ok((input, (cmd, resp))) +} + +impl ExtendedLengthInfo { + pub fn from(input: &[u8]) -> Result { + let eli = parse::complete(parse(input))?; + + Ok(Self { + max_command_bytes: eli.0, + max_response_bytes: eli.1, + }) + } +} diff --git a/openpgp-card/src/parse/fingerprint.rs b/openpgp-card/src/parse/fingerprint.rs new file mode 100644 index 0000000..96ffee8 --- /dev/null +++ b/openpgp-card/src/parse/fingerprint.rs @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use nom::{bytes::complete as bytes, combinator, sequence}; +use std::fmt; +use anyhow::anyhow; + +use crate::parse::KeySet; +use crate::errors::OpenpgpCardError; + +#[derive(Clone, Eq, PartialEq)] +pub struct Fingerprint([u8; 20]); + +impl From<[u8; 20]> for Fingerprint { + fn from(data: [u8; 20]) -> Self { + Self(data) + } +} + +impl Fingerprint { + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Display for Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:X}", self) + } +} + +impl fmt::UpperHex for Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for b in &self.0 { + write!(f, "{:02X}", b)?; + } + Ok(()) + } +} + +impl fmt::Debug for Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("Fingerprint") + .field(&self.to_string()) + .finish() + } +} + +fn fingerprint(input: &[u8]) -> nom::IResult<&[u8], Option> { + combinator::map(bytes::take(20u8), |i: &[u8]| { + if i.iter().any(|&c| c > 0) { + use std::convert::TryInto; + // We requested 20 bytes, so we can unwrap here + let i: [u8; 20] = i.try_into().unwrap(); + Some(i.into()) + } else { + None + } + })(input) +} + +fn fingerprints(input: &[u8]) -> nom::IResult<&[u8], KeySet> { + combinator::into(sequence::tuple((fingerprint, fingerprint, fingerprint)))(input) +} + +pub fn from(input: &[u8]) -> Result, OpenpgpCardError> { + log::trace!("Fingerprint from input: {:x?}, len {}", input, input.len()); + + // The input may be longer than 3 fingerprint, don't fail if it hasn't + // been completely consumed. + self::fingerprints(input) + .map(|res| res.1) + .map_err(|err| anyhow!("Parsing failed: {:?}", err)) + .map_err(OpenpgpCardError::InternalError) +} \ No newline at end of file diff --git a/openpgp-card/src/parse/historical.rs b/openpgp-card/src/parse/historical.rs new file mode 100644 index 0000000..9d38e75 --- /dev/null +++ b/openpgp-card/src/parse/historical.rs @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use anyhow::{anyhow, Result}; +use crate::errors::OpenpgpCardError; + +#[derive(Debug)] +pub struct CardCapabilities { + command_chaining: bool, + extended_lc_le: bool, + extended_length_information: bool, +} + +impl CardCapabilities { + pub fn get_command_chaining(&self) -> bool { + self.command_chaining + } + + pub fn get_extended_lc_le(&self) -> bool { + self.extended_lc_le + } + + pub fn get_extended_length_information(&self) -> bool { + self.extended_length_information + } + + + pub fn from(data: [u8; 3]) -> Self { + let byte3 = data[2]; + + let command_chaining = byte3 & 0x80 != 0; + let extended_lc_le = byte3 & 0x40 != 0; + let extended_length_information = byte3 & 0x20 != 0; + + Self { command_chaining, extended_lc_le, extended_length_information } + } +} + +#[derive(Debug)] +pub struct CardSeviceData { + select_by_full_df_name: bool, + select_by_partial_df_name: bool, + dos_available_in_ef_dir: bool, + dos_available_in_ef_atr_info: bool, + access_services: [bool; 3], + mf: bool, +} + +impl CardSeviceData { + pub fn from(data: u8) -> Self { + let select_by_full_df_name = data & 0x80 != 0; + let select_by_partial_df_name = data & 0x40 != 0; + let dos_available_in_ef_dir = data & 0x20 != 0; + let dos_available_in_ef_atr_info = data & 0x10 != 0; + let access_services = + [data & 0x8 != 0, data & 0x4 != 0, data & 0x2 != 0]; + let mf = data & 0x1 != 0; + + Self { + select_by_full_df_name, + select_by_partial_df_name, + dos_available_in_ef_dir, + dos_available_in_ef_atr_info, + access_services, + mf, + } + } +} + +#[derive(Debug)] +pub struct Historical { + // category indicator byte + cib: u8, + + // Card service data (31) + csd: Option, + + // Card Capabilities (73) + cc: Option, + + // status indicator byte (o-card 3.4.1, pg 44) + sib: u8, +} + +impl Historical { + pub fn get_card_capabilities(&self) -> Option<&CardCapabilities> { + self.cc.as_ref() + } + + pub fn from(data: &[u8]) -> Result { + if data[0] == 0 { + // The OpenPGP application assumes a category indicator byte + // set to '00' (o-card 3.4.1, pg 44) + + let len = data.len(); + let cib = data[0]; + let mut csd = None; + let mut cc = None; + + // COMPACT - TLV data objects [ISO 12.1.1.2] + let mut ctlv = data[1..len - 3].to_vec(); + while !ctlv.is_empty() { + match ctlv[0] { + 0x31 => { + csd = Some(ctlv[1]); + ctlv.drain(0..2); + } + 0x73 => { + cc = Some([ctlv[1], ctlv[2], ctlv[3]]); + ctlv.drain(0..4); + } + 0 => { ctlv.drain(0..1); } + _ => unimplemented!("unexpected tlv in historical bytes") + } + } + + let sib = + match data[len - 3] { + 0 => { + // Card does not offer life cycle management, commands + // TERMINATE DF and ACTIVATE FILE are not supported + 0 + } + 3 => { + // Initialisation state + // OpenPGP application can be reset to default values with + // an ACTIVATE FILE command + 3 + } + 5 => { + // Operational state (activated) + // Card supports life cycle management, commands TERMINATE + // DF and ACTIVATE FILE are available + 5 + } + _ => { + return Err(anyhow!("unexpected status indicator in \ + historical bytes").into()); + } + }; + + // Ignore final two bytes: according to the spec, they should + // show [0x90, 0x0] - but Yubikey Neo shows [0x0, 0x0]. + // It's unclear if these status bytes are ever useful to process. + + let cc = cc.map(CardCapabilities::from); + let csd = csd.map(CardSeviceData::from); + + Ok(Self { cib, csd, cc, sib }) + } else { + Err(anyhow!("Unexpected category indicator in historical \ + bytes").into()) + } + } +} + +// FIXME: add tests diff --git a/openpgp-card/src/parse/mod.rs b/openpgp-card/src/parse/mod.rs new file mode 100644 index 0000000..f477505 --- /dev/null +++ b/openpgp-card/src/parse/mod.rs @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Parsing of replies to GET DO requests. +//! Turn OpenPGP card replies into our own data structures. + +pub mod algo_attrs; +pub mod algo_info; +pub mod cardholder; +pub mod historical; +pub mod extended_cap; +pub mod extended_length_info; +pub mod fingerprint; +pub mod application_id; + +use anyhow::{Error, anyhow}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct KeySet { + signature: Option, + decryption: Option, + authentication: Option, +} + +impl From<(Option, Option, Option)> for KeySet { + fn from(tuple: (Option, Option, Option)) -> Self { + Self { + signature: tuple.0, + decryption: tuple.1, + authentication: tuple.2, + } + } +} + +impl KeySet { + pub fn signature(&self) -> Option<&T> { + self.signature.as_ref() + } + + pub fn decryption(&self) -> Option<&T> { + self.decryption.as_ref() + } + + pub fn authentication(&self) -> Option<&T> { + self.authentication.as_ref() + } +} + +pub fn complete(result: nom::IResult<&[u8], O>) -> Result { + let (rem, output) = + result.map_err(|err| anyhow!("Parsing failed: {:?}", err))?; + if rem.is_empty() { + Ok(output) + } else { + Err(anyhow!("Parsing incomplete -- trailing data: {:x?}", rem)) + } +} diff --git a/openpgp-card/src/tlv/length.rs b/openpgp-card/src/tlv/length.rs new file mode 100644 index 0000000..28b3fa5 --- /dev/null +++ b/openpgp-card/src/tlv/length.rs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use nom::{branch, bytes::complete as bytes, combinator, number::complete as number, sequence}; + +fn length1(input: &[u8]) -> nom::IResult<&[u8], u8> { + combinator::verify(number::u8, |&c| c < 0x80)(input) +} + +fn length2(input: &[u8]) -> nom::IResult<&[u8], u8> { + sequence::preceded(bytes::tag(&[0x81]), number::u8)(input) +} + +fn length3(input: &[u8]) -> nom::IResult<&[u8], u16> { + sequence::preceded(bytes::tag(&[0x82]), number::be_u16)(input) +} + +pub(crate) fn length(input: &[u8]) -> nom::IResult<&[u8], u16> { + branch::alt(( + combinator::into(length1), + combinator::into(length2), + length3, + ))(input) +} diff --git a/openpgp-card/src/tlv/mod.rs b/openpgp-card/src/tlv/mod.rs new file mode 100644 index 0000000..c508d40 --- /dev/null +++ b/openpgp-card/src/tlv/mod.rs @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use anyhow::Result; +use nom::{bytes::complete as bytes, combinator}; + +// mod value; +pub mod length; +pub mod tag; + +use tag::Tag; + +#[derive(Debug, Eq, PartialEq)] +pub struct Tlv(pub Tag, pub TlvEntry); + +impl Tlv { + pub fn find(&self, tag: &Tag) -> Option<&TlvEntry> { + if &self.0 == tag { + Some(&self.1) + } else { + if let TlvEntry::C(inner) = &self.1 { + for tlv in inner { + let found = tlv.find(tag); + if found.is_some() { + return found; + } + } + } + None + } + } + + pub fn serialize(&self) -> Vec { + let value = self.1.serialize(); + let length = crate::tlv::tlv_encode_length(value.len() as u16); + + let mut ser = Vec::new(); + ser.extend(self.0.0.iter()); + ser.extend(length.iter()); + ser.extend(value.iter()); + ser + } + + fn parse(input: &[u8]) -> nom::IResult<&[u8], Tlv> { + // read tag + let (input, tag) = tag::tag(input)?; + + let (input, value) = + combinator::flat_map(length::length, bytes::take)(input)?; + + let (_, entry) = TlvEntry::parse(value, tag.is_constructed())?; + + Ok((input, Self(tag, entry))) + } + + pub fn try_from(input: &[u8]) -> Result { + crate::parse::complete(Tlv::parse(input)) + } +} + +pub fn tlv_encode_length(len: u16) -> Vec { + if len > 255 { + vec![0x82, (len >> 8) as u8, (len & 255) as u8] + } else if len > 127 { + vec![0x81, len as u8] + } else { + vec![len as u8] + } +} + +#[derive(Debug, Eq, PartialEq)] +pub enum TlvEntry { + C(Vec), + S(Vec), +} + +impl TlvEntry { + pub fn parse(data: &[u8], constructed: bool) -> nom::IResult<&[u8], Self> { + match constructed { + false => Ok((&[], TlvEntry::S(data.to_vec()))), + true => { + let mut c = vec![]; + let mut input = data; + + while !input.is_empty() { + let (rest, tlv) = Tlv::parse(&input)?; + input = rest; + c.push(tlv); + } + + Ok((&[], TlvEntry::C(c))) + } + } + } + + pub fn from(data: &[u8], constructed: bool) -> Result { + crate::parse::complete(Self::parse(data, constructed)) + } + + pub fn serialize(&self) -> Vec { + match self { + TlvEntry::S(data) => data.clone(), + TlvEntry::C(data) => { + let mut s = vec![]; + for t in data { + s.extend(&t.serialize()); + } + s + } + } + } +} + +#[cfg(test)] +mod test { + use super::{Tag, Tlv}; + use hex_literal::hex; + use anyhow::Result; + use crate::tlv::TlvEntry; + + #[test] + fn test_tlv() -> Result<()> { + // From OpenPGP card spec § 7.2.6 + let data = hex!("5B0B546573743C3C54657374695F2D0264655F350131") + .to_vec(); + + let (input, tlv) = Tlv::parse(&data).unwrap(); + + assert_eq!(tlv, + Tlv(Tag::from([0x5b]), + TlvEntry::S(hex!("546573743C3C5465737469") + .to_vec()))); + + let (input, tlv) = Tlv::parse(input).unwrap(); + + assert_eq!(tlv, + Tlv(Tag::from([0x5f, 0x2d]), + TlvEntry::S(hex!("6465") + .to_vec()))); + + let (input, tlv) = Tlv::parse(input).unwrap(); + + assert_eq!(tlv, + Tlv(Tag::from([0x5f, 0x35]), + TlvEntry::S(hex!("31") + .to_vec()))); + + + assert!(input.is_empty()); + + Ok(()) + } + + + #[test] + fn test_tlv_yubi5() -> Result<()> { + // 'Yubikey 5 NFC' output for GET DATA on "Application Related Data" + let data = hex!("6e8201374f10d27600012401030400061601918000005f520800730000e00590007f740381012073820110c00a7d000bfe080000ff0000c106010800001100c206010800001100c306010800001100da06010800001100c407ff7f7f7f030003c5500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd1000000000000000000000000000000000de0801000200030081027f660802020bfe02020bfed6020020d7020020d8020020d9020020"); + let tlv = Tlv::try_from(&data[..])?; + + // outermost layer contains all bytes as value + let entry = tlv.find(&Tag::from([0x6e])).unwrap(); + assert_eq!(entry.serialize(), + hex!("4f10d27600012401030400061601918000005f520800730000e00590007f740381012073820110c00a7d000bfe080000ff0000c106010800001100c206010800001100c306010800001100da06010800001100c407ff7f7f7f030003c5500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd1000000000000000000000000000000000de0801000200030081027f660802020bfe02020bfed6020020d7020020d8020020d9020020")); + + // get and verify data for ecap tag + let entry = tlv.find(&Tag::from([0xc0])).unwrap(); + assert_eq!(entry.serialize(), hex!("7d000bfe080000ff0000")); + + let entry = tlv.find(&Tag::from([0x4f])).unwrap(); + assert_eq!(entry.serialize(), hex!("d2760001240103040006160191800000")); + + let entry = tlv.find(&Tag::from([0x5f, 0x52])).unwrap(); + assert_eq!(entry.serialize(), hex!("00730000e0059000")); + + let entry = tlv.find(&Tag::from([0x7f, 0x74])).unwrap(); + assert_eq!(entry.serialize(), hex!("810120")); + + let entry = tlv.find(&Tag::from([0x73])).unwrap(); + assert_eq!(entry.serialize(), hex!("c00a7d000bfe080000ff0000c106010800001100c206010800001100c306010800001100da06010800001100c407ff7f7f7f030003c5500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd1000000000000000000000000000000000de0801000200030081027f660802020bfe02020bfed6020020d7020020d8020020d9020020")); + + let entry = tlv.find(&Tag::from([0xc0])).unwrap(); + assert_eq!(entry.serialize(), hex!("7d000bfe080000ff0000")); + + let entry = tlv.find(&Tag::from([0xc1])).unwrap(); + assert_eq!(entry.serialize(), hex!("010800001100")); + + let entry = tlv.find(&Tag::from([0xc2])).unwrap(); + assert_eq!(entry.serialize(), hex!("010800001100")); + + let entry = tlv.find(&Tag::from([0xc3])).unwrap(); + assert_eq!(entry.serialize(), hex!("010800001100")); + + let entry = tlv.find(&Tag::from([0xda])).unwrap(); + assert_eq!(entry.serialize(), hex!("010800001100")); + + let entry = tlv.find(&Tag::from([0xc4])).unwrap(); + assert_eq!(entry.serialize(), hex!("ff7f7f7f030003")); + + let entry = tlv.find(&Tag::from([0xc5])).unwrap(); + assert_eq!(entry.serialize(), hex!("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")); + + let entry = tlv.find(&Tag::from([0xc6])).unwrap(); + assert_eq!(entry.serialize(), hex!("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")); + + + let entry = tlv.find(&Tag::from([0xcd])).unwrap(); + assert_eq!(entry.serialize(), hex!("00000000000000000000000000000000")); + + let entry = tlv.find(&Tag::from([0xde])).unwrap(); + assert_eq!(entry.serialize(), hex!("0100020003008102")); + + let entry = tlv.find(&Tag::from([0x7f, 0x66])).unwrap(); + assert_eq!(entry.serialize(), hex!("02020bfe02020bfe")); + + let entry = tlv.find(&Tag::from([0xd6])).unwrap(); + assert_eq!(entry.serialize(), hex!("0020")); + + let entry = tlv.find(&Tag::from([0xd7])).unwrap(); + assert_eq!(entry.serialize(), hex!("0020")); + + let entry = tlv.find(&Tag::from([0xd8])).unwrap(); + assert_eq!(entry.serialize(), hex!("0020")); + + let entry = tlv.find(&Tag::from([0xd9])).unwrap(); + assert_eq!(entry.serialize(), hex!("0020")); + + Ok(()) + } + + #[test] + fn test_tlv_builder() { + // NOTE: The data used in this example is similar to key upload, + // but has been abridged and changed. It does not represent a + // complete valid OpenPGP card DO! + + let a = Tlv(Tag::from(&[0x7F, 0x48][..]), + TlvEntry::S(vec![0x92, 0x03])); + + let b = Tlv(Tag::from(&[0x5F, 0x48][..]), + TlvEntry::S(vec![0x1, 0x2, 0x3])); + + let tlv = Tlv(Tag::from(&[0x4d][..]), + TlvEntry::C(vec![a, b])); + + assert_eq!(tlv.serialize(), &[0x4d, 0xb, + 0x7f, 0x48, 0x2, 0x92, 0x3, + 0x5f, 0x48, 0x3, 0x1, 0x2, 0x3]); + } +} \ No newline at end of file diff --git a/openpgp-card/src/tlv/tag.rs b/openpgp-card/src/tlv/tag.rs new file mode 100644 index 0000000..9e36dfb --- /dev/null +++ b/openpgp-card/src/tlv/tag.rs @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use nom::{branch, bytes::complete as bytes, combinator, number::complete as number, sequence}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Tag(pub Vec); + +impl Tag { + pub fn new(t: Vec) -> Self { + Self(t) + } + + pub fn is_constructed(&self) -> bool { + if self.0.is_empty() { + false + } else { + self.0[0] & 0x20 != 0 + } + } +} + +impl From<&[u8]> for Tag { + fn from(t: &[u8]) -> Self { + Tag(t.to_vec()) + } +} + +impl From<[u8; 1]> for Tag { + fn from(t: [u8; 1]) -> Self { + Tag(t.to_vec()) + } +} + +impl From<[u8; 2]> for Tag { + fn from(t: [u8; 2]) -> Self { + Tag(t.to_vec()) + } +} + + +fn multi_byte_tag(input: &[u8]) -> nom::IResult<&[u8], &[u8]> { + combinator::recognize(sequence::pair(multi_byte_tag_first, multi_byte_tag_rest))(input) +} + +fn multi_byte_tag_first(input: &[u8]) -> nom::IResult<&[u8], u8> { + combinator::verify(number::u8, is_multi_byte_tag_first)(input) +} + +fn is_multi_byte_tag_first(c: &u8) -> bool { + c.trailing_ones() >= 5 +} + +fn multi_byte_tag_rest(input: &[u8]) -> nom::IResult<&[u8], &[u8]> { + fn is_first(c: &u8) -> bool { + c.trailing_zeros() < 7 + } + + fn is_last(c: &u8) -> bool { + c.leading_ones() == 0 + } + + fn single_byte_rest(input: &[u8]) -> nom::IResult<&[u8], &[u8]> { + combinator::verify(bytes::take(1u8), + |c: &[u8]| { + c.len() == 1 && + is_first(&c[0]) && + is_last(&c[0]) + })(input) + } + + fn multi_byte_rest(input: &[u8]) -> nom::IResult<&[u8], &[u8]> { + combinator::recognize(sequence::tuple(( + combinator::verify(number::u8, |c| is_first(c) && !is_last(c)), + bytes::take_while(|c| !is_last(&c)), + combinator::verify(number::u8, |c| is_last(c)), + )))(input) + } + + branch::alt((single_byte_rest, multi_byte_rest))(input) +} + +fn single_byte_tag(input: &[u8]) -> nom::IResult<&[u8], &[u8]> { + combinator::verify(bytes::take(1u8), + |c: &[u8]| { + c.len() == 1 && + !is_multi_byte_tag_first(&c[0]) + })(input) +} + +pub(super) fn tag(input: &[u8]) -> nom::IResult<&[u8], Tag> { + combinator::map(branch::alt((multi_byte_tag, single_byte_tag)), + Tag::from)(input) +} + + +#[cfg(test)] +mod test { + #[test] + fn test_tag() { + let (_, tag) = super::tag(&[0x0f]).unwrap(); + assert_eq!(tag.0, &[0x0f]); + let (_, tag) = super::tag(&[0x0f, 0x4f]).unwrap(); + assert_eq!(tag.0, &[0x0f]); + let (_, tag) = super::tag(&[0x4f]).unwrap(); + assert_eq!(tag.0, &[0x4f]); + let (_, tag) = super::tag(&[0x5f, 0x1f]).unwrap(); + assert_eq!(tag.0, &[0x5f, 0x1f]); + let (_, tag) = super::tag(&[0x5f, 0x2d]).unwrap(); + assert_eq!(tag.0, &[0x5f, 0x2d]); + let (_, tag) = super::tag(&[0x5f, 0x35]).unwrap(); + assert_eq!(tag.0, &[0x5f, 0x35]); + let (_, tag) = super::tag(&[0x5f, 0x35, 0x35]).unwrap(); + assert_eq!(tag.0, &[0x5f, 0x35]); + let (_, tag) = super::tag(&[0x5f, 0x35, 0x2d]).unwrap(); + assert_eq!(tag.0, &[0x5f, 0x35]); + assert!(super::tag(&[0x5f]).is_err()); + } +} diff --git a/openpgp-card/src/tlv/value.rs b/openpgp-card/src/tlv/value.rs new file mode 100644 index 0000000..14995f8 --- /dev/null +++ b/openpgp-card/src/tlv/value.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#[derive(Debug, Eq, PartialEq)] +pub enum Value { + // "Primitive (Simple) DO" + S(Vec), + + // "Constructed DO" + C(Tlv), +} + +impl Value { + pub fn data(&self) -> &[u8] { + match self { + Value::S(v) => &v, + Value::C(c) => &c.0, + } + } +}