From 58031a9d6847c0612f754e31de0f855b35820b3e Mon Sep 17 00:00:00 2001 From: Jacob Heider Date: Mon, 25 Nov 2024 21:58:00 -0500 Subject: [PATCH] combine cli and lib using workspace --- Cargo.lock | 79 +++++- Cargo.toml | 31 +-- README.md | 2 +- bpb-pkgx-cli/Cargo.toml | 26 ++ {src => bpb-pkgx-cli/src}/config.rs | 0 {src => bpb-pkgx-cli/src}/key_data.rs | 0 {src => bpb-pkgx-cli/src}/keychain.rs | 0 {src => bpb-pkgx-cli/src}/main.rs | 0 {src => bpb-pkgx-cli/src}/tests.rs | 0 pbp-pkgx-lib/Cargo.toml | 29 ++ pbp-pkgx-lib/LICENSE-APACHE | 201 ++++++++++++++ pbp-pkgx-lib/LICENSE-MIT | 23 ++ pbp-pkgx-lib/README.md | 41 +++ pbp-pkgx-lib/examples/print.rs | 18 ++ pbp-pkgx-lib/examples/props/data.txt | 6 + pbp-pkgx-lib/examples/props/key.txt | 9 + pbp-pkgx-lib/examples/props/sig.txt | 7 + pbp-pkgx-lib/examples/read_sig.rs | 36 +++ pbp-pkgx-lib/examples/round_trip.rs | 34 +++ pbp-pkgx-lib/examples/verify_sig.rs | 27 ++ pbp-pkgx-lib/src/ascii_armor.rs | 117 ++++++++ pbp-pkgx-lib/src/key.rs | 300 +++++++++++++++++++++ pbp-pkgx-lib/src/lib.rs | 84 ++++++ pbp-pkgx-lib/src/packet.rs | 71 +++++ pbp-pkgx-lib/src/sig.rs | 375 ++++++++++++++++++++++++++ 25 files changed, 1477 insertions(+), 39 deletions(-) create mode 100644 bpb-pkgx-cli/Cargo.toml rename {src => bpb-pkgx-cli/src}/config.rs (100%) rename {src => bpb-pkgx-cli/src}/key_data.rs (100%) rename {src => bpb-pkgx-cli/src}/keychain.rs (100%) rename {src => bpb-pkgx-cli/src}/main.rs (100%) rename {src => bpb-pkgx-cli/src}/tests.rs (100%) create mode 100644 pbp-pkgx-lib/Cargo.toml create mode 100644 pbp-pkgx-lib/LICENSE-APACHE create mode 100644 pbp-pkgx-lib/LICENSE-MIT create mode 100644 pbp-pkgx-lib/README.md create mode 100644 pbp-pkgx-lib/examples/print.rs create mode 100644 pbp-pkgx-lib/examples/props/data.txt create mode 100644 pbp-pkgx-lib/examples/props/key.txt create mode 100644 pbp-pkgx-lib/examples/props/sig.txt create mode 100644 pbp-pkgx-lib/examples/read_sig.rs create mode 100644 pbp-pkgx-lib/examples/round_trip.rs create mode 100644 pbp-pkgx-lib/examples/verify_sig.rs create mode 100644 pbp-pkgx-lib/src/ascii_armor.rs create mode 100644 pbp-pkgx-lib/src/key.rs create mode 100644 pbp-pkgx-lib/src/lib.rs create mode 100644 pbp-pkgx-lib/src/packet.rs create mode 100644 pbp-pkgx-lib/src/sig.rs diff --git a/Cargo.lock b/Cargo.lock index 2885445..d53ee58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,9 +49,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitflags" -version = "1.0.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" @@ -80,7 +80,7 @@ dependencies = [ "failure", "hex", "pbp_pkgx", - "rand", + "rand 0.8.5", "serde", "serde_derive", "sha2 0.7.1", @@ -274,6 +274,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -282,7 +293,7 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -300,7 +311,6 @@ checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "pbp_pkgx" version = "0.4.1" -source = "git+https://github.com/pkgxdev//pbp-pkgx.git?rev=5aa86ffeb794a8c120b33b85f8ae463b7cc05f3d#5aa86ffeb794a8c120b33b85f8ae463b7cc05f3d" dependencies = [ "base64", "bitflags", @@ -308,7 +318,9 @@ dependencies = [ "digest 0.7.5", "ed25519-dalek", "failure", + "rand 0.7.3", "sha1", + "sha2 0.7.1", "typenum", ] @@ -355,6 +367,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -362,8 +387,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -373,7 +408,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -382,7 +426,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -467,7 +520,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -560,6 +613,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index bbba798..5076e81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,3 @@ -[package] -name = "bpb_pkgx" -description = "boats's personal barricade - pkgx updates" -license = "MIT OR Apache-2.0" -version = "1.1.1" -repository = "https://github.com/pkgxdev/bpb-pkgx" -edition = "2021" -authors = ["Without Boats ", "Jacob Heider "] - -[dependencies] -toml = "0.4.6" -rand = { version = "0.8.5", features = ["std"] } -sha2 = "0.7.1" -serde_derive = "1.0.215" -serde = "1.0.215" -hex = "0.3.2" -failure = "0.1.1" - -[dependencies.pbp_pkgx] -version = "0.4.1" -git = "https://github.com/pkgxdev//pbp-pkgx.git" -rev = "5aa86ffeb794a8c120b33b85f8ae463b7cc05f3d" -features = ["dalek"] - -[dependencies.ed25519-dalek] -# [dependencies.ed25519] -# package = "ed25519-dalek" -version = "2.1.1" +[workspace] +members = ["bpb-pkgx-cli", "pbp-pkgx-lib"] +default-members = ["bpb-pkgx-cli"] diff --git a/README.md b/README.md index 0f18773..73f7acf 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ do. ```sh git clone https://github.com/pkgxdev/bpb-pkgx cd bpb-pkgx -cargo install --path . +cargo install --path bpb-pkgx-cli ``` ## How to Set Up diff --git a/bpb-pkgx-cli/Cargo.toml b/bpb-pkgx-cli/Cargo.toml new file mode 100644 index 0000000..fb18f3f --- /dev/null +++ b/bpb-pkgx-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "bpb_pkgx" +description = "boats's personal barricade - pkgx updates" +license = "MIT OR Apache-2.0" +version = "1.1.1" +repository = "https://github.com/pkgxdev/bpb-pkgx" +edition = "2021" +authors = ["Without Boats ", "Jacob Heider "] + +[dependencies] +toml = "0.4.6" +rand = { version = "0.8.5", features = ["std"] } +sha2 = "0.7.1" +serde_derive = "1.0.215" +serde = "1.0.215" +hex = "0.3.2" +failure = "0.1.1" + +[dependencies.pbp_pkgx] +path = "../pbp-pkgx-lib" +features = ["dalek"] + +[dependencies.ed25519-dalek] +# [dependencies.ed25519] +# package = "ed25519-dalek" +version = "2.1.1" diff --git a/src/config.rs b/bpb-pkgx-cli/src/config.rs similarity index 100% rename from src/config.rs rename to bpb-pkgx-cli/src/config.rs diff --git a/src/key_data.rs b/bpb-pkgx-cli/src/key_data.rs similarity index 100% rename from src/key_data.rs rename to bpb-pkgx-cli/src/key_data.rs diff --git a/src/keychain.rs b/bpb-pkgx-cli/src/keychain.rs similarity index 100% rename from src/keychain.rs rename to bpb-pkgx-cli/src/keychain.rs diff --git a/src/main.rs b/bpb-pkgx-cli/src/main.rs similarity index 100% rename from src/main.rs rename to bpb-pkgx-cli/src/main.rs diff --git a/src/tests.rs b/bpb-pkgx-cli/src/tests.rs similarity index 100% rename from src/tests.rs rename to bpb-pkgx-cli/src/tests.rs diff --git a/pbp-pkgx-lib/Cargo.toml b/pbp-pkgx-lib/Cargo.toml new file mode 100644 index 0000000..16922c8 --- /dev/null +++ b/pbp-pkgx-lib/Cargo.toml @@ -0,0 +1,29 @@ +[package] +authors = ["Without Boats ", "Jacob Heider "] +description = "bridge non-PGP system to PGP data format - pkgx updates" +edition = "2021" +license = "MIT OR Apache-2.0" +readme = "README.md" +name = "pbp_pkgx" +version = "0.4.1" +repository = "https://github.com/pkgxdev/bpb-pkgx" + +[dependencies] +base64 = "0.9.2" +byteorder = "1.1.0" +digest = "0.7.0" +sha1 = "0.2.0" +typenum = "1.9.0" +failure = "0.1.1" +bitflags = "1.3.2" + +[dependencies.ed25519-dalek] +version = "2.1.1" +optional = true + +[features] +dalek = ["ed25519-dalek"] + +[dev-dependencies] +rand = "0.7.3" +sha2 = "0.7.1" diff --git a/pbp-pkgx-lib/LICENSE-APACHE b/pbp-pkgx-lib/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/pbp-pkgx-lib/LICENSE-APACHE @@ -0,0 +1,201 @@ + 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 + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pbp-pkgx-lib/LICENSE-MIT b/pbp-pkgx-lib/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/pbp-pkgx-lib/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pbp-pkgx-lib/README.md b/pbp-pkgx-lib/README.md new file mode 100644 index 0000000..cfe561d --- /dev/null +++ b/pbp-pkgx-lib/README.md @@ -0,0 +1,41 @@ +# pbp - Pretty Bad Protocol - pkgx updates + +## Update + +updated by pkgx to edition 2021. + +### TODO + +- [ ] fix `cargo run --examples foo` + +## Description + +This crate lets you generate OpenPGP datagrams from ed25519 keys and +signatures; it is intended to bridge from a non-PGP system to a transport +medium that expects PGP data. + +```rust +fn print_key(keypair: KeyPair) { + let pgp_key = PgpKey::new(&keypair.public[..], "user id string", |data| { + keypair.sign(data).to_bytes() + }); + println!("{}", pgp_key); +} +``` + +It's agnostic about what library you use to implement ed25519, but it has a +feature which integrates with [ed25519-dalek][dalek] + +Thanks to isis lovecruft and Henry de Valence for assistance with the dalek API +and understanding the OpenPGP specification. + +## Demonstration + +The "print" example prints an ASCII armored OpenPGP public key to stdout; you +can check that using: + +```sh +cargo run --features dalek --example print +``` + +[dalek]: https://github.com/isislovecruft/ed25519-dalek diff --git a/pbp-pkgx-lib/examples/print.rs b/pbp-pkgx-lib/examples/print.rs new file mode 100644 index 0000000..d5dd31c --- /dev/null +++ b/pbp-pkgx-lib/examples/print.rs @@ -0,0 +1,18 @@ +extern crate ed25519_dalek as dalek; +extern crate pbp_pkgx; +extern crate rand; +extern crate sha2; + +use dalek::SigningKey; +use pbp_pkgx::{KeyFlags, PgpKey}; +use rand::{rngs::OsRng, RngCore}; +use sha2::{Sha256, Sha512}; + +fn main() { + let mut cspring = [0u8; 32]; + OsRng.fill_bytes(&mut cspring); + let keypair = SigningKey::from_bytes(&mut cspring); + + let key = PgpKey::from_dalek::(&keypair, KeyFlags::NONE, 0, "withoutboats"); + println!("{}", key); +} diff --git a/pbp-pkgx-lib/examples/props/data.txt b/pbp-pkgx-lib/examples/props/data.txt new file mode 100644 index 0000000..82ceb24 --- /dev/null +++ b/pbp-pkgx-lib/examples/props/data.txt @@ -0,0 +1,6 @@ +tree 01ea054cde7d42a706d3e1734014e4c2aadd2a3b +parent 170bc1aece1c5dc0c9e02150fbc430750a90ef8e +author Without Boats 1512095290 -0800 +committer Without Boats 1512095290 -0800 + +Update README. diff --git a/pbp-pkgx-lib/examples/props/key.txt b/pbp-pkgx-lib/examples/props/key.txt new file mode 100644 index 0000000..e13bd5b --- /dev/null +++ b/pbp-pkgx-lib/examples/props/key.txt @@ -0,0 +1,9 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEWh36qhYJKwYBBAHaRw8BAQdAOHEGR6r6ulmlAiaaH4e+OHzhxLrDX7S5GXZZ +HJwDLze0IHdpdGhvdXRib2F0cyA8Ym9hdHNAbW96aWxsYS5jb20+iJAEExYIADgW +IQS2NbX7Xtnqc/lTXwocxwMQvjkS1QUCWh36qgIbAwULCQgHAgYVCAkKCwIEFgID +AQIeAQIXgAAKCRAcxwMQvjkS1RPlAP48cIZPgy6wPlfydr8CoPMEYy9n9grbmCDw +KxxTmKyWHAD+LZiHpQ3LCYuUidQYYbr/+4GhtuJUNLiIbSwxtgBdVAQ= +=P0tw +-----END PGP PUBLIC KEY BLOCK----- diff --git a/pbp-pkgx-lib/examples/props/sig.txt b/pbp-pkgx-lib/examples/props/sig.txt new file mode 100644 index 0000000..0f5807b --- /dev/null +++ b/pbp-pkgx-lib/examples/props/sig.txt @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYIAB0WIQS2NbX7Xtnqc/lTXwocxwMQvjkS1QUCWiC+PgAKCRAcxwMQvjkS +1Y5IAQDNk4Bu4sCAHTlvUSS9ioOo9yWDqIvliE1aBvZeZCzDLgEAgQSsQtP8Rqq/ +f+SHxLV2cgZpFLcKEIg0odi8Uxv4WAk= +=uBdw +-----END PGP SIGNATURE----- diff --git a/pbp-pkgx-lib/examples/read_sig.rs b/pbp-pkgx-lib/examples/read_sig.rs new file mode 100644 index 0000000..0b5df7b --- /dev/null +++ b/pbp-pkgx-lib/examples/read_sig.rs @@ -0,0 +1,36 @@ +extern crate ed25519_dalek as dalek; +extern crate pbp_pkgx; +extern crate rand; +extern crate sha2; + +use std::io::{self, BufRead}; + +use pbp_pkgx::PgpSig; + +fn main() { + let stdin = io::stdin(); + let mut stdin = stdin.lock(); + + let mut armor = String::new(); + + let mut in_armor = false; + + loop { + let mut buf = String::new(); + stdin.read_line(&mut buf).unwrap(); + if buf.trim().starts_with("-----") && buf.trim().ends_with("-----") { + armor.push_str(&buf); + if in_armor { + break; + } else { + in_armor = true; + } + } else if in_armor { + armor.push_str(&buf); + } + } + + if PgpSig::from_ascii_armor(&armor).is_ok() { + println!("Valid PGP Signature"); + } +} diff --git a/pbp-pkgx-lib/examples/round_trip.rs b/pbp-pkgx-lib/examples/round_trip.rs new file mode 100644 index 0000000..8b1d8c0 --- /dev/null +++ b/pbp-pkgx-lib/examples/round_trip.rs @@ -0,0 +1,34 @@ +extern crate ed25519_dalek as dalek; +extern crate pbp_pkgx; +extern crate rand; +extern crate sha2; + +use dalek::{SigningKey, VerifyingKey}; +use pbp_pkgx::{KeyFlags, PgpKey, PgpSig, SigType}; +use rand::{rngs::OsRng, RngCore}; +use sha2::{Sha256, Sha512}; + +const DATA: &[u8] = b"How will I ever get out of this labyrinth?"; + +fn main() { + let mut cspring = [0u8; 32]; + OsRng.fill_bytes(&mut cspring); + let keypair = SigningKey::from_bytes(&mut cspring); + + let key = PgpKey::from_dalek::(&keypair, KeyFlags::SIGN, 0, "withoutboats"); + let sig = PgpSig::from_dalek::( + &keypair, + DATA, + key.fingerprint(), + SigType::BinaryDocument, + 0, + ); + let mut key_bytes = [0u8; 32]; + key_bytes.copy_from_slice(DATA); + let public_key = VerifyingKey::from_bytes(&key_bytes).unwrap(); + if sig.verify_dalek::(&public_key, keypair.verifying_key().into()) { + println!("Verified successfully."); + } else { + println!("Could not verify."); + } +} diff --git a/pbp-pkgx-lib/examples/verify_sig.rs b/pbp-pkgx-lib/examples/verify_sig.rs new file mode 100644 index 0000000..5d359ae --- /dev/null +++ b/pbp-pkgx-lib/examples/verify_sig.rs @@ -0,0 +1,27 @@ +extern crate pbp_pkgx; +extern crate sha2; + +use std::env; +use std::fs; +use std::path::PathBuf; + +use pbp_pkgx::{PgpKey, PgpSig}; +use sha2::{Sha256, Sha512}; + +fn main() { + let root = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let props = root.join("examples").join("props"); + + let sig: String = fs::read_to_string(props.join("sig.txt")).unwrap(); + let key: String = fs::read_to_string(props.join("key.txt")).unwrap(); + let data: String = fs::read_to_string(props.join("data.txt")).unwrap(); + + let sig = PgpSig::from_ascii_armor(&sig).unwrap(); + let key = PgpKey::from_ascii_armor(&key).unwrap(); + + if sig.verify_dalek::(data.as_bytes().into(), &key.to_dalek().unwrap()) { + println!("Verified signature."); + } else { + println!("Could not verify signature."); + } +} diff --git a/pbp-pkgx-lib/src/ascii_armor.rs b/pbp-pkgx-lib/src/ascii_armor.rs new file mode 100644 index 0000000..0bc6995 --- /dev/null +++ b/pbp-pkgx-lib/src/ascii_armor.rs @@ -0,0 +1,117 @@ +// This module implements the ASCII armoring required by the OpenPGP +// specification, converting binary PGP datagrams into ASCII data. +use std::fmt; + +use byteorder::{BigEndian, ByteOrder}; + +use crate::PgpError; +use crate::PgpError::InvalidAsciiArmor; + +impl From for PgpError { + fn from(_: base64::DecodeError) -> PgpError { + InvalidAsciiArmor + } +} + +// Convert from an ASCII armored string into binary data. +pub fn remove_ascii_armor( + s: &str, + expected_header: &str, + expected_footer: &str, +) -> Result, PgpError> { + let lines: Vec<&str> = s.lines().map(|s| s.trim()).collect(); + let header = lines.first().ok_or(InvalidAsciiArmor)?; + let footer = lines.last().ok_or(InvalidAsciiArmor)?; + + // Check header and footer + if !header.starts_with("-----") + || !footer.starts_with("-----") + || !header.ends_with("-----") + || !footer.ends_with("-----") + || header.trim_matches('-').trim() != expected_header + || footer.trim_matches('-').trim() != expected_footer + { + return Err(InvalidAsciiArmor); + } + + // Find the end of the header section + let end_of_headers = 1 + lines.iter().take_while(|l| !l.is_empty()).count(); + if end_of_headers >= lines.len() - 2 { + return Err(InvalidAsciiArmor); + } + + // Decode the base64'd data + let ascii_armored: String = lines[end_of_headers..lines.len() - 2].concat(); + let data = base64::decode(&ascii_armored)?; + + // Confirm checksum + let cksum_line = &lines[lines.len() - 2]; + if !cksum_line.starts_with("=") || !cksum_line.len() > 1 { + return Err(InvalidAsciiArmor); + } + let mut cksum = [0; 4]; + base64::decode_config_slice(&cksum_line[1..], base64::STANDARD, &mut cksum[..])?; + if BigEndian::read_u32(&cksum[..]) != checksum_crc24(&data) { + return Err(InvalidAsciiArmor); + } + + Ok(data) +} + +// Ascii armors data into the formatter +pub fn ascii_armor( + header: &'static str, + footer: &'static str, + data: &[u8], + f: &mut fmt::Formatter, +) -> fmt::Result { + // Header Line + f.write_str("-----")?; + f.write_str(header)?; + f.write_str("-----\n\n")?; + + // Base64'd data + let b64_cfg = base64::Config::new( + base64::CharacterSet::Standard, + true, + false, + base64::LineWrap::Wrap(76, base64::LineEnding::LF), + ); + f.write_str(&base64::encode_config(data, b64_cfg))?; + f.write_str("\n=")?; + + // Checksum + let cksum = checksum_crc24(data); + let mut cksum_buf = [0; 4]; + BigEndian::write_u32(&mut cksum_buf, cksum); + f.write_str(&base64::encode(&cksum_buf[1..4]))?; + + // Footer Line + f.write_str("\n-----")?; + f.write_str(footer)?; + f.write_str("-----\n")?; + + Ok(()) +} + +// Translation of checksum function from RFC 4880, section 6.1. +fn checksum_crc24(data: &[u8]) -> u32 { + const CRC24_INIT: u32 = 0x_00B7_04CE; + const CRC24_POLY: u32 = 0x_0186_4CFB; + + let mut crc = CRC24_INIT; + + for &byte in data { + crc ^= (byte as u32) << 16; + + for _ in 0..8 { + crc <<= 1; + + if (crc & 0x_0100_0000) != 0 { + crc ^= CRC24_POLY; + } + } + } + + crc & 0x_00FF_FFFF +} diff --git a/pbp-pkgx-lib/src/key.rs b/pbp-pkgx-lib/src/key.rs new file mode 100644 index 0000000..e847617 --- /dev/null +++ b/pbp-pkgx-lib/src/key.rs @@ -0,0 +1,300 @@ +use std::fmt::{self, Debug, Display}; +use std::ops::Range; +use std::str::FromStr; + +use byteorder::{BigEndian, ByteOrder}; +use digest::Digest; +use sha1::Sha1; +use typenum::U32; + +#[cfg(feature = "dalek")] +use dalek::Signer; +#[cfg(feature = "dalek")] +use ed25519_dalek as dalek; +#[cfg(feature = "dalek")] +use typenum::U64; + +use crate::ascii_armor::{ascii_armor, remove_ascii_armor}; +use crate::packet::*; +use crate::Base64; + +use crate::PgpError; +use crate::{Fingerprint, KeyFlags, Signature}; +use crate::{PgpSig, SigType, SubPacket}; + +// curve identifier (curve25519) +const CURVE: &[u8] = &[0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xda, 0x47, 0x0f, 0x01]; + +/// An OpenPGP formatted ed25519 public key. +/// +/// This allows you to transmit an ed25519 key as a PGP key. Though gpg +/// and other implementations will probably be willing to import this +/// public key, it is not designed for use within the PGP ecosystem, but +/// rather to transfer public key data through mediums in which an OpenPGP +/// formatted key is expected. +/// +/// This type implements Display by ASCII armoring the public key data. +#[derive(Eq, PartialEq, Hash)] +pub struct PgpKey { + data: Vec, +} + +impl PgpKey { + /// Construct a PgpKey from an ed25519 public key. + /// + /// This will construct a valid OpenPGP Public Key datagram with the + /// following packets: + /// + /// - A public key packet (formatted according to the "EdDSA for OpenPGP" + /// extension draft) + /// - A user id (whatever string you pass as the user id argument) + /// - A self-signature + /// + /// The sign function must be a valid function for signing data with the + /// private key paired with the public key. You are required to provide + /// this so that you don't have to trust this library with direct access + /// to the private key. + /// + /// # Warnings + /// + /// This will panic if your key is not 32 bits of data. It will not + /// otherwise verify that your key is a valid ed25519 key. + pub fn new( + key: &[u8], + flags: KeyFlags, + user_id: &str, + unix_time: u32, + sign: F, + ) -> PgpKey + where + Sha256: Digest, + F: Fn(&[u8]) -> Signature, + { + assert!(key.len() == 32); + + let mut data = Vec::with_capacity(user_id.len() + 180); + + let key_packet_range = write_public_key_packet(&mut data, key, unix_time); + let fingerprint = fingerprint(&data[key_packet_range.clone()]); + write_user_id_packet(&mut data, user_id); + + let sig_data = { + let mut data = Vec::from(&data[key_packet_range]); + data.extend(&[0xb4]); + data.extend(&bigendian_u32(user_id.len() as u32)); + data.extend(user_id.as_bytes()); + data + }; + + let signature_packet = PgpSig::new::( + &sig_data, + fingerprint, + SigType::PositiveCertification, + unix_time, + &[ + SubPacket { + tag: 27, + data: &[flags.bits()], + }, + SubPacket { + tag: 23, + data: &[0x80], + }, + ], + sign, + ); + + data.extend(signature_packet.as_bytes()); + + PgpKey { data } + } + + /// Construct a PgpKey struct from an OpenPGP public key. + /// + /// This does minimal verification of the data received. it ensures that + /// the initial portion of the data is an OpenPGP public key packet, + /// formatted to contain an ed25519 public key. It does not ensure that + /// the actual public key is a valid ed25519 key, and no verification is + /// done on the remainder of the data. + /// + /// As a result, a key constructed this way many not successfully import + /// into an OpenPGP implementation like gpg. + pub fn from_bytes(bytes: &[u8]) -> Result { + let (packet_data, end) = find_public_key_packet(bytes)?; + + // Validate that this is a version 4 curve25519 EdDSA key. + if !is_ed25519_valid(packet_data) { + return Err(PgpError::UnsupportedPublicKeyPacket); + } + + // convert public key packet to the old style header, + // two byte length. All methods on PgpKey assume the + // public key is in that format (e.g. the fingerprint + // method). + let data = if bytes[0] != 0x99 { + let mut packet = prepare_packet(6, |packet| packet.extend(packet_data)); + packet.extend(&bytes[end..]); + packet + } else { + bytes.to_owned() + }; + + Ok(PgpKey { data }) + } + + /// Construct a PgpKey from an ASCII armored string. + pub fn from_ascii_armor(string: &str) -> Result { + let data = remove_ascii_armor( + string, + "BEGIN PGP PUBLIC KEY BLOCK", + "END PGP PUBLIC KEY BLOCK", + )?; + PgpKey::from_bytes(&data) + } + + /// The ed25519 public key data contained in this key. + /// + /// This slice will be thirty-two bytes long. + pub fn key_data(&self) -> [u8; 32] { + let mut rv = [0; 32]; + rv.copy_from_slice(&self.data[22..54]); + rv + } + + /// All of the bytes in this key (including PGP metadata). + pub fn as_bytes(&self) -> &[u8] { + &self.data[..] + } + + /// The OpenPGP fingerprint of this public key. + pub fn fingerprint(&self) -> Fingerprint { + fingerprint(&self.data[0..54]) + } + + #[cfg(feature = "dalek")] + /// Create a PgpKey from a dalek Keypair and a user_id string. + pub fn from_dalek( + keypair: &dalek::SigningKey, + flags: KeyFlags, + unix_time: u32, + user_id: &str, + ) -> PgpKey + where + Sha256: Digest, + Sha512: Digest, + { + PgpKey::new::( + keypair.verifying_key().as_bytes(), + flags, + user_id, + unix_time, + |data| keypair.sign(data).to_bytes(), + ) + } + + #[cfg(feature = "dalek")] + /// Convert this key into a dalek PublicKey. + /// + /// This will validate that the key data is a correct ed25519 public key. + pub fn to_dalek(&self) -> Result { + dalek::VerifyingKey::from_bytes(&self.key_data()) + } +} + +impl Debug for PgpKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("PgpKey") + .field("key", &Base64(&self.data[..])) + .finish() + } +} + +impl Display for PgpKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + ascii_armor( + "BEGIN PGP PUBLIC KEY BLOCK", + "END PGP PUBLIC KEY BLOCK", + &self.data[..], + f, + ) + } +} + +impl FromStr for PgpKey { + type Err = PgpError; + fn from_str(s: &str) -> Result { + PgpKey::from_ascii_armor(s) + } +} + +fn write_public_key_packet(data: &mut Vec, key: &[u8], unix_time: u32) -> Range { + write_packet(data, 6, |packet| { + packet.push(4); // packet version #4 + packet.extend(&bigendian_u32(unix_time)); + packet.push(22); // algorithm id #22 (edDSA) + + packet.extend(CURVE); + + let mut key_data = Vec::with_capacity(33); + key_data.push(0x40); + key_data.extend(key); + write_mpi(packet, &key_data); + }) +} + +fn write_user_id_packet(data: &mut Vec, user_id: &str) -> Range { + write_packet(data, 13, |packet| packet.extend(user_id.as_bytes())) +} + +// Mainly this function parses the possible packet headers. +// If the data begins with a valid old public key packet using +// anything but the indeterminate length header format, it +// will return the data of that public key packet. +fn find_public_key_packet(data: &[u8]) -> Result<(&[u8], usize), PgpError> { + let (init, len) = match data.first() { + Some(&0x98) => { + if data.len() < 2 { + return Err(PgpError::InvalidPacketHeader); + } + let len = data[1] as usize; + (2, len) + } + Some(&0x99) => { + if data.len() < 3 { + return Err(PgpError::InvalidPacketHeader); + } + let len = BigEndian::read_u16(&data[1..3]) as usize; + (3, len) + } + Some(&0x9a) => { + if data.len() < 5 { + return Err(PgpError::InvalidPacketHeader); + } + let len = BigEndian::read_u32(&data[1..5]) as usize; + if len > u16::MAX as usize { + return Err(PgpError::UnsupportedPacketLength); + } + (5, len) + } + _ => return Err(PgpError::UnsupportedPacketLength), + }; + let end = init + len; + if data.len() < end { + return Err(PgpError::InvalidPacketHeader); + } + Ok((&data[init..end], end)) +} + +fn fingerprint(key_packet: &[u8]) -> [u8; 20] { + let mut hasher = Sha1::new(); + hasher.update(key_packet); + hasher.digest().bytes() +} + +fn is_ed25519_valid(packet: &[u8]) -> bool { + packet.len() == 51 + && packet[0] == 0x04 + && packet[5] == 0x16 + && &packet[6..16] == CURVE + && packet[16..19] == [0x01, 0x07, 0x40] +} diff --git a/pbp-pkgx-lib/src/lib.rs b/pbp-pkgx-lib/src/lib.rs new file mode 100644 index 0000000..de88ee8 --- /dev/null +++ b/pbp-pkgx-lib/src/lib.rs @@ -0,0 +1,84 @@ +//! This library is designed to integrate non-PGP generated and verified keys +//! and signatures with channels that expect PGP data. It specifically only +//! supports the ed25519 signature scheme. +//! +//! Sometimes you want to be able to sign data, and the only reasonable channel +//! to transmit signatures and public keys available to you expects them to be +//! PGP formatted. If you don't want to use a heavyweight dependency like gpg, +//! this library supports only the minimal necessary components of the PGP +//! format to transmit your keys and signatures. +#![deny(missing_docs, missing_debug_implementations)] +// Otherwise, bitflags! complains about a 0x0 value +#![allow(clippy::bad_bit_mask)] + +#[macro_use] +extern crate failure; +#[macro_use] +extern crate bitflags; + +#[cfg(feature = "dalek")] +extern crate ed25519_dalek as dalek; + +mod ascii_armor; +mod packet; + +mod key; +mod sig; + +pub use crate::key::PgpKey; +pub use crate::sig::{PgpSig, SigType, SubPacket}; + +/// An OpenPGP public key fingerprint. +pub type Fingerprint = [u8; 20]; +/// An ed25519 signature. +pub type Signature = [u8; 64]; + +bitflags! { + /// The key flags assigned to this key. + pub struct KeyFlags: u8 { + /// No key flags. + const NONE = 0x00; + /// The Certify flag. + const CERTIFY = 0x01; + /// The Sign flag. + const SIGN = 0x02; + /// The Encrypt Communication flag. + const ENCRYPT_COMS = 0x04; + /// The Encrypt Storage flag. + const ENCRYPT_STORAGE = 0x08; + /// The Authentication flag. + const AUTHENTICATION = 0x20; + } +} + +/// An error returned while attempting to parse a PGP signature or public key. +#[derive(Fail, Debug)] +pub enum PgpError { + /// Invalid ASCII armor format + #[fail(display = "Invalid ASCII armor format")] + InvalidAsciiArmor, + /// Packet header incorrectly formatted + #[fail(display = "Packet header incorrectly formatted")] + InvalidPacketHeader, + /// Unsupported packet length format + #[fail(display = "Unsupported packet length format")] + UnsupportedPacketLength, + /// Unsupported form of signature packet + #[fail(display = "Unsupported form of signature packet")] + UnsupportedSignaturePacket, + /// First hashed subpacket of signature must be the key fingerprint + #[fail(display = "First hashed subpacket of signature must be the key fingerprint")] + MissingFingerprintSubpacket, + /// Unsupported form of public key packet + #[fail(display = "Unsupported form of public key packet")] + UnsupportedPublicKeyPacket, +} + +// Helper for writing base64 data +struct Base64<'a>(&'a [u8]); + +impl<'a> std::fmt::Debug for Base64<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(&base64::encode(self.0)) + } +} diff --git a/pbp-pkgx-lib/src/packet.rs b/pbp-pkgx-lib/src/packet.rs new file mode 100644 index 0000000..577760d --- /dev/null +++ b/pbp-pkgx-lib/src/packet.rs @@ -0,0 +1,71 @@ +use std::ops::Range; + +use byteorder::{BigEndian, ByteOrder}; + +pub(crate) type BigEndianU32 = [u8; 4]; +pub(crate) type BigEndianU16 = [u8; 2]; + +pub(crate) fn write_packet)>( + data: &mut Vec, + tag: u8, + write: F, +) -> Range { + let init = data.len(); + let header_tag = (tag << 2) | 0b_1000_0001; + data.extend(&[header_tag, 0, 0]); + write(data); + let len = data.len() - init - 3; + assert!(len < u16::MAX as usize); + BigEndian::write_u16(&mut data[(init + 1)..(init + 3)], len as u16); + init..data.len() +} + +pub(crate) fn prepare_packet)>(tag: u8, write: F) -> Vec { + let mut packet = vec![0, 0, 0]; + write(&mut packet); + packet[0] = (tag << 2) | 0b_1000_0001; + let len = packet.len() - 3; + BigEndian::write_u16(&mut packet[1..3], len as u16); + packet +} + +pub(crate) fn write_subpackets(packet: &mut Vec, write_each_subpacket: F) +where + F: Fn(&mut Vec), +{ + packet.extend(&[0, 0]); + let init = packet.len(); + write_each_subpacket(packet); + let len = packet.len() - init; + assert!(len < u16::MAX as usize); + BigEndian::write_u16(&mut packet[(init - 2)..init], len as u16); +} + +pub(crate) fn write_single_subpacket)>(packet: &mut Vec, tag: u8, write: F) { + packet.extend(&[0, tag]); + let init = packet.len() - 1; + write(packet); + let len = packet.len() - init; + assert!(len < 191); + packet[init - 1] = len as u8; +} + +pub(crate) fn write_mpi(data: &mut Vec, mpi: &[u8]) { + assert!(mpi.len() < (u16::MAX / 8) as usize); + assert!(!mpi.is_empty()); + let len = bigendian_u16((mpi.len() * 8 - (mpi[0].leading_zeros() as usize)) as u16); + data.extend(&len); + data.extend(mpi); +} + +pub(crate) fn bigendian_u32(data: u32) -> BigEndianU32 { + let mut out = BigEndianU32::default(); + BigEndian::write_u32(&mut out, data); + out +} + +pub(crate) fn bigendian_u16(data: u16) -> BigEndianU16 { + let mut out = BigEndianU16::default(); + BigEndian::write_u16(&mut out, data); + out +} diff --git a/pbp-pkgx-lib/src/sig.rs b/pbp-pkgx-lib/src/sig.rs new file mode 100644 index 0000000..e508f8f --- /dev/null +++ b/pbp-pkgx-lib/src/sig.rs @@ -0,0 +1,375 @@ +use std::fmt::{self, Debug, Display}; +use std::str::FromStr; + +use byteorder::{BigEndian, ByteOrder}; +use digest::Digest; +use typenum::U32; + +#[cfg(feature = "dalek")] +use dalek::Signer; +#[cfg(feature = "dalek")] +use ed25519_dalek as dalek; +#[cfg(feature = "dalek")] +use typenum::U64; + +use crate::ascii_armor::{ascii_armor, remove_ascii_armor}; +use crate::packet::*; +use crate::Base64; +use crate::PgpError; +use crate::{Fingerprint, Signature}; + +/// The valid types of OpenPGP signatures. +#[allow(missing_docs)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub enum SigType { + BinaryDocument = 0x00, + TextDocument = 0x01, + Standalone = 0x02, + GenericCertification = 0x10, + PersonaCertification = 0x11, + CasualCertification = 0x12, + PositiveCertification = 0x13, + SubkeyBinding = 0x18, + PrimaryKeyBinding = 0x19, + DirectlyOnKey = 0x1F, + KeyRevocation = 0x20, + SubkeyRevocation = 0x28, + CertificationRevocation = 0x30, + Timestamp = 0x40, + ThirdPartyConfirmation = 0x50, +} + +/// A subpacket to be hashed into the signed data. +/// +/// See RFC 4880 for more information. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Debug)] +pub struct SubPacket<'a> { + /// The tag for this subpacket. + pub tag: u8, + /// The data in this subpacket. + pub data: &'a [u8], +} + +/// An OpenPGP formatted ed25519 signature. +#[derive(Eq, PartialEq, Hash)] +pub struct PgpSig { + data: Vec, +} + +impl PgpSig { + /// Construct a new PGP signature. + /// + /// This will construct a valid OpenPGP signature using the ed25519 + /// signing algorithm & SHA-256 hashing algorithm. It will contain + /// these hashed subpackets: + /// - A version 4 key fingerprint + /// - A timestamp + /// - Whatever subpackets you pass as arguments + /// + /// It will contain the key id as an unhashed subpacket. + pub fn new( + data: &[u8], + fingerprint: Fingerprint, + sig_type: SigType, + unix_time: u32, + subpackets: &[SubPacket], + sign: F, + ) -> PgpSig + where + Sha256: Digest, + F: Fn(&[u8]) -> Signature, + { + let data = prepare_packet(2, |packet| { + packet.push(4); // version number + packet.push(sig_type as u8); // signature class + packet.push(22); // signing algorithm (EdDSA) + packet.push(8); // hash algorithm (SHA-256) + + write_subpackets(packet, |hashed_subpackets| { + // fingerprint + write_single_subpacket(hashed_subpackets, 33, |packet| { + packet.push(4); + packet.extend(&fingerprint); + }); + + // timestamp + write_single_subpacket(hashed_subpackets, 2, |packet| { + packet.extend(&bigendian_u32(unix_time)) + }); + + for &SubPacket { tag, data } in subpackets { + write_single_subpacket(hashed_subpackets, tag, |packet| packet.extend(data)); + } + }); + + let hash = { + let mut hasher = Sha256::default(); + + hasher.process(data); + + hasher.process(&packet[3..]); + + hasher.process(&[0x04, 0xff]); + hasher.process(&bigendian_u32((packet.len() - 3) as u32)); + + hasher.fixed_result() + }; + + write_subpackets(packet, |unhashed_subpackets| { + write_single_subpacket(unhashed_subpackets, 16, |packet| { + packet.extend(&fingerprint[12..]); + }); + }); + + packet.extend(&hash[0..2]); + + let signature = sign(&hash[..]); + write_mpi(packet, &signature[00..32]); + write_mpi(packet, &signature[32..64]); + }); + + PgpSig { data } + } + + /// Parse an OpenPGP signature from binary data. + /// + /// This must be an ed25519 signature using SHA-256 for hashing, + /// and it must be in the subset of OpenPGP supported by this library. + pub fn from_bytes(bytes: &[u8]) -> Result { + // TODO: convert to three byte header + let (data, packet) = find_signature_packet(bytes)?; + has_correct_structure(packet)?; + has_correct_hashed_subpackets(packet)?; + Ok(PgpSig { data }) + } + + /// Parse an OpenPGP signature from ASCII armored data. + pub fn from_ascii_armor(string: &str) -> Result { + let data = remove_ascii_armor(string, "BEGIN PGP SIGNATURE", "END PGP SIGNATURE")?; + PgpSig::from_bytes(&data) + } + + /// Get the binary representation of this signature. + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + /// Get the portion of this signature hashed into the signed data. + pub fn hashed_section(&self) -> &[u8] { + let subpackets_len = BigEndian::read_u16(&self.data[7..9]) as usize; + &self.data[3..(subpackets_len + 9)] + } + + /// Get the actual ed25519 signature contained. + pub fn signature(&self) -> Signature { + let init = self.data.len() - 68; + let sig_data = &self.data[init..]; + let mut sig = [0; 64]; + sig[00..32].clone_from_slice(&sig_data[2..34]); + sig[32..64].clone_from_slice(&sig_data[36..68]); + sig + } + + /// Get the fingerprint of the public key which made this signature. + pub fn fingerprint(&self) -> Fingerprint { + let mut fingerprint = [0; 20]; + fingerprint.clone_from_slice(&self.data[10..30]); + fingerprint + } + + /// Get the type of this signature. + pub fn sig_type(&self) -> SigType { + match self.data[4] { + 0x00 => SigType::BinaryDocument, + 0x01 => SigType::TextDocument, + 0x02 => SigType::Standalone, + 0x10 => SigType::GenericCertification, + 0x11 => SigType::PersonaCertification, + 0x12 => SigType::CasualCertification, + 0x13 => SigType::PositiveCertification, + 0x18 => SigType::SubkeyBinding, + 0x19 => SigType::PrimaryKeyBinding, + 0x1F => SigType::DirectlyOnKey, + 0x20 => SigType::KeyRevocation, + 0x28 => SigType::SubkeyRevocation, + 0x30 => SigType::CertificationRevocation, + 0x40 => SigType::Timestamp, + 0x50 => SigType::ThirdPartyConfirmation, + _ => panic!("Unrecognized signature type."), + } + } + + /// Verify data against this signature. + /// + /// The data to be verified should be inputed by hashing it into the + /// SHA-256 hasher using the input function. + pub fn verify(&self, input: F1, verify: F2) -> bool + where + Sha256: Digest, + F1: FnOnce(&mut Sha256), + F2: FnOnce(&[u8], Signature) -> bool, + { + let hash = { + let mut hasher = Sha256::default(); + + input(&mut hasher); + + let hashed_section = self.hashed_section(); + hasher.process(hashed_section); + + hasher.process(&[0x04, 0xff]); + hasher.process(&bigendian_u32(hashed_section.len() as u32)); + + hasher.fixed_result() + }; + + verify(&hash[..], self.signature()) + } + + #[cfg(feature = "dalek")] + /// Convert this signature from an ed25519-dalek signature. + pub fn from_dalek( + keypair: &dalek::SigningKey, + data: &[u8], + fingerprint: Fingerprint, + sig_type: SigType, + timestamp: u32, + ) -> PgpSig + where + Sha256: Digest, + Sha512: Digest, + { + PgpSig::new::(data, fingerprint, sig_type, timestamp, &[], |data| { + keypair.sign(data).to_bytes() + }) + } + + #[cfg(feature = "dalek")] + /// Convert this signature to an ed25519-dalek signature. + pub fn to_dalek(&self) -> dalek::Signature { + dalek::Signature::from_bytes(&self.signature()) + } + + #[cfg(feature = "dalek")] + /// Verify this signature against an ed25519-dalek public key. + pub fn verify_dalek(&self, key: &dalek::VerifyingKey, input: F) -> bool + where + Sha256: Digest, + Sha512: Digest, + F: FnOnce(&mut Sha256), + { + self.verify::(input, |data, signature| { + let sig = dalek::Signature::from_bytes(&signature); + key.verify_strict(data, &sig).is_ok() + }) + } +} + +impl Debug for PgpSig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("PgpSig") + .field("key", &Base64(&self.data[..])) + .finish() + } +} + +impl Display for PgpSig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + ascii_armor( + "BEGIN PGP SIGNATURE", + "END PGP SIGNATURE", + &self.data[..], + f, + ) + } +} + +impl FromStr for PgpSig { + type Err = PgpError; + fn from_str(s: &str) -> Result { + PgpSig::from_ascii_armor(s) + } +} + +fn find_signature_packet(data: &[u8]) -> Result<(Vec, &[u8]), PgpError> { + let (init, len) = match data.first() { + Some(&0x88) => { + if data.len() < 2 { + return Err(PgpError::InvalidPacketHeader); + } + (2, data[1] as usize) + } + Some(&0x89) => { + if data.len() < 3 { + return Err(PgpError::InvalidPacketHeader); + } + let len = BigEndian::read_u16(&data[1..3]); + (3, len as usize) + } + Some(&0x8a) => { + if data.len() < 5 { + return Err(PgpError::InvalidPacketHeader); + } + let len = BigEndian::read_u32(&data[1..5]); + if len > u16::MAX as u32 { + return Err(PgpError::UnsupportedPacketLength); + } + (5, len as usize) + } + _ => return Err(PgpError::UnsupportedPacketLength), + }; + + if data.len() < init + len { + return Err(PgpError::InvalidPacketHeader); + } + + let packet = &data[init..][..len]; + + if init == 3 { + Ok((data.to_owned(), packet)) + } else { + let mut vec = Vec::with_capacity(3 + len); + let len = bigendian_u16(len as u16); + vec.push(0x89); + vec.push(len[0]); + vec.push(len[1]); + vec.extend(packet.iter().cloned()); + Ok((vec, packet)) + } +} + +fn has_correct_structure(packet: &[u8]) -> Result<(), PgpError> { + if packet.len() < 6 { + return Err(PgpError::UnsupportedSignaturePacket); + } + + if !(packet[0] == 4 && packet[2] == 22 && packet[3] == 8) { + return Err(PgpError::UnsupportedSignaturePacket); + } + + let hashed_len = BigEndian::read_u16(&packet[4..6]) as usize; + if packet.len() < hashed_len + 8 { + return Err(PgpError::UnsupportedSignaturePacket); + } + + let unhashed_len = BigEndian::read_u16(&packet[(hashed_len + 6)..][..2]) as usize; + if packet.len() != unhashed_len + hashed_len + 78 { + return Err(PgpError::UnsupportedSignaturePacket); + } + + Ok(()) +} + +fn has_correct_hashed_subpackets(packet: &[u8]) -> Result<(), PgpError> { + let hashed_len = BigEndian::read_u16(&packet[4..6]) as usize; + if hashed_len < 23 { + return Err(PgpError::MissingFingerprintSubpacket); + } + + // check that the first subpacket is a fingerprint subpacket + if !(packet[6] == 22 && packet[7] == 33 && packet[8] == 4) { + return Err(PgpError::MissingFingerprintSubpacket); + } + + Ok(()) +}