Initial commit

This commit is contained in:
Heiko Schaefer 2021-06-30 22:29:23 +02:00
commit 88f0598eab
46 changed files with 4929 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
# SPDX-License-Identifier: CC0-1.0
target/
.idea
Cargo.lock
_test/
notes/

8
.reuse/dep5 Normal file
View file

@ -0,0 +1,8 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: OpenPGP card
Upstream-Contact: Heiko Schaefer <heiko@schaefer.name>
Source: https://gitlab.com/hkos/openpgp-card
Files: example/*
Copyright: 2021 Heiko Schaefer <heiko@schaefer.name>
License: CC0-1.0

9
Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
# SPDX-License-Identifier: MIT OR Apache-2.0
[workspace]
members = [
"openpgp-card",
"openpgp-card-sequoia",
]

191
LICENSES/Apache-2.0.txt Normal file
View file

@ -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.

119
LICENSES/CC0-1.0.txt Normal file
View file

@ -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.

19
LICENSES/MIT.txt Normal file
View file

@ -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.

42
README.md Normal file
View file

@ -0,0 +1,42 @@
<!--
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
SPDX-License-Identifier: MIT OR Apache-2.0
-->
**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).

View file

@ -0,0 +1,9 @@
-----BEGIN PGP MESSAGE-----
hF4Dc8fxqe7aw2ASAQdAqTjTObPuwUiRtLQEgEX9hrmCujWNZBLfh9kwCwDR3FEw
ybpUXusFaZUtR7cWbB/csRuMQF0eSgFgYZYuY53TVpdN1hqv5ZRDlUY+zX8vCgke
0kgBxlDCnhLKtI402g8mz36rIxBTQSyGVAa8NekCddl95OUEwLwU84XfxsYk+Ghp
D9XPoW1l7W3bAli91QGriuv9ui+qBbxHoE8=
=nMA1
-----END PGP MESSAGE-----

View file

@ -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-----

View file

@ -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-----

View file

@ -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-----

16
example/nist256.sec Normal file
View file

@ -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-----

23
example/nist521.sec Normal file
View file

@ -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-----

15
example/test25519.sec Normal file
View file

@ -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-----

57
example/test2k.sec Normal file
View file

@ -0,0 +1,57 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=c/bE
-----END PGP PRIVATE KEY BLOCK-----

81
example/test3k.sec Normal file
View file

@ -0,0 +1,81 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=1jDp
-----END PGP PRIVATE KEY BLOCK-----

105
example/test4k.sec Normal file
View file

@ -0,0 +1,105 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=AhOY
-----END PGP PRIVATE KEY BLOCK-----

View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
# 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 <heiko@schaefer.name>"]
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"

View file

@ -0,0 +1,33 @@
<!--
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
SPDX-License-Identifier: MIT OR Apache-2.0
-->
**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
```

View file

@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<CardDecryptor<'a>, 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<usize>,
) -> openpgp::Result<crypto::SessionKey> {
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<D>(
&mut self,
pkesks: &[packet::PKESK],
_skesks: &[packet::SKESK],
sym_algo: Option<SymmetricAlgorithm>,
mut dec_fn: D,
) -> openpgp::Result<Option<openpgp::Fingerprint>>
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<Vec<openpgp::Cert>> {
Ok(vec![])
}
fn check(&mut self, _structure: MessageStructure) -> openpgp::Result<()> {
Ok(())
}
}

View file

@ -0,0 +1,284 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<key::PublicParts, key::UnspecifiedRole>;
/// 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<SecretParts, UnspecifiedRole>,
public: mpi::PublicKey,
password: Option<String>,
}
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<SecretParts>,
password: Option<String>) -> 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<PrivateKeyMaterial> {
// 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<Utc> = self.key.creation_time().into();
key_creation.timestamp() as u64
}
fn get_fp(&self) -> Vec<u8> {
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<u8>,
scalar: ProtectedMPI,
ecc_type: EccType,
}
impl SqEccKey {
fn new(oid: Vec<u8>, 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<String>,
) -> Result<(), Box<dyn Error>> {
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<SecretParts>,
key_type: KeyType,
password: Option<String>,
) -> 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<u8>,
) -> Result<Vec<u8>> {
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<String> {
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")
}

View file

@ -0,0 +1,203 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<dyn Error>> {
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<<Foo")?;
println!("set name {:x?}", res);
let res = oc_admin.set_sex(openpgp_card::Sex::NotApplicable)?;
println!("set sex {:x?}", res);
let res = oc_admin.set_lang("en")?;
println!("set lang {:x?}", res);
let res = oc_admin.set_url("https://keys.openpgp.org")?;
println!("set url {:x?}", res);
let cert = Cert::from_file(TEST_KEY_PATH)?;
openpgp_card_sequoia::upload_from_cert_yolo(
&oc_admin,
&cert,
KeyType::Decryption,
None,
)?;
openpgp_card_sequoia::upload_from_cert_yolo(
&oc_admin,
&cert,
KeyType::Signing,
None,
)?;
// TODO: test keys currently have no auth-capable key
// openpgp_card_sequoia::upload_from_cert(
// &oc_admin,
// &cert,
// KeyType::Authentication,
// None,
// )?;
}
_ => 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(())
}

View file

@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<CardSigner<'a>, 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<mpi::Signature> {
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
)),
}
}
}

20
openpgp-card/Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
# 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 <heiko@schaefer.name>"]
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"

14
openpgp-card/README.md Normal file
View file

@ -0,0 +1,14 @@
<!--
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
SPDX-License-Identifier: MIT OR Apache-2.0
-->
**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.

View file

@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<u8>,
}
impl Command {
pub fn new(cla: u8, ins: u8, p1: u8, p2: u8, data: Vec<u8>) -> Self {
Command { cla, ins, p1, p2, data }
}
pub(crate) fn serialize(&self, ext: Le) -> Result<Vec<u8>> {
// 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)
}
}

View file

@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<u8>) -> Command {
Command::new(0x00, 0x20, 0x00, 0x81, pin)
}
/// VERIFY pin for PW1 (82)
pub fn verify_pw1_82(pin: Vec<u8>) -> Command {
Command::new(0x00, 0x20, 0x00, 0x82, pin)
}
/// VERIFY pin for PW3 (83)
pub fn verify_pw3(pin: Vec<u8>) -> 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<u8>) -> 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<u8>) -> Command {
put_data(&[0x5b], name)
}
/// PUT DO Language preferences
pub fn put_lang(lang: Vec<u8>) -> 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<u8>) -> 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<u8>) -> Command {
Command::new(0x00, 0x2C, 0x02, 0x81, pin)
}
/// Change PW3 (admin pin)
pub fn change_pw3(oldpin: Vec<u8>, newpin: Vec<u8>) -> 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<u8>) -> Command {
Command::new(0x00, 0x2A, 0x80, 0x86, data)
}
/// Creates new APDU for decryption operation
pub fn signature(data: Vec<u8>) -> Command {
Command::new(0x00, 0x2A, 0x9e, 0x9a, data)
}

View file

@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<Response, OpenpgpCardError> {
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<Vec<u8>, 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())
}
}

View file

@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<u8>,
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<u8> {
&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<Self, OcErrorStatus> {
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<Vec<u8>> for Response {
type Error = OcErrorStatus;
fn try_from(mut data: Vec<u8>) -> Result<Self, OcErrorStatus> {
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]);
}
}

55
openpgp-card/src/card.rs Normal file
View file

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<Vec<Card>, 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)
}
}

157
openpgp-card/src/errors.rs Normal file
View file

@ -0,0 +1,157 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<OcErrorStatus> for OpenpgpCardError {
fn from(oce: OcErrorStatus) -> Self {
OpenpgpCardError::OcStatus(oce)
}
}
impl From<anyhow::Error> 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),
}

View file

@ -0,0 +1,383 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<dyn CardUploadableKey>,
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<dyn EccKey>, key_type: KeyType)
-> Result<Command, OpenpgpCardError> {
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<Tlv, OpenpgpCardError> {
// "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<dyn RSAKey>,
algo_attrs: &RsaAttrs) -> Result<Command, OpenpgpCardError> {
// 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<Command> {
// 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<u8>,
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<u8> = 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(())
}

762
openpgp-card/src/lib.rs Normal file
View file

@ -0,0 +1,762 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<PrivateKeyMaterial>;
/// timestamp of (sub)key creation
fn get_ts(&self) -> u64;
/// fingerprint
fn get_fp(&self) -> Vec<u8>;
}
/// Algorithm-independent container for private key material to upload to
/// an OpenPGP card
pub enum PrivateKeyMaterial {
R(Box<dyn RSAKey>),
E(Box<dyn EccKey>),
}
/// 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<u8> 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<Vec<Self>> {
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<Self, OpenpgpCardError> {
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<Self, OpenpgpCardError> {
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<Self, OpenpgpCardError> {
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<Tlv> {
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<ApplicationId, OpenpgpCardError> {
// 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<Historical, OpenpgpCardError> {
// 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<Option<ExtendedLengthInfo>> {
// 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<bool> {
unimplemented!()
}
pub fn get_discretionary_data_objects() {
unimplemented!()
}
pub fn get_extended_capabilities(&self) -> Result<ExtendedCap, OpenpgpCardError> {
// 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<Algo> {
// 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<KeySet<fingerprint::Fingerprint>, 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<String> {
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<CardHolder> {
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<Tlv> {
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<Option<AlgoInfo>> {
// 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<OpenPGPCardUser, OpenPGPCard> {
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<OpenPGPCardUser, OpenPGPCard> {
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<OpenPGPCardAdmin, OpenPGPCard> {
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<Vec<u8>, 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<u8>)
-> Result<Vec<u8>, 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<Vec<u8>, 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<u8>)
-> Result<Vec<u8>, 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<Response, OpenpgpCardError> {
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<Response, OpenpgpCardError> {
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<Response, OpenpgpCardError> {
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<Response, OpenpgpCardError> {
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<dyn CardUploadableKey>,
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,
]);
}
}

View file

@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<u8>,
pub import_format: Option<u8>,
}
impl EcdsaAttrs {
pub fn new(curve: Curve, import_format: Option<u8>) -> Self {
Self { curve, oid: curve.oid().to_vec(), import_format }
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct EddsaAttrs {
pub curve: Curve,
pub oid: Vec<u8>,
pub import_format: Option<u8>,
}
impl EddsaAttrs {
pub fn new(curve: Curve, import_format: Option<u8>) -> Self {
Self { curve, oid: curve.oid().to_vec(), import_format }
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct EcdhAttrs {
pub curve: Curve,
pub oid: Vec<u8>,
pub import_format: Option<u8>,
}
impl EcdhAttrs {
pub fn new(curve: Curve, import_format: Option<u8>) -> 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<u8>> {
let (input, b) = bytes::take(1usize)(input)?;
Ok((input, Some(b[0])))
}
fn default_import_format(input: &[u8]) -> nom::IResult<&[u8], Option<u8>> {
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<Self> {
parse::complete(parse(data))
}
}

View file

@ -0,0 +1,307 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<Self> {
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)))
]
)
);
}
}

View file

@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<Self> {
parse::complete(parse(data))
}
}
impl ApplicationId {
pub fn serial(&self) -> String {
format!("{:08X}", self.serial)
}
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<String>,
pub lang: Option<Vec<[char; 2]>>,
pub sex: Option<Sex>,
}
impl TryFrom<&[u8]> for CardHolder {
type Error = anyhow::Error;
fn try_from(data: &[u8]) -> Result<Self> {
let entry = TlvEntry::from(&data, true)?;
let tlv = Tlv(Tag(vec![0x65]), entry);
let name: Option<String> = tlv.find(&Tag::from(&[0x5b][..]))
.map(|v| String::from_utf8_lossy(&v.serialize()).to_string());
let lang: Option<Vec<[char; 2]>> = 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 })
}
}

View file

@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<Features>,
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<Features>> {
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<Features>, 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<Self, Self::Error> {
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,
}
);
}
}

View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<Self> {
let eli = parse::complete(parse(input))?;
Ok(Self {
max_command_bytes: eli.0,
max_response_bytes: eli.1,
})
}
}

View file

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<Fingerprint>> {
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<Fingerprint>> {
combinator::into(sequence::tuple((fingerprint, fingerprint, fingerprint)))(input)
}
pub fn from(input: &[u8]) -> Result<KeySet<Fingerprint>, 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)
}

View file

@ -0,0 +1,157 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<CardSeviceData>,
// Card Capabilities (73)
cc: Option<CardCapabilities>,
// 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<Self, OpenpgpCardError> {
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

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<T> {
signature: Option<T>,
decryption: Option<T>,
authentication: Option<T>,
}
impl<T> From<(Option<T>, Option<T>, Option<T>)> for KeySet<T> {
fn from(tuple: (Option<T>, Option<T>, Option<T>)) -> Self {
Self {
signature: tuple.0,
decryption: tuple.1,
authentication: tuple.2,
}
}
}
impl<T> KeySet<T> {
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<O>(result: nom::IResult<&[u8], O>) -> Result<O, Error> {
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))
}
}

View file

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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)
}

250
openpgp-card/src/tlv/mod.rs Normal file
View file

@ -0,0 +1,250 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<u8> {
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<Self> {
crate::parse::complete(Tlv::parse(input))
}
}
pub fn tlv_encode_length(len: u16) -> Vec<u8> {
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<Tlv>),
S(Vec<u8>),
}
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<Self> {
crate::parse::complete(Self::parse(data, constructed))
}
pub fn serialize(&self) -> Vec<u8> {
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]);
}
}

119
openpgp-card/src/tlv/tag.rs Normal file
View file

@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// 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<u8>);
impl Tag {
pub fn new(t: Vec<u8>) -> 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());
}
}

View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
#[derive(Debug, Eq, PartialEq)]
pub enum Value {
// "Primitive (Simple) DO"
S(Vec<u8>),
// "Constructed DO"
C(Tlv),
}
impl Value {
pub fn data(&self) -> &[u8] {
match self {
Value::S(v) => &v,
Value::C(c) => &c.0,
}
}
}