Initial commit
This commit is contained in:
commit
88f0598eab
46 changed files with 4929 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal 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
8
.reuse/dep5
Normal 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
9
Cargo.toml
Normal 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
191
LICENSES/Apache-2.0.txt
Normal 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
119
LICENSES/CC0-1.0.txt
Normal 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
19
LICENSES/MIT.txt
Normal 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
42
README.md
Normal 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).
|
9
example/encrypted_to_25519.asc
Normal file
9
example/encrypted_to_25519.asc
Normal file
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hF4Dc8fxqe7aw2ASAQdAqTjTObPuwUiRtLQEgEX9hrmCujWNZBLfh9kwCwDR3FEw
|
||||
ybpUXusFaZUtR7cWbB/csRuMQF0eSgFgYZYuY53TVpdN1hqv5ZRDlUY+zX8vCgke
|
||||
0kgBxlDCnhLKtI402g8mz36rIxBTQSyGVAa8NekCddl95OUEwLwU84XfxsYk+Ghp
|
||||
D9XPoW1l7W3bAli91QGriuv9ui+qBbxHoE8=
|
||||
=nMA1
|
||||
-----END PGP MESSAGE-----
|
||||
|
11
example/encrypted_to_nist521.asc
Normal file
11
example/encrypted_to_nist521.asc
Normal 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-----
|
||||
|
12
example/encrypted_to_rsa2k.asc
Normal file
12
example/encrypted_to_rsa2k.asc
Normal 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-----
|
18
example/encrypted_to_rsa4k.asc
Normal file
18
example/encrypted_to_rsa4k.asc
Normal 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
16
example/nist256.sec
Normal 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
23
example/nist521.sec
Normal 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
15
example/test25519.sec
Normal 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
57
example/test2k.sec
Normal file
|
@ -0,0 +1,57 @@
|
|||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
lQOYBGDOQxoBCAC+C6rvdVplEEFAydO8ofu4x/wDj/4WI6dA1u6ZFTzHpl45/wIJ
|
||||
tTqoVA0TrG907PpwnUqjaJPoO4j4WhipyoPC1V1Yddj8nFozErHWXcMhSOXiQlq4
|
||||
zWQyayuQvwEsMf46jDvPi7ec5bR3IZsV+lH61PiIu5wvkc2DvtqpWqRJybS3Juzv
|
||||
kw0cRPH5A/HNYHTtgRnt7pAKizjRVrTIGR+ewF5udH88itoyV+6rAvSqEtsqLqhF
|
||||
GyRlPUuikTbqP0KHSElMuNCXkMiPS62IvhmHvhEl5cE60Y9wnVHXzye48yV7XFNs
|
||||
TUt1ky+DDJ/UTBR41c07zNtxjieNb2E9KMqDABEBAAEAB/wMpx02ofMM2hcZSt0T
|
||||
tYGOk2Ev4Yg/dBjTsQIKSiCVg3n8u+goU+fd9iUEVs1frp/PG86XPSqy+84j1TSU
|
||||
9VxuMXA5QZta2JQLmEAj1aYP3/K1bJW6sRE6Z1BrhVpjVWH0oIqI4ZrHa0znefxn
|
||||
0a/5Y1cd6Pokk3BoR/tdHbsYOHEeIFcBzDWH6hSkalBiMqcG3VA+D7uQeN/JynQe
|
||||
PIFRCDRLCvl5c4rOyWXEG32u+MbsjgW1fcQJtETdkxMMG/90vZ+9H4rHo0NJwrPg
|
||||
1sGxmuhGW/NZ+GZ5VQgrDrp2fKpuW5znTM2zZ0HVfZ7p0ByYFpyGHeJWMZCtqaLC
|
||||
0zw5BADLgrQvWZbAL+MAk7Cun8AOAv7pj407WwqbdP31KWY+bXrdbq70ZJ5b5JNK
|
||||
XntvXo0125PwoOksKcbr/sa6LaRu9oHV/g/m+WXMDjUbA3+WZswJOZDEdaNW9IAV
|
||||
BYHoNJsty79yIJvATisLBVaZRCyw+oD9DLcJQePV9rE7Fc2lWwQA7w/nh4wpvVkE
|
||||
efcIJtDdhLJRGS1rqUoEPu52/jR/JWtESbXiD7hvGoygzWXXuAe5VFVERfoV3szR
|
||||
4oPcGVxFGR4yiSLT45fvPW2spqXsB8TWZGKTj0DcvEmpmj6G3i45cJmdSYM2sqIk
|
||||
vqiVsRO7e6F8m/KDO9q0FU71LmS57/kEAI2LwlxfI/MheahFJbiWdOzPe4TKQuGx
|
||||
Of4/JN9TRtV2ibr53HoJioXk35z4/oo3UoUxBN0WvBuVQxfr3toE+6gAQaiunYU2
|
||||
a9HXn0ZGkqjst0O529sHjTgNkRroobkuuCtR47SgnaLfQNCFjYYMT16+r+ygvZOF
|
||||
3VUClmXwHwRtRpi0HFRlc3QgVXNlciA8dGVzdEBleGFtcGxlLm9yZz6JAU4EEwEI
|
||||
ADgWIQSXAmJZ/XISimwHy2tD1f7egUIA/gUCYM5DGgIbAwULCQgHAgYVCgkICwIE
|
||||
FgIDAQIeAQIXgAAKCRBD1f7egUIA/nkYCACDzHPEO6TImZlv1d5yKJgk7ky++vzl
|
||||
vd2lQlbwymFV438qk111F2xgDvNYLu0Lg6T+oXNv4cqXoCJQuAp5Wqj1xMqXsW+i
|
||||
SCe7/S0KfhQALnLIGRqIYEdqZmirbN9T1QE9OQsaYtbrH66kgBMczMz1Up9dSLWZ
|
||||
C4yFAnmv/ax2zfzU2LO14KYoSp98weoVIs/UjroLeVjP7XsRcN7n3P//4DU66rNf
|
||||
JoFtHE9lfAPoAcLQK+/JjekI388MvewB8wMLgi+Iz8Lm+XKYE49QpyMFmDeQYZfP
|
||||
8eaSZCCyLywLdAwJ1JDT0hLKMbxoEHualNI/ux/BAsVi6jsnghALJiwTnQOYBGDO
|
||||
QxoBCAC3AY3xkgxR5TTvePEIaRFaSL36KyGy0uS08/ymVFX9s0HMPMFo6wLnFzJR
|
||||
Ig315xN2ZEExm3NaXdExEsove16RST8LtOALVcpvwwdzgkmwCruaCx18gWbB7bTv
|
||||
Bh8AzckvXGlk9VlhV1eE2cV/19UrdCDhuXm8XKmAq9djbZ+fRodNkaQHbCqAd+86
|
||||
RSsSuEfyF7tSsqT4yG78egq2KITfOvfiQmm3spTQTeEXH1phQkNLKqSxLtU71oek
|
||||
P4+rhWQ8TMC10vzfzrE6CX6GzKGwwnBpExUiFUS2lcpik2u3hqWucopf934wzeGj
|
||||
L4UV+gy1ziE3mU2JmoeEg0vWaordABEBAAEAB/0ZCFTmsNAPmbcqdJQfzuNpQp79
|
||||
681xvQg8uk0aYVnb2JvM+JiKJe9hNdqTn4FiXAfc/2ytgPJ/72pQeJ4AbbMrU2YU
|
||||
z1qAIm2M6RQJWE6FDorH0PJpF/g62a2QrnkqLnvxBwaBoU/nET/u86zgxmCpz3o5
|
||||
9hlxSwmCiL7vIk0dx5f5FFFM5q45qJPkKMIy9FKb0q3l9c85KSkluY9wKUKbRTCb
|
||||
dkZfYHYHIrBRZItB3rbCdXvHXapQCw3/KmNY+5cOTc4kHlb2nKXt5d9mKyobiwyI
|
||||
QgSJg0yOPchyR+PXY8/PYsY6RRhIa9dC4CAzlmfX12m3QPeOfNVaaiNcUhrvBADE
|
||||
tQ4opGnqV3ipzNUAzxEH1a3fsNiSD8SJSOUwL5poEO945RiTOtt0jvnnuqh0ZQEh
|
||||
RhOBntVQkvrxZEnEFKPGcV6fqmkaNYlt0Bbl7fW87LDJ4BBqNb0jwBZsDaptryMq
|
||||
2pKX6/sohJYJG0OW9yKineBzzltHlNddfZzbGO23MwQA7is+8sq+MgsTMDeTQGPS
|
||||
f5MwQIGTj6mmfhXa0FazupU3ESPGpkdqLvWWpFSD5t+DNdVkolcz0Vf85coQxF49
|
||||
eBmx0W36nEJOKF51hZdS911qgPWniJjDKKTB6lhibYaildKdqbA3t7VwRPhlnBWL
|
||||
BrUvW2eYU4ERA+y6U3wQda8D/3osEv/YWPvU5dcm9GS/6ojFhmVXvdkqz58aUeGz
|
||||
XjXkxDTtTQ2IaVpLwHkVNFc42dqHbG+L/Yd8rp0tfAsu19aH/NcMMrVBPb0VJFER
|
||||
8CPYOZjnHTE1KYKPr2zyzF4Gm7EEaPLnuUzkj1MMWfg/B0XjXkOftgB1qNhFs5yA
|
||||
oeTRRH6JATYEGAEIACAWIQSXAmJZ/XISimwHy2tD1f7egUIA/gUCYM5DGgIbDAAK
|
||||
CRBD1f7egUIA/rxbB/90aohL+/0QxcEI/i4txkuM3dzPMXO72p/jPOKCY4gFbVSZ
|
||||
K8kduH9aF7XvhloBKbPMGlvDr7bSN3XLsexeZ3+shPlblYgBn7o7ar+vEtI8xgW+
|
||||
Twt7jcxrTuZYeRjc5PpZ/prPbtLuzuzd95Ba5hAO+W5SpE+fF0WpN6HxkxsOnIGS
|
||||
U2q63EnnJAxnF2eMhAFZ3cVqCA6zTm3NDj3iHzRTCHiHXotc2ZDtzLlwgwIKgWDq
|
||||
FZivkerXU/1+723L4ruJmukVDObO2GufULDs2YuUKh0Dd8EDcYqkVIm8fe4gpiSu
|
||||
EPObfS7HFHddyP2Oa+tYEoCAJmN31MGmA7mQ+xWR
|
||||
=c/bE
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
81
example/test3k.sec
Normal file
81
example/test3k.sec
Normal 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
105
example/test4k.sec
Normal file
|
@ -0,0 +1,105 @@
|
|||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
=AhOY
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
19
openpgp-card-sequoia/Cargo.toml
Normal file
19
openpgp-card-sequoia/Cargo.toml
Normal 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"
|
33
openpgp-card-sequoia/README.md
Normal file
33
openpgp-card-sequoia/README.md
Normal 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
|
||||
```
|
169
openpgp-card-sequoia/src/decryptor.rs
Normal file
169
openpgp-card-sequoia/src/decryptor.rs
Normal 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(())
|
||||
}
|
||||
}
|
284
openpgp-card-sequoia/src/lib.rs
Normal file
284
openpgp-card-sequoia/src/lib.rs
Normal 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")
|
||||
}
|
203
openpgp-card-sequoia/src/main.rs
Normal file
203
openpgp-card-sequoia/src/main.rs
Normal 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(())
|
||||
}
|
140
openpgp-card-sequoia/src/signer.rs
Normal file
140
openpgp-card-sequoia/src/signer.rs
Normal 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
20
openpgp-card/Cargo.toml
Normal 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
14
openpgp-card/README.md
Normal 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.
|
74
openpgp-card/src/apdu/command.rs
Normal file
74
openpgp-card/src/apdu/command.rs
Normal 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)
|
||||
}
|
||||
}
|
128
openpgp-card/src/apdu/commands.rs
Normal file
128
openpgp-card/src/apdu/commands.rs
Normal 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)
|
||||
}
|
169
openpgp-card/src/apdu/mod.rs
Normal file
169
openpgp-card/src/apdu/mod.rs
Normal 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())
|
||||
}
|
||||
}
|
111
openpgp-card/src/apdu/response.rs
Normal file
111
openpgp-card/src/apdu/response.rs
Normal 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
55
openpgp-card/src/card.rs
Normal 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
157
openpgp-card/src/errors.rs
Normal 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),
|
||||
}
|
383
openpgp-card/src/key_upload.rs
Normal file
383
openpgp-card/src/key_upload.rs
Normal 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
762
openpgp-card/src/lib.rs
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
209
openpgp-card/src/parse/algo_attrs.rs
Normal file
209
openpgp-card/src/parse/algo_attrs.rs
Normal 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))
|
||||
}
|
||||
}
|
307
openpgp-card/src/parse/algo_info.rs
Normal file
307
openpgp-card/src/parse/algo_info.rs
Normal 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)))
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
56
openpgp-card/src/parse/application_id.rs
Normal file
56
openpgp-card/src/parse/application_id.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
43
openpgp-card/src/parse/cardholder.rs
Normal file
43
openpgp-card/src/parse/cardholder.rs
Normal 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 })
|
||||
}
|
||||
}
|
111
openpgp-card/src/parse/extended_cap.rs
Normal file
111
openpgp-card/src/parse/extended_cap.rs
Normal 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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
36
openpgp-card/src/parse/extended_length_info.rs
Normal file
36
openpgp-card/src/parse/extended_length_info.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
75
openpgp-card/src/parse/fingerprint.rs
Normal file
75
openpgp-card/src/parse/fingerprint.rs
Normal 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)
|
||||
}
|
157
openpgp-card/src/parse/historical.rs
Normal file
157
openpgp-card/src/parse/historical.rs
Normal 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
|
57
openpgp-card/src/parse/mod.rs
Normal file
57
openpgp-card/src/parse/mod.rs
Normal 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))
|
||||
}
|
||||
}
|
24
openpgp-card/src/tlv/length.rs
Normal file
24
openpgp-card/src/tlv/length.rs
Normal 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
250
openpgp-card/src/tlv/mod.rs
Normal 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
119
openpgp-card/src/tlv/tag.rs
Normal 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());
|
||||
}
|
||||
}
|
20
openpgp-card/src/tlv/value.rs
Normal file
20
openpgp-card/src/tlv/value.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue