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-----
|
||||||
|
|
||||||
|
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
|
||||||
|
=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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
|
||||||
|
v8D42gLfcfS7nP4f03y16G7+xd1yyl3AFH+xugPRPaa7VvA2NyRhUdlVYlGkBNTO
|
||||||
|
JbZJmX72daEFRv3Mxffs+aaDEzf+h4X4Kr5byc3WiebDmW5fzpEeIKsTPeK2VNq7
|
||||||
|
oMyRo7ektNn9+PacI7RQ5A4tXgJODwzPrkoUiTkx94C9NsWAekVvS8JMrXOK9JL/
|
||||||
|
0jY/k5bNCLvEZ9APAM4U3qhZvOgeiQG2BBgBCAAgFiEE9rYTQEGbxDo+snN8jmBA
|
||||||
|
Z/uzF28FAmDPuNICGwwACgkQjmBAZ/uzF28+iQwAjIv6+tU3kc6lIIwvv4VcaZVy
|
||||||
|
Wpt+cpSnzdnRHv7MNWfb7tGssOL02uj2+DH25Xfz7/wS1nAtLyRloLZQID2ZsSBr
|
||||||
|
7XItoUXMKFvqVqn3Kn0LmMyQpP1J6iHZFOBRUDW7D9lFGR8PUIk+8S8gBWPJkmCa
|
||||||
|
QN9Aeuo+WK1L/Y+tx4T4GM/VRlQStD7dvUFG+kdGgP2Djv0Ur41GDLg8LzzinSeV
|
||||||
|
7xy0EG9whCzxAPJZAr7guW626LPwQbEgZ/ssIVJqqE+zlU7s7zHbXVWBENO5Mbkv
|
||||||
|
+aSI2VTlISfz2y3MVRegiVamVFOqzdgxEP1C7uql/wujBS82jw8LCltUqb+VdSpE
|
||||||
|
V5fowI3OaCFyMa0ymtsMtfaa9SFbsLs4kgzkVAufI+E/z6qYUFql8UiVA1lgEqIM
|
||||||
|
Bj8h4HrC0a9D1N4p4X0UGOdR+X5JSIfdoCfV7PfDvn3RDJiROlSxpzu7qVPnkiVE
|
||||||
|
noy+ZG9vgolIYAxj0DiTaz5pdHfHSN51YxhHM10d
|
||||||
|
=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