diff --git a/.github/workflows/ssh-encoding.yml b/.github/workflows/ssh-encoding.yml new file mode 100644 index 00000000..86dab636 --- /dev/null +++ b/.github/workflows/ssh-encoding.yml @@ -0,0 +1,61 @@ +name: ssh-encoding + +on: + pull_request: + paths: + - ".github/workflows/ssh-encoding.yml" + - "ssh-encoding/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: ssh-encoding + +env: + RUSTFLAGS: "-Dwarnings" + +jobs: + minimal-versions: + uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master + with: + working-directory: ${{ github.workflow }} + + no_std: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.60.0 # MSRV + - stable + target: + - thumbv7em-none-eabi + - wasm32-unknown-unknown + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + override: true + - uses: RustCrypto/actions/cargo-hack-install@master + - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features default,getrandom,std + + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.60.0 # MSRV + - stable + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - uses: RustCrypto/actions/cargo-hack-install@master + - run: cargo hack test --feature-powerset diff --git a/Cargo.lock b/Cargo.lock index 71ea0703..b30061f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,12 +724,20 @@ dependencies = [ "der", ] +[[package]] +name = "ssh-encoding" +version = "0.0.0" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2 0.10.6", +] + [[package]] name = "ssh-key" version = "0.5.0-pre.0" dependencies = [ "aes", - "base64ct", "bcrypt-pbkdf", "ctr", "dsa", @@ -738,7 +746,6 @@ dependencies = [ "num-bigint-dig", "p256", "p384", - "pem-rfc7468", "rand_chacha 0.3.1", "rand_core 0.6.4", "rsa", @@ -747,6 +754,7 @@ dependencies = [ "sha1", "sha2 0.10.6", "signature", + "ssh-encoding", "subtle", "tempfile", "zeroize", diff --git a/Cargo.toml b/Cargo.toml index 73ae08df..dcae887c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,9 @@ [workspace] -members = ["ssh-key"] resolver = "2" +members = [ + "ssh-encoding", + "ssh-key" +] [profile.dev] opt-level = 2 diff --git a/ssh-encoding/CHANGELOG.md b/ssh-encoding/CHANGELOG.md new file mode 100644 index 00000000..d6637e04 --- /dev/null +++ b/ssh-encoding/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/ssh-encoding/Cargo.toml b/ssh-encoding/Cargo.toml new file mode 100644 index 00000000..3534024d --- /dev/null +++ b/ssh-encoding/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "ssh-encoding" +version = "0.0.0" +description = """ +Pure Rust implementation of SSH data type decoders/encoders as described +in RFC4251 +""" +authors = ["RustCrypto Developers"] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/RustCrypto/SSH/tree/master/ssh-encoding" +categories = ["authentication", "cryptography", "encoding", "no-std", "parser-implementations"] +keywords = ["crypto", "certificate", "key", "openssh", "ssh"] +readme = "README.md" +edition = "2021" +rust-version = "1.60" + +[dependencies] +base64 = { package = "base64ct", version = "1.4", optional = true } +pem = { package = "pem-rfc7468", version = "0.6", optional = true } +sha2 = { version = "0.10", optional = true, default-features = false } + +[features] +alloc = ["base64?/alloc", "pem?/alloc"] +std = ["alloc", "base64?/std", "pem?/std", "sha2?/std"] + +pem = ["base64", "dep:pem"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/ssh-encoding/LICENSE-APACHE b/ssh-encoding/LICENSE-APACHE new file mode 100644 index 00000000..78173fa2 --- /dev/null +++ b/ssh-encoding/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/ssh-encoding/LICENSE-MIT b/ssh-encoding/LICENSE-MIT new file mode 100644 index 00000000..c869ada5 --- /dev/null +++ b/ssh-encoding/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2021 The RustCrypto Project Developers + +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/ssh-encoding/README.md b/ssh-encoding/README.md new file mode 100644 index 00000000..3251474d --- /dev/null +++ b/ssh-encoding/README.md @@ -0,0 +1,55 @@ +# [RustCrypto]: SSH Encoding + +[![crate][crate-image]][crate-link] +[![Docs][docs-image]][docs-link] +[![Build Status][build-image]][build-link] +![Apache2/MIT licensed][license-image] +![Rust Version][rustc-image] +[![Project Chat][chat-image]][chat-link] + +[Documentation][docs-link] + +## About + +Pure Rust implementation of SSH data type decoders/encoders as described +in [RFC4251]. + +## Minimum Supported Rust Version + +This crate requires **Rust 1.60** at a minimum. + +We may change the MSRV in the future, but it will be accompanied by a minor +version bump. + +## License + +Licensed under either of: + + * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) + * [MIT license](http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[//]: # (badges) + +[crate-image]: https://buildstats.info/crate/ssh-encoding +[crate-link]: https://crates.io/crates/ssh-encoding +[docs-image]: https://docs.rs/ssh-encoding/badge.svg +[docs-link]: https://docs.rs/ssh-encoding/ +[license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg +[rustc-image]: https://img.shields.io/badge/rustc-1.60+-blue.svg +[chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg +[chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/346919-SSH +[build-image]: https://github.com/RustCrypto/SSH/actions/workflows/ssh-encoding.yml/badge.svg +[build-link]: https://github.com/RustCrypto/SSH/actions/workflows/ssh-encoding.yml + +[//]: # (links) + +[RustCrypto]: https://github.com/rustcrypto +[RFC4251]: https://datatracker.ietf.org/doc/html/rfc4251 diff --git a/ssh-key/src/checked.rs b/ssh-encoding/src/checked.rs similarity index 93% rename from ssh-key/src/checked.rs rename to ssh-encoding/src/checked.rs index 6e6db30f..013574da 100644 --- a/ssh-key/src/checked.rs +++ b/ssh-encoding/src/checked.rs @@ -3,7 +3,7 @@ use crate::{Error, Result}; /// Extension trait for providing checked [`Iterator::sum`]-like functionality. -pub(crate) trait CheckedSum: Sized { +pub trait CheckedSum: Sized { /// Iterate over the values of this type, computing a checked sum. /// /// Returns [`Error::Length`] on overflow. diff --git a/ssh-key/src/decode.rs b/ssh-encoding/src/decode.rs similarity index 88% rename from ssh-key/src/decode.rs rename to ssh-encoding/src/decode.rs index de0f6da8..332c0a7d 100644 --- a/ssh-key/src/decode.rs +++ b/ssh-encoding/src/decode.rs @@ -14,13 +14,18 @@ const MAX_SIZE: usize = 0xFFFFF; /// Decoder trait. /// /// This trait describes how to decode a given type. -pub(crate) trait Decode: Sized { - /// Attempt to decode a value of this type using the provided [`Decoder`]. - fn decode(reader: &mut impl Reader) -> Result; +pub trait Decode: Sized { + /// Type returned in the event of a decoding error. + type Error: From; + + /// Attempt to decode a value of this type using the provided [`Reader`]. + fn decode(reader: &mut impl Reader) -> core::result::Result; } /// Decode a single `byte` from the input data. impl Decode for u8 { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let mut buf = [0]; reader.read(&mut buf)?; @@ -36,6 +41,8 @@ impl Decode for u8 { /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 impl Decode for u32 { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let mut bytes = [0u8; 4]; reader.read(&mut bytes)?; @@ -50,6 +57,8 @@ impl Decode for u32 { /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 impl Decode for u64 { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let mut bytes = [0u8; 8]; reader.read(&mut bytes)?; @@ -65,6 +74,8 @@ impl Decode for u64 { /// Enforces a library-internal limit of 1048575, as the main use case for /// `usize` is length prefixes. impl Decode for usize { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let n = usize::try_from(u32::decode(reader)?)?; @@ -80,10 +91,12 @@ impl Decode for usize { /// /// > A byte represents an arbitrary 8-bit value (octet). Fixed length /// > data is sometimes represented as an array of bytes, written -/// > byte[n], where n is the number of bytes in the array. +/// > `byte[n]`, where n is the number of bytes in the array. /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 impl Decode for [u8; N] { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { reader.read_nested(|reader| { let mut result = [(); N].map(|_| 0); @@ -97,12 +110,14 @@ impl Decode for [u8; N] { /// /// > A byte represents an arbitrary 8-bit value (octet). Fixed length /// > data is sometimes represented as an array of bytes, written -/// > byte[n], where n is the number of bytes in the array. +/// > `byte[n]`, where n is the number of bytes in the array. /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] impl Decode for Vec { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { reader.read_nested(|reader| { let mut result = vec![0u8; reader.remaining_len()]; @@ -115,6 +130,8 @@ impl Decode for Vec { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] impl Decode for String { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { String::from_utf8(Vec::decode(reader)?).map_err(|_| Error::CharacterEncoding) } @@ -123,6 +140,8 @@ impl Decode for String { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] impl Decode for Vec { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { reader.read_nested(|reader| { let mut entries = Self::new(); diff --git a/ssh-key/src/encode.rs b/ssh-encoding/src/encode.rs similarity index 83% rename from ssh-key/src/encode.rs rename to ssh-encoding/src/encode.rs index bf7f1d16..90a83697 100644 --- a/ssh-key/src/encode.rs +++ b/ssh-encoding/src/encode.rs @@ -3,27 +3,33 @@ //! //! [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 -use crate::{checked::CheckedSum, writer::Writer, Result}; +use crate::{checked::CheckedSum, writer::Writer, Error, Result}; #[cfg(feature = "alloc")] -use { - crate::Error, - alloc::{string::String, vec::Vec}, -}; +use alloc::{string::String, vec::Vec}; /// Encoding trait. /// /// This trait describes how to encode a given type. -pub(crate) trait Encode { +pub trait Encode { + /// Type returned in the event of an encoding error. + type Error: From; + /// Get the length of this type encoded in bytes, prior to Base64 encoding. - fn encoded_len(&self) -> Result; + fn encoded_len(&self) -> core::result::Result; + + /// Encode this value using the provided [`Writer`]. + fn encode(&self, writer: &mut impl Writer) -> core::result::Result<(), Self::Error>; - /// Encode this value using the provided [`Encoder`]. - fn encode(&self, writer: &mut impl Writer) -> Result<()>; + /// Return the length of this type after encoding when prepended with a + /// `uint32` length prefix. + fn encoded_len_nested(&self) -> core::result::Result { + Ok([4, self.encoded_len()?].checked_sum()?) + } /// Encode this value, first prepending a `uint32` length prefix /// set to [`Encode::encoded_len`]. - fn encode_nested(&self, writer: &mut impl Writer) -> Result<()> { + fn encode_nested(&self, writer: &mut impl Writer) -> core::result::Result<(), Self::Error> { self.encoded_len()?.encode(writer)?; self.encode(writer) } @@ -31,6 +37,8 @@ pub(crate) trait Encode { /// Encode a single `byte` to the writer. impl Encode for u8 { + type Error = Error; + fn encoded_len(&self) -> Result { Ok(1) } @@ -48,6 +56,8 @@ impl Encode for u8 { /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 impl Encode for u32 { + type Error = Error; + fn encoded_len(&self) -> Result { Ok(4) } @@ -64,6 +74,8 @@ impl Encode for u32 { /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 impl Encode for u64 { + type Error = Error; + fn encoded_len(&self) -> Result { Ok(8) } @@ -80,6 +92,8 @@ impl Encode for u64 { /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 impl Encode for usize { + type Error = Error; + fn encoded_len(&self) -> Result { Ok(4) } @@ -93,10 +107,12 @@ impl Encode for usize { /// /// > A byte represents an arbitrary 8-bit value (octet). Fixed length /// > data is sometimes represented as an array of bytes, written -/// > byte[n], where n is the number of bytes in the array. +/// > `byte[n]`, where n is the number of bytes in the array. /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 impl Encode for [u8] { + type Error = Error; + fn encoded_len(&self) -> Result { [4, self.len()].checked_sum() } @@ -111,10 +127,12 @@ impl Encode for [u8] { /// /// > A byte represents an arbitrary 8-bit value (octet). Fixed length /// > data is sometimes represented as an array of bytes, written -/// > byte[n], where n is the number of bytes in the array. +/// > `byte[n]`, where n is the number of bytes in the array. /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 impl Encode for [u8; N] { + type Error = Error; + fn encoded_len(&self) -> Result { self.as_slice().encoded_len() } @@ -142,6 +160,8 @@ impl Encode for [u8; N] { /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 impl Encode for &str { + type Error = Error; + fn encoded_len(&self) -> Result { self.as_bytes().encoded_len() } @@ -154,6 +174,8 @@ impl Encode for &str { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] impl Encode for Vec { + type Error = Error; + fn encoded_len(&self) -> Result { self.as_slice().encoded_len() } @@ -166,6 +188,8 @@ impl Encode for Vec { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] impl Encode for String { + type Error = Error; + fn encoded_len(&self) -> Result { self.as_str().encoded_len() } @@ -178,6 +202,8 @@ impl Encode for String { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] impl Encode for Vec { + type Error = Error; + fn encoded_len(&self) -> Result { self.iter().try_fold(4usize, |acc, string| { acc.checked_add(string.encoded_len()?).ok_or(Error::Length) diff --git a/ssh-encoding/src/error.rs b/ssh-encoding/src/error.rs new file mode 100644 index 00000000..93bacbf4 --- /dev/null +++ b/ssh-encoding/src/error.rs @@ -0,0 +1,102 @@ +//! Error types + +use core::fmt; + +/// Result type with `ssh-encoding` crate's [`Error`] as the error type. +pub type Result = core::result::Result; + +/// Error type. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Error { + #[cfg(feature = "base64")] + #[cfg_attr(docsrs, doc(cfg(feature = "base64")))] + /// Base64-related errors. + Base64(base64::Error), + + /// Character encoding-related errors. + CharacterEncoding, + + /// Invalid length. + Length, + + /// Overflow errors. + Overflow, + + /// PEM encoding errors. + #[cfg(feature = "pem")] + Pem(pem::Error), + + /// Unexpected trailing data at end of message. + TrailingData { + /// Number of bytes of remaining data at end of message. + remaining: usize, + }, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + #[cfg(feature = "base64")] + Error::Base64(err) => write!(f, "Base64 encoding error: {}", err), + Error::CharacterEncoding => write!(f, "character encoding invalid"), + Error::Length => write!(f, "length invalid"), + Error::Overflow => write!(f, "internal overflow error"), + #[cfg(feature = "pem")] + Error::Pem(err) => write!(f, "{}", err), + Error::TrailingData { remaining } => write!( + f, + "unexpected trailing data at end of message ({} bytes)", + remaining + ), + } + } +} + +impl From for Error { + fn from(_: core::num::TryFromIntError) -> Error { + Error::Overflow + } +} + +impl From for Error { + fn from(_: core::str::Utf8Error) -> Error { + Error::CharacterEncoding + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl From for Error { + fn from(_: alloc::string::FromUtf8Error) -> Error { + Error::CharacterEncoding + } +} + +#[cfg(feature = "base64")] +#[cfg_attr(docsrs, doc(cfg(feature = "base64")))] +impl From for Error { + fn from(err: base64::Error) -> Error { + Error::Base64(err) + } +} + +#[cfg(feature = "base64")] +#[cfg_attr(docsrs, doc(cfg(feature = "base64")))] +impl From for Error { + fn from(_: base64::InvalidLengthError) -> Error { + Error::Length + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl From for Error { + fn from(err: pem::Error) -> Error { + Error::Pem(err) + } +} + +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +impl std::error::Error for Error {} diff --git a/ssh-encoding/src/label.rs b/ssh-encoding/src/label.rs new file mode 100644 index 00000000..b952769f --- /dev/null +++ b/ssh-encoding/src/label.rs @@ -0,0 +1,36 @@ +//! Convenience trait for decoding/encoding string labels. + +use crate::{Decode, Encode, Reader, Writer}; +use core::str::FromStr; + +/// Maximum size of any algorithm name/identifier. +const MAX_LABEL_SIZE: usize = 48; + +/// Labels for e.g. cryptographic algorithms. +/// +/// Receives a blanket impl of [`Decode`] and [`Encode`]. +pub trait Label: AsRef + FromStr { + /// Type returned in the event of an encoding error. + type Error: From; +} + +impl Decode for T { + type Error = T::Error; + + fn decode(reader: &mut impl Reader) -> Result { + let mut buf = [0u8; MAX_LABEL_SIZE]; + reader.read_string(buf.as_mut())?.parse() + } +} + +impl Encode for T { + type Error = T::Error; + + fn encoded_len(&self) -> Result { + Ok(self.as_ref().encoded_len()?) + } + + fn encode(&self, writer: &mut impl Writer) -> Result<(), T::Error> { + Ok(self.as_ref().encode(writer)?) + } +} diff --git a/ssh-encoding/src/lib.rs b/ssh-encoding/src/lib.rs new file mode 100644 index 00000000..148c89a0 --- /dev/null +++ b/ssh-encoding/src/lib.rs @@ -0,0 +1,57 @@ +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg" +)] +#![forbid(unsafe_code)] +#![warn( + clippy::integer_arithmetic, + clippy::panic, + clippy::panic_in_result_fn, + clippy::unwrap_used, + missing_docs, + rust_2018_idioms, + unused_lifetimes, + unused_qualifications +)] + +#[cfg(feature = "alloc")] +#[macro_use] +extern crate alloc; +#[cfg(feature = "std")] +extern crate std; + +mod checked; +mod decode; +mod encode; +mod error; +mod label; +mod reader; +mod writer; + +pub use crate::{ + checked::CheckedSum, + decode::Decode, + encode::Encode, + error::{Error, Result}, + label::Label, + reader::{NestedReader, Reader}, + writer::Writer, +}; + +#[cfg(feature = "base64")] +pub use { + crate::{ + reader::Base64Reader, + writer::{base64_len_approx, Base64Writer}, + }, + base64, +}; + +#[cfg(feature = "pem")] +pub use pem::{self, LineEnding}; + +/// Line width used by the PEM encoding of OpenSSH documents. +pub const PEM_LINE_WIDTH: usize = 70; diff --git a/ssh-key/src/reader.rs b/ssh-encoding/src/reader.rs similarity index 88% rename from ssh-key/src/reader.rs rename to ssh-encoding/src/reader.rs index d5e3f0f5..d2302f36 100644 --- a/ssh-key/src/reader.rs +++ b/ssh-encoding/src/reader.rs @@ -2,14 +2,15 @@ use crate::{decode::Decode, Error, Result}; use core::str; -use pem_rfc7468 as pem; /// Constant-time Base64 reader implementation. -pub(crate) type Base64Reader<'i> = base64ct::Decoder<'i, base64ct::Base64>; +#[cfg(feature = "base64")] +#[cfg_attr(docsrs, doc(cfg(feature = "base64")))] +pub type Base64Reader<'i> = base64::Decoder<'i, base64::Base64>; /// Reader trait which decodes the binary SSH protocol serialization from /// various inputs. -pub(crate) trait Reader: Sized { +pub trait Reader: Sized { /// Read as much data as is needed to exactly fill `out`. /// /// This is the base decoding method on which the rest of the trait is @@ -33,9 +34,10 @@ pub(crate) trait Reader: Sized { /// Decodes a `uint32` which identifies the length of some encapsulated /// data, then calls the given nested reader function with the length of /// the remaining data. - fn read_nested<'r, T, F>(&'r mut self, f: F) -> Result + fn read_nested<'r, T, E, F>(&'r mut self, f: F) -> core::result::Result where - F: FnOnce(&mut NestedReader<'r, Self>) -> Result, + E: From, + F: FnOnce(&mut NestedReader<'r, Self>) -> core::result::Result, { let len = usize::decode(self)?; @@ -49,12 +51,12 @@ pub(crate) trait Reader: Sized { /// /// > A byte represents an arbitrary 8-bit value (octet). Fixed length /// > data is sometimes represented as an array of bytes, written - /// > byte[n], where n is the number of bytes in the array. + /// > `byte[n]`, where n is the number of bytes in the array. /// /// Storage for the byte array must be provided as mutable byte slice in /// order to accommodate `no_std` use cases. /// - /// The [`Decode`] impl on [`Vec`] can be used to allocate a buffer for + /// The [`Decode`] impl on `Vec` can be used to allocate a buffer for /// the result. /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 @@ -85,7 +87,7 @@ pub(crate) trait Reader: Sized { /// Storage for the string data must be provided as mutable byte slice in /// order to accommodate `no_std` use cases. /// - /// The [`Decode`] impl on [`String`] can be used to allocate a buffer for + /// The [`Decode`] impl on `String` can be used to allocate a buffer for /// the result. /// /// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5 @@ -127,17 +129,26 @@ pub(crate) trait Reader: Sized { } } -impl Reader for Base64Reader<'_> { +impl Reader for &[u8] { fn read<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> { - Ok(self.decode(out)?) + if self.len() >= out.len() { + let (head, tail) = self.split_at(out.len()); + *self = tail; + out.copy_from_slice(head); + Ok(out) + } else { + Err(Error::Length) + } } fn remaining_len(&self) -> usize { - self.remaining_len() + self.len() } } -impl Reader for pem::Decoder<'_> { +#[cfg(feature = "base64")] +#[cfg_attr(docsrs, doc(cfg(feature = "base64")))] +impl Reader for Base64Reader<'_> { fn read<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> { Ok(self.decode(out)?) } @@ -147,25 +158,20 @@ impl Reader for pem::Decoder<'_> { } } -impl Reader for &[u8] { +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl Reader for pem::Decoder<'_> { fn read<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> { - if self.len() >= out.len() { - let (head, tail) = self.split_at(out.len()); - *self = tail; - out.copy_from_slice(head); - Ok(out) - } else { - Err(Error::Length) - } + Ok(self.decode(out)?) } fn remaining_len(&self) -> usize { - self.len() + self.remaining_len() } } /// Reader type used by [`Reader::read_nested`]. -pub(crate) struct NestedReader<'r, R: Reader> { +pub struct NestedReader<'r, R: Reader> { /// Inner reader type. inner: &'r mut R, diff --git a/ssh-key/src/writer.rs b/ssh-encoding/src/writer.rs similarity index 64% rename from ssh-key/src/writer.rs rename to ssh-encoding/src/writer.rs index cc26ea4b..2454ebc4 100644 --- a/ssh-key/src/writer.rs +++ b/ssh-encoding/src/writer.rs @@ -1,53 +1,65 @@ //! Writer trait and associated implementations. use crate::Result; -use pem_rfc7468 as pem; -use sha2::{Digest, Sha256, Sha512}; #[cfg(feature = "alloc")] use alloc::vec::Vec; +#[cfg(feature = "sha2")] +use sha2::{Digest, Sha256, Sha512}; + /// Get the estimated length of data when encoded as Base64. /// -/// This is an upper bound where the actual length might be slightly shorter. -#[cfg(feature = "alloc")] +/// This is an upper bound where the actual length might be slightly shorter, +/// and can be used to estimate the capacity of an output buffer. However, the +/// final result may need to be sliced and should use the actual encoded length +/// rather than this estimate. +#[cfg(feature = "base64")] #[allow(clippy::integer_arithmetic)] -pub(crate) fn base64_len(input_len: usize) -> usize { +pub fn base64_len_approx(input_len: usize) -> usize { // TODO(tarcieri): checked arithmetic (((input_len * 4) / 3) + 3) & !3 } /// Constant-time Base64 writer implementation. -pub(crate) type Base64Writer<'o> = base64ct::Encoder<'o, base64ct::Base64>; +#[cfg(feature = "base64")] +#[cfg_attr(docsrs, doc(cfg(feature = "base64")))] +pub type Base64Writer<'o> = base64::Encoder<'o, base64::Base64>; /// Writer trait which encodes the SSH binary format to various output /// encodings. -pub(crate) trait Writer: Sized { +pub trait Writer: Sized { /// Write the given bytes to the writer. fn write(&mut self, bytes: &[u8]) -> Result<()>; } -impl Writer for Base64Writer<'_> { +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl Writer for Vec { fn write(&mut self, bytes: &[u8]) -> Result<()> { - Ok(self.encode(bytes)?) + self.extend_from_slice(bytes); + Ok(()) } } -impl Writer for pem::Encoder<'_, '_> { +#[cfg(feature = "base64")] +#[cfg_attr(docsrs, doc(cfg(feature = "base64")))] +impl Writer for Base64Writer<'_> { fn write(&mut self, bytes: &[u8]) -> Result<()> { Ok(self.encode(bytes)?) } } -#[cfg(feature = "alloc")] -#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -impl Writer for Vec { +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl Writer for pem::Encoder<'_, '_> { fn write(&mut self, bytes: &[u8]) -> Result<()> { - self.extend_from_slice(bytes); - Ok(()) + Ok(self.encode(bytes)?) } } +#[cfg(feature = "sha2")] +#[cfg_attr(docsrs, doc(cfg(feature = "sha2")))] impl Writer for Sha256 { fn write(&mut self, bytes: &[u8]) -> Result<()> { self.update(bytes); @@ -55,6 +67,8 @@ impl Writer for Sha256 { } } +#[cfg(feature = "sha2")] +#[cfg_attr(docsrs, doc(cfg(feature = "sha2")))] impl Writer for Sha512 { fn write(&mut self, bytes: &[u8]) -> Result<()> { self.update(bytes); diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index faf0e2e8..1b7a0bf5 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -18,8 +18,7 @@ edition = "2021" rust-version = "1.60" [dependencies] -base64ct = "1.4" -pem-rfc7468 = "0.6" +encoding = { package = "ssh-encoding", version = "0", features = ["base64", "pem", "sha2"], path = "../ssh-encoding" } sha2 = { version = "0.10.6", default-features = false } zeroize = { version = "1", default-features = false } @@ -48,14 +47,17 @@ zeroize_derive = "1.3" # hack to make minimal-versions lint happy (pulled in by [features] default = ["ecdsa", "rand_core", "std"] -alloc = ["base64ct/alloc", "signature/hazmat-preview", "zeroize/alloc"] +alloc = [ + "encoding/alloc", + "signature/hazmat-preview", + "zeroize/alloc" +] std = [ "alloc", - "base64ct/std", "ed25519-dalek?/std", + "encoding/std", "p256?/std", "p384?/std", - "pem-rfc7468/std", "rsa?/std", "sec1?/std", "signature?/std" diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index 2465cd91..ca211614 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -1,7 +1,8 @@ //! Algorithm support. -use crate::{decode::Decode, encode::Encode, reader::Reader, writer::Writer, Error, Result}; +use crate::{Error, Result}; use core::{fmt, str}; +use encoding::Label; #[cfg(feature = "alloc")] use { @@ -75,34 +76,6 @@ const SK_ECDSA_SHA2_P256: &str = "sk-ecdsa-sha2-nistp256@openssh.com"; /// U2F/FIDO security key with Ed25519 const SK_SSH_ED25519: &str = "sk-ssh-ed25519@openssh.com"; -/// Maximum size of any algorithm name/identifier. -const MAX_ALG_NAME_SIZE: usize = 48; - -/// String identifiers for cryptographic algorithms. -/// -/// Receives a blanket impl of [`Decode`] and [`Encode`]. -pub(crate) trait AlgString: AsRef + str::FromStr {} - -impl Decode for T { - fn decode(reader: &mut impl Reader) -> Result { - let mut buf = [0u8; MAX_ALG_NAME_SIZE]; - reader - .read_string(buf.as_mut()) - .map_err(|_| Error::Algorithm)? - .parse() - } -} - -impl Encode for T { - fn encoded_len(&self) -> Result { - self.as_ref().encoded_len() - } - - fn encode(&self, writer: &mut impl Writer) -> Result<()> { - self.as_ref().encode(writer) - } -} - /// SSH key algorithms. /// /// This type provides a registry of supported digital signature algorithms @@ -285,7 +258,9 @@ impl AsRef for Algorithm { } } -impl AlgString for Algorithm {} +impl Label for Algorithm { + type Error = Error; +} impl Default for Algorithm { fn default() -> Algorithm { @@ -363,7 +338,9 @@ impl AsRef for EcdsaCurve { } } -impl AlgString for EcdsaCurve {} +impl Label for EcdsaCurve { + type Error = Error; +} impl fmt::Display for EcdsaCurve { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -432,7 +409,9 @@ impl HashAlg { } } -impl AlgString for HashAlg {} +impl Label for HashAlg { + type Error = Error; +} impl AsRef for HashAlg { fn as_ref(&self) -> &str { @@ -498,7 +477,9 @@ impl KdfAlg { } } -impl AlgString for KdfAlg {} +impl Label for KdfAlg { + type Error = Error; +} impl AsRef for KdfAlg { fn as_ref(&self) -> &str { diff --git a/ssh-key/src/authorized_keys.rs b/ssh-key/src/authorized_keys.rs index c24ab055..d449a136 100644 --- a/ssh-key/src/authorized_keys.rs +++ b/ssh-key/src/authorized_keys.rs @@ -293,10 +293,10 @@ impl<'a> ConfigOptsIter<'a> { | b'}' | b'|' | b'~' => (), - _ => return Err(Error::CharacterEncoding), + _ => return Err(encoding::Error::CharacterEncoding.into()), } - index = index.checked_add(1).ok_or(Error::Length)?; + index = index.checked_add(1).ok_or(encoding::Error::Length)?; } let remaining = self.0; @@ -317,7 +317,6 @@ impl<'a> Iterator for ConfigOptsIter<'a> { #[cfg(all(test, feature = "alloc"))] mod tests { use super::ConfigOptsIter; - use crate::Error; #[test] fn options_empty() { @@ -362,10 +361,16 @@ mod tests { #[test] fn options_invalid_character() { let mut opts = ConfigOptsIter("❌"); - assert_eq!(opts.try_next(), Err(Error::CharacterEncoding)); + assert_eq!( + opts.try_next(), + Err(encoding::Error::CharacterEncoding.into()) + ); let mut opts = ConfigOptsIter("x,❌"); assert_eq!(opts.try_next(), Ok(Some("x"))); - assert_eq!(opts.try_next(), Err(Error::CharacterEncoding)); + assert_eq!( + opts.try_next(), + Err(encoding::Error::CharacterEncoding.into()) + ); } } diff --git a/ssh-key/src/certificate.rs b/ssh-key/src/certificate.rs index 26e99e05..a1530246 100644 --- a/ssh-key/src/certificate.rs +++ b/ssh-key/src/certificate.rs @@ -10,12 +10,7 @@ pub use self::{builder::Builder, cert_type::CertType, field::Field, options_map: use self::unix_time::UnixTime; use crate::{ - checked::CheckedSum, - decode::Decode, - encode::Encode, public::{Encapsulation, KeyData}, - reader::{Base64Reader, Reader}, - writer::{base64_len, Writer}, Algorithm, Error, Fingerprint, HashAlg, Result, Signature, }; use alloc::{ @@ -24,6 +19,7 @@ use alloc::{ vec::Vec, }; use core::str::FromStr; +use encoding::{base64_len_approx, Base64Reader, CheckedSum, Decode, Encode, Reader, Writer}; use signature::Verifier; #[cfg(feature = "serde")] @@ -185,14 +181,14 @@ impl Certificate { } cert.comment = encapsulation.comment.to_owned(); - reader.finish(cert) + Ok(reader.finish(cert)?) } /// Parse a raw binary OpenSSH certificate. pub fn from_bytes(mut bytes: &[u8]) -> Result { let reader = &mut bytes; let cert = Certificate::decode(reader)?; - reader.finish(cert) + Ok(reader.finish(cert)?) } /// Encode OpenSSH certificate to a [`String`]. @@ -200,7 +196,7 @@ impl Certificate { let encoded_len = [ 2, // interstitial spaces self.algorithm().as_certificate_str().len(), - base64_len(self.encoded_len()?), + base64_len_approx(self.encoded_len()?), self.comment.len(), ] .checked_sum()?; @@ -472,6 +468,8 @@ impl Certificate { } impl Decode for Certificate { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let algorithm = Algorithm::new_certificate(&String::decode(reader)?)?; @@ -495,8 +493,10 @@ impl Decode for Certificate { } impl Encode for Certificate { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.algorithm().as_certificate_str().encoded_len()?, self.nonce.encoded_len()?, self.public_key.encoded_key_data_len()?, @@ -514,7 +514,7 @@ impl Encode for Certificate { 4, // signature length prefix (uint32) self.signature.encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { diff --git a/ssh-key/src/certificate/cert_type.rs b/ssh-key/src/certificate/cert_type.rs index 9ba47d3b..38f040f7 100644 --- a/ssh-key/src/certificate/cert_type.rs +++ b/ssh-key/src/certificate/cert_type.rs @@ -1,5 +1,7 @@ -/// OpenSSH certificate types. -use crate::{decode::Decode, encode::Encode, reader::Reader, writer::Writer, Error, Result}; +//! OpenSSH certificate types. + +use crate::{Error, Result}; +use encoding::{Decode, Encode, Reader, Writer}; /// Types of OpenSSH certificates: user or host. #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -24,25 +26,30 @@ impl CertType { } } -impl Decode for CertType { - fn decode(reader: &mut impl Reader) -> Result { - u32::decode(reader)?.try_into() - } -} - impl Default for CertType { fn default() -> Self { Self::User } } +impl Decode for CertType { + type Error = Error; + + fn decode(reader: &mut impl Reader) -> Result { + u32::decode(reader)?.try_into() + } +} + impl Encode for CertType { + type Error = Error; + fn encoded_len(&self) -> Result { Ok(4) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { - u32::from(*self).encode(writer) + u32::from(*self).encode(writer)?; + Ok(()) } } diff --git a/ssh-key/src/certificate/options_map.rs b/ssh-key/src/certificate/options_map.rs index ff9461b5..7bf639c4 100644 --- a/ssh-key/src/certificate/options_map.rs +++ b/ssh-key/src/certificate/options_map.rs @@ -1,16 +1,41 @@ //! OpenSSH certificate options used by critical options and extensions. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, reader::Reader, writer::Writer, Error, - Result, +use crate::{Error, Result}; +use alloc::{collections::BTreeMap, string::String, vec::Vec}; +use core::{ + cmp::Ordering, + ops::{Deref, DerefMut}, }; -use alloc::{string::String, vec::Vec}; -use core::cmp::Ordering; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; /// Key/value map type used for certificate's critical options and extensions. -pub type OptionsMap = alloc::collections::BTreeMap; +#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)] +pub struct OptionsMap(pub BTreeMap); + +impl OptionsMap { + /// Create a new [`OptionsMap`]. + pub fn new() -> Self { + Self::default() + } +} + +impl Deref for OptionsMap { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OptionsMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} impl Decode for OptionsMap { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { reader.read_nested(|reader| { let mut entries = Vec::<(String, String)>::new(); @@ -37,19 +62,23 @@ impl Decode for OptionsMap { } impl Encode for OptionsMap { + type Error = Error; + fn encoded_len(&self) -> Result { - self.iter().try_fold(4, |acc, (name, data)| { - [acc, 4, name.len(), 4, data.len()].checked_sum() - }) + self.iter() + .try_fold(4, |acc, (name, data)| { + [acc, 4, name.len(), 4, data.len()].checked_sum() + }) + .map_err(Into::into) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { self.encoded_len()? .checked_sub(4) - .ok_or(Error::Length)? + .ok_or(encoding::Error::Length)? .encode(writer)?; - for (name, data) in self { + for (name, data) in self.iter() { name.encode(writer)?; data.encode(writer)?; } @@ -57,3 +86,24 @@ impl Encode for OptionsMap { Ok(()) } } + +impl From> for OptionsMap { + fn from(map: BTreeMap) -> OptionsMap { + OptionsMap(map) + } +} + +impl From for BTreeMap { + fn from(map: OptionsMap) -> BTreeMap { + map.0 + } +} + +impl FromIterator<(String, String)> for OptionsMap { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + BTreeMap::from_iter(iter).into() + } +} diff --git a/ssh-key/src/certificate/unix_time.rs b/ssh-key/src/certificate/unix_time.rs index f2190c49..847ed924 100644 --- a/ssh-key/src/certificate/unix_time.rs +++ b/ssh-key/src/certificate/unix_time.rs @@ -1,8 +1,9 @@ //! Unix timestamps. -use crate::{decode::Decode, encode::Encode, reader::Reader, writer::Writer, Error, Result}; +use crate::{Error, Result}; use core::fmt; use core::fmt::Formatter; +use encoding::{Decode, Encode, Reader, Writer}; #[cfg(feature = "std")] use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -65,18 +66,23 @@ impl UnixTime { } impl Decode for UnixTime { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { u64::decode(reader)?.try_into() } } impl Encode for UnixTime { + type Error = Error; + fn encoded_len(&self) -> Result { - self.secs.encoded_len() + Ok(self.secs.encoded_len()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { - self.secs.encode(writer) + self.secs.encode(writer)?; + Ok(()) } } diff --git a/ssh-key/src/cipher.rs b/ssh-key/src/cipher.rs index d4a74526..df3e80b4 100644 --- a/ssh-key/src/cipher.rs +++ b/ssh-key/src/cipher.rs @@ -2,8 +2,9 @@ //! //! These are used for encrypting private keys. -use crate::{algorithm::AlgString, Error, Result}; +use crate::{Error, Result}; use core::{fmt, str}; +use encoding::Label; #[cfg(feature = "encryption")] use aes::{ @@ -126,7 +127,9 @@ impl AsRef for Cipher { } } -impl AlgString for Cipher {} +impl Label for Cipher { + type Error = Error; +} impl Default for Cipher { fn default() -> Cipher { diff --git a/ssh-key/src/error.rs b/ssh-key/src/error.rs index 4436e791..257b5ee4 100644 --- a/ssh-key/src/error.rs +++ b/ssh-key/src/error.rs @@ -1,6 +1,5 @@ //! Error types -use crate::pem; use core::fmt; #[cfg(feature = "alloc")] @@ -16,9 +15,6 @@ pub enum Error { /// Algorithm-related errors. Algorithm, - /// Base64-related errors. - Base64(base64ct::Error), - /// Certificate field is invalid or already set. #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] @@ -27,9 +23,6 @@ pub enum Error { /// Certificate validation failed. CertificateValidation, - /// Character encoding-related errors. - CharacterEncoding, - /// Cryptographic errors. Crypto, @@ -41,6 +34,9 @@ pub enum Error { #[cfg_attr(docsrs, doc(cfg(feature = "ecdsa")))] Ecdsa(sec1::Error), + /// Encoding errors. + Encoding(encoding::Error), + /// Cannot perform operation on encrypted private key. Encrypted, @@ -52,18 +48,9 @@ pub enum Error { #[cfg_attr(docsrs, doc(cfg(feature = "std")))] Io(std::io::ErrorKind), - /// Invalid length. - Length, - /// Namespace invalid. Namespace, - /// Overflow errors. - Overflow, - - /// PEM encoding errors. - Pem(pem::Error), - /// Public key is incorrect. PublicKey, @@ -87,25 +74,21 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::Algorithm => write!(f, "unknown or unsupported algorithm"), - Error::Base64(err) => write!(f, "Base64 encoding error: {}", err), #[cfg(feature = "alloc")] Error::CertificateFieldInvalid(field) => { write!(f, "certificate field invalid: {}", field) } Error::CertificateValidation => write!(f, "certificate validation failed"), - Error::CharacterEncoding => write!(f, "character encoding invalid"), Error::Crypto => write!(f, "cryptographic error"), Error::Decrypted => write!(f, "private key is already decrypted"), #[cfg(feature = "ecdsa")] Error::Ecdsa(err) => write!(f, "ECDSA encoding error: {}", err), + Error::Encoding(err) => write!(f, "{}", err), Error::Encrypted => write!(f, "private key is encrypted"), Error::FormatEncoding => write!(f, "format encoding error"), #[cfg(feature = "std")] Error::Io(err) => write!(f, "I/O error: {}", std::io::Error::from(*err)), - Error::Length => write!(f, "length invalid"), Error::Namespace => write!(f, "namespace invalid"), - Error::Overflow => write!(f, "internal overflow error"), - Error::Pem(err) => write!(f, "{}", err), Error::PublicKey => write!(f, "public key is incorrect"), Error::Time => write!(f, "invalid time"), Error::TrailingData { remaining } => write!( @@ -118,47 +101,41 @@ impl fmt::Display for Error { } } -impl From for Error { - fn from(err: base64ct::Error) -> Error { - Error::Base64(err) - } -} - -impl From for Error { - fn from(_: base64ct::InvalidLengthError) -> Error { - Error::Length +impl From for Error { + fn from(_: core::array::TryFromSliceError) -> Error { + Error::Encoding(encoding::Error::Length) } } -impl From for Error { - fn from(_: core::array::TryFromSliceError) -> Error { - Error::Length +impl From for Error { + fn from(err: core::str::Utf8Error) -> Error { + Error::Encoding(err.into()) } } -impl From for Error { - fn from(_: core::num::TryFromIntError) -> Error { - Error::Overflow +impl From for Error { + fn from(err: encoding::Error) -> Error { + Error::Encoding(err) } } -impl From for Error { - fn from(_: core::str::Utf8Error) -> Error { - Error::CharacterEncoding +impl From for Error { + fn from(err: encoding::base64::Error) -> Error { + Error::Encoding(err.into()) } } -impl From for Error { - fn from(err: pem_rfc7468::Error) -> Error { - Error::Pem(err) +impl From for Error { + fn from(err: encoding::pem::Error) -> Error { + Error::Encoding(err.into()) } } #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] impl From for Error { - fn from(_: alloc::string::FromUtf8Error) -> Error { - Error::CharacterEncoding + fn from(err: alloc::string::FromUtf8Error) -> Error { + Error::Encoding(err.into()) } } @@ -203,6 +180,7 @@ impl From for Error { } #[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl From for Error { fn from(_: std::time::SystemTimeError) -> Error { Error::Time @@ -210,4 +188,5 @@ impl From for Error { } #[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for Error {} diff --git a/ssh-key/src/fingerprint.rs b/ssh-key/src/fingerprint.rs index 7231e439..563b701d 100644 --- a/ssh-key/src/fingerprint.rs +++ b/ssh-key/src/fingerprint.rs @@ -1,11 +1,14 @@ //! SSH public key fingerprints. -use crate::{encode::Encode, public, Error, HashAlg, Result}; -use base64ct::{Base64Unpadded, Encoding}; +use crate::{public, Error, HashAlg, Result}; use core::{ fmt::{self, Display}, str::{self, FromStr}, }; +use encoding::{ + base64::{Base64Unpadded, Encoding}, + Encode, +}; use sha2::{Digest, Sha256, Sha512}; /// Fingerprint encoding error message. diff --git a/ssh-key/src/kdf.rs b/ssh-key/src/kdf.rs index 5b4a49bb..488a5a87 100644 --- a/ssh-key/src/kdf.rs +++ b/ssh-key/src/kdf.rs @@ -2,10 +2,8 @@ //! //! These are used for deriving an encryption key from a password. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, reader::Reader, writer::Writer, Error, - KdfAlg, Result, -}; +use crate::{Error, KdfAlg, Result}; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; #[cfg(feature = "alloc")] use alloc::vec::Vec; @@ -98,7 +96,9 @@ impl Kdf { password: impl AsRef<[u8]>, ) -> Result<(Zeroizing>, Vec)> { let (key_size, iv_size) = cipher.key_and_iv_size().ok_or(Error::Decrypted)?; - let okm_size = key_size.checked_add(iv_size).ok_or(Error::Length)?; + let okm_size = key_size + .checked_add(iv_size) + .ok_or(encoding::Error::Length)?; let mut okm = Zeroizing::new(vec![0u8; okm_size]); self.derive(password, &mut okm)?; @@ -131,6 +131,8 @@ impl Default for Kdf { } impl Decode for Kdf { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { match KdfAlg::decode(reader)? { KdfAlg::None => { @@ -157,6 +159,8 @@ impl Decode for Kdf { } impl Encode for Kdf { + type Error = Error; + fn encoded_len(&self) -> Result { let kdfopts_len = match self { Self::None => 0, @@ -164,25 +168,27 @@ impl Encode for Kdf { Self::Bcrypt { salt, .. } => [8, salt.len()].checked_sum()?, }; - [ + Ok([ self.algorithm().encoded_len()?, 4, // kdfopts length prefix (uint32) kdfopts_len, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { self.algorithm().encode(writer)?; match self { - Self::None => 0usize.encode(writer), + Self::None => 0usize.encode(writer)?, #[cfg(feature = "alloc")] Self::Bcrypt { salt, rounds } => { [8, salt.len()].checked_sum()?.encode(writer)?; salt.encode(writer)?; - rounds.encode(writer) + rounds.encode(writer)? } } + + Ok(()) } } diff --git a/ssh-key/src/known_hosts.rs b/ssh-key/src/known_hosts.rs index 0ba12ac1..197ec214 100644 --- a/ssh-key/src/known_hosts.rs +++ b/ssh-key/src/known_hosts.rs @@ -1,9 +1,8 @@ //! Parser for `KnownHostsFile`-formatted data. -use base64ct::{Base64, Encoding}; - use crate::{Error, PublicKey, Result}; use core::str; +use encoding::base64::{Base64, Encoding}; use { alloc::string::{String, ToString}, diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index 6b8621ad..638ad48e 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -149,15 +149,10 @@ pub mod public; pub mod certificate; mod algorithm; -mod checked; mod cipher; -mod decode; -mod encode; mod error; mod fingerprint; mod kdf; -mod reader; -mod writer; #[cfg(feature = "alloc")] mod mpint; @@ -176,8 +171,7 @@ pub use crate::{ private::PrivateKey, public::PublicKey, }; -pub use base64ct::LineEnding; -pub use pem_rfc7468 as pem; +pub use encoding::LineEnding; pub use sha2; #[cfg(feature = "alloc")] @@ -194,6 +188,3 @@ pub use sec1; #[cfg(feature = "rand_core")] pub use rand_core; - -/// Line width used by the PEM encoding of OpenSSH documents. -const PEM_LINE_WIDTH: usize = 70; diff --git a/ssh-key/src/mpint.rs b/ssh-key/src/mpint.rs index eb925a92..149e40ed 100644 --- a/ssh-key/src/mpint.rs +++ b/ssh-key/src/mpint.rs @@ -1,11 +1,9 @@ //! Multiple precision integer -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, reader::Reader, writer::Writer, Error, - Result, -}; +use crate::{Error, Result}; use alloc::vec::Vec; use core::fmt; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use zeroize::Zeroize; #[cfg(any(feature = "dsa", feature = "rsa"))] @@ -107,18 +105,23 @@ impl AsRef<[u8]> for MPInt { } impl Decode for MPInt { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { Vec::decode(reader)?.try_into() } } impl Encode for MPInt { + type Error = Error; + fn encoded_len(&self) -> Result { - [4, self.as_bytes().len()].checked_sum() + Ok([4, self.as_bytes().len()].checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { - self.as_bytes().encode(writer) + self.as_bytes().encode(writer)?; + Ok(()) } } diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index 85bc843a..b3d9cfcc 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -115,8 +115,10 @@ mod rsa; #[cfg(feature = "alloc")] mod sk; -pub use self::ed25519::{Ed25519Keypair, Ed25519PrivateKey}; -pub use self::keypair::KeypairData; +pub use self::{ + ed25519::{Ed25519Keypair, Ed25519PrivateKey}, + keypair::KeypairData, +}; #[cfg(feature = "alloc")] pub use crate::{ @@ -134,17 +136,12 @@ pub use self::ecdsa::{EcdsaKeypair, EcdsaPrivateKey}; #[cfg(all(feature = "alloc", feature = "ecdsa"))] pub use self::sk::SkEcdsaSha2NistP256; -use crate::{ - checked::CheckedSum, - decode::Decode, - encode::Encode, +use crate::{public, Algorithm, Cipher, Error, Fingerprint, HashAlg, Kdf, PublicKey, Result}; +use core::str; +use encoding::{ pem::{self, LineEnding, PemLabel}, - public, - reader::Reader, - writer::Writer, - Algorithm, Cipher, Error, Fingerprint, HashAlg, Kdf, PublicKey, Result, PEM_LINE_WIDTH, + CheckedSum, Decode, Encode, Reader, Writer, PEM_LINE_WIDTH, }; -use core::str; #[cfg(feature = "alloc")] use { @@ -232,14 +229,14 @@ impl PrivateKey { let mut reader = pem::Decoder::new_wrapped(input.as_ref(), PEM_LINE_WIDTH)?; Self::validate_pem_label(reader.type_label())?; let private_key = Self::decode(&mut reader)?; - reader.finish(private_key) + Ok(reader.finish(private_key)?) } /// Parse a raw binary SSH private key. pub fn from_bytes(mut bytes: &[u8]) -> Result { let reader = &mut bytes; let private_key = Self::decode(reader)?; - reader.finish(private_key) + Ok(reader.finish(private_key)?) } /// Encode OpenSSH-formatted (PEM) private key. @@ -530,7 +527,7 @@ impl PrivateKey { // Ensure input data is padding-aligned if reader.remaining_len().checked_rem(block_size) != Some(0) { - return Err(Error::Length); + return Err(encoding::Error::Length.into()); } let checkint1 = u32::decode(reader)?; @@ -553,7 +550,7 @@ impl PrivateKey { let padding_len = reader.remaining_len(); if padding_len >= block_size { - return Err(Error::Length); + return Err(encoding::Error::Length.into()); } if padding_len != 0 { @@ -603,7 +600,7 @@ impl PrivateKey { /// and padded using the padding size for the given cipher. fn encoded_privatekey_comment_pair_len(&self, cipher: Cipher) -> Result { let len = self.unpadded_privatekey_comment_pair_len()?; - [len, cipher.padding_len(len)].checked_sum() + Ok([len, cipher.padding_len(len)].checked_sum()?) } /// Get the length of this private key when encoded with the given comment. @@ -616,16 +613,18 @@ impl PrivateKey { return Err(Error::Encrypted); } - [ + Ok([ 8, // 2 x uint32 checkints, self.key_data.encoded_len()?, self.comment().encoded_len()?, ] - .checked_sum() + .checked_sum()?) } } impl Decode for PrivateKey { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let mut auth_magic = [0u8; Self::AUTH_MAGIC.len()]; reader.read(&mut auth_magic)?; @@ -640,7 +639,7 @@ impl Decode for PrivateKey { // TODO(tarcieri): support more than one key? if nkeys != 1 { - return Err(Error::Length); + return Err(encoding::Error::Length.into()); } let public_key = reader.read_nested(public::KeyData::decode)?; @@ -660,7 +659,7 @@ impl Decode for PrivateKey { } if !reader.is_finished() { - return Err(Error::Length); + return Err(encoding::Error::Length.into()); } return Ok(Self { @@ -684,6 +683,8 @@ impl Decode for PrivateKey { } impl Encode for PrivateKey { + type Error = Error; + fn encoded_len(&self) -> Result { let private_key_len = if self.is_encrypted() { self.key_data.encoded_len()? @@ -691,7 +692,7 @@ impl Encode for PrivateKey { self.encoded_privatekey_comment_pair_len(Cipher::None)? }; - [ + Ok([ Self::AUTH_MAGIC.len(), self.cipher.encoded_len()?, self.kdf.encoded_len()?, @@ -701,7 +702,7 @@ impl Encode for PrivateKey { 4, // private key length prefix (uint32) private_key_len, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { diff --git a/ssh-key/src/private/dsa.rs b/ssh-key/src/private/dsa.rs index 34370e04..f9aa8920 100644 --- a/ssh-key/src/private/dsa.rs +++ b/ssh-key/src/private/dsa.rs @@ -1,15 +1,10 @@ //! Digital Signature Algorithm (DSA) private keys. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, public::DsaPublicKey, reader::Reader, - writer::Writer, MPInt, Result, -}; +use crate::{public::DsaPublicKey, Error, MPInt, Result}; use core::fmt; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use zeroize::Zeroize; -#[cfg(feature = "dsa")] -use crate::Error; - #[cfg(feature = "subtle")] use subtle::{Choice, ConstantTimeEq}; @@ -48,6 +43,8 @@ impl AsRef<[u8]> for DsaPrivateKey { } impl Decode for DsaPrivateKey { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { Ok(Self { inner: MPInt::decode(reader)?, @@ -62,6 +59,8 @@ impl Drop for DsaPrivateKey { } impl Encode for DsaPrivateKey { + type Error = Error; + fn encoded_len(&self) -> Result { self.inner.encoded_len() } @@ -166,6 +165,8 @@ impl DsaKeypair { } impl Decode for DsaKeypair { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let public = DsaPublicKey::decode(reader)?; let private = DsaPrivateKey::decode(reader)?; @@ -174,8 +175,10 @@ impl Decode for DsaKeypair { } impl Encode for DsaKeypair { + type Error = Error; + fn encoded_len(&self) -> Result { - [self.public.encoded_len()?, self.private.encoded_len()?].checked_sum() + Ok([self.public.encoded_len()?, self.private.encoded_len()?].checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { diff --git a/ssh-key/src/private/ecdsa.rs b/ssh-key/src/private/ecdsa.rs index c1f520da..16b15bc1 100644 --- a/ssh-key/src/private/ecdsa.rs +++ b/ssh-key/src/private/ecdsa.rs @@ -1,10 +1,8 @@ //! Elliptic Curve Digital Signature Algorithm (ECDSA) private keys. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, public::EcdsaPublicKey, reader::Reader, - writer::Writer, Algorithm, EcdsaCurve, Error, Result, -}; +use crate::{public::EcdsaPublicKey, Algorithm, EcdsaCurve, Error, Result}; use core::fmt; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use sec1::consts::{U32, U48, U66}; use zeroize::Zeroize; @@ -33,10 +31,18 @@ impl EcdsaPrivateKey { self.bytes } - /// Decode ECDSA private key using the provided Base64 reader. + /// Does this private key need to be prefixed with a leading zero? + fn needs_leading_zero(&self) -> bool { + self.bytes[0] >= 0x80 + } +} + +impl Decode for EcdsaPrivateKey { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { reader.read_nested(|reader| { - if reader.remaining_len() == SIZE.checked_add(1).ok_or(Error::Length)? { + if reader.remaining_len() == SIZE.checked_add(1).ok_or(encoding::Error::Length)? { // Strip leading zero // TODO(tarcieri): make sure leading zero was necessary if u8::decode(reader)? != 0 { @@ -49,16 +55,13 @@ impl EcdsaPrivateKey { Ok(Self { bytes }) }) } - - /// Does this private key need to be prefixed with a leading zero? - fn needs_leading_zero(&self) -> bool { - self.bytes[0] >= 0x80 - } } impl Encode for EcdsaPrivateKey { + type Error = Error; + fn encoded_len(&self) -> Result { - [4, self.needs_leading_zero().into(), SIZE].checked_sum() + Ok([4, self.needs_leading_zero().into(), SIZE].checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { @@ -70,7 +73,8 @@ impl Encode for EcdsaPrivateKey { writer.write(&[0])?; } - writer.write(&self.bytes) + writer.write(&self.bytes)?; + Ok(()) } } @@ -247,6 +251,8 @@ impl EcdsaKeypair { } impl Decode for EcdsaKeypair { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { match EcdsaPublicKey::decode(reader)? { EcdsaPublicKey::NistP256(public) => { @@ -266,6 +272,8 @@ impl Decode for EcdsaKeypair { } impl Encode for EcdsaKeypair { + type Error = Error; + fn encoded_len(&self) -> Result { let public_len = EcdsaPublicKey::from(self).encoded_len()?; @@ -275,7 +283,7 @@ impl Encode for EcdsaKeypair { Self::NistP521 { private, .. } => private.encoded_len()?, }; - [public_len, private_len].checked_sum() + Ok([public_len, private_len].checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { diff --git a/ssh-key/src/private/ed25519.rs b/ssh-key/src/private/ed25519.rs index a6078598..2babdb35 100644 --- a/ssh-key/src/private/ed25519.rs +++ b/ssh-key/src/private/ed25519.rs @@ -2,11 +2,9 @@ //! //! Edwards Digital Signature Algorithm (EdDSA) over Curve25519. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, public::Ed25519PublicKey, reader::Reader, - writer::Writer, Error, Result, -}; +use crate::{public::Ed25519PublicKey, Error, Result}; use core::fmt; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use zeroize::{Zeroize, Zeroizing}; #[cfg(feature = "rand_core")] @@ -211,6 +209,8 @@ impl Ed25519Keypair { } impl Decode for Ed25519Keypair { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { // Decode private key let public = Ed25519PublicKey::decode(reader)?; @@ -233,13 +233,16 @@ impl Decode for Ed25519Keypair { } impl Encode for Ed25519Keypair { + type Error = Error; + fn encoded_len(&self) -> Result { - [4, self.public.encoded_len()?, Self::BYTE_SIZE].checked_sum() + Ok([4, self.public.encoded_len()?, Self::BYTE_SIZE].checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { self.public.encode(writer)?; - Zeroizing::new(self.to_bytes()).as_ref().encode(writer) + Zeroizing::new(self.to_bytes()).as_ref().encode(writer)?; + Ok(()) } } diff --git a/ssh-key/src/private/keypair.rs b/ssh-key/src/private/keypair.rs index 3f9d000b..892551c4 100644 --- a/ssh-key/src/private/keypair.rs +++ b/ssh-key/src/private/keypair.rs @@ -1,10 +1,8 @@ //! Private key pairs. use super::ed25519::Ed25519Keypair; -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, public, reader::Reader, writer::Writer, - Algorithm, Error, Result, -}; +use crate::{public, Algorithm, Error, Result}; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; #[cfg(feature = "alloc")] use { @@ -240,6 +238,8 @@ impl KeypairData { } impl Decode for KeypairData { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { match Algorithm::decode(reader)? { #[cfg(feature = "alloc")] @@ -265,6 +265,8 @@ impl Decode for KeypairData { } impl Encode for KeypairData { + type Error = Error; + fn encoded_len(&self) -> Result { let key_len = match self { #[cfg(feature = "alloc")] @@ -282,7 +284,7 @@ impl Encode for KeypairData { Self::SkEd25519(sk) => sk.encoded_len()?, }; - [self.algorithm()?.encoded_len()?, key_len].checked_sum() + Ok([self.algorithm()?.encoded_len()?, key_len].checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { @@ -292,19 +294,21 @@ impl Encode for KeypairData { match self { #[cfg(feature = "alloc")] - Self::Dsa(key) => key.encode(writer), + Self::Dsa(key) => key.encode(writer)?, #[cfg(feature = "ecdsa")] - Self::Ecdsa(key) => key.encode(writer), - Self::Ed25519(key) => key.encode(writer), + Self::Ecdsa(key) => key.encode(writer)?, + Self::Ed25519(key) => key.encode(writer)?, #[cfg(feature = "alloc")] - Self::Encrypted(ciphertext) => writer.write(ciphertext), + Self::Encrypted(ciphertext) => writer.write(ciphertext)?, #[cfg(feature = "alloc")] - Self::Rsa(key) => key.encode(writer), + Self::Rsa(key) => key.encode(writer)?, #[cfg(all(feature = "alloc", feature = "ecdsa"))] - Self::SkEcdsaSha2NistP256(sk) => sk.encode(writer), + Self::SkEcdsaSha2NistP256(sk) => sk.encode(writer)?, #[cfg(feature = "alloc")] - Self::SkEd25519(sk) => sk.encode(writer), + Self::SkEd25519(sk) => sk.encode(writer)?, } + + Ok(()) } } diff --git a/ssh-key/src/private/rsa.rs b/ssh-key/src/private/rsa.rs index 36536e8d..19bb4cd0 100644 --- a/ssh-key/src/private/rsa.rs +++ b/ssh-key/src/private/rsa.rs @@ -1,15 +1,12 @@ //! Rivest–Shamir–Adleman (RSA) private keys. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, public::RsaPublicKey, reader::Reader, - writer::Writer, MPInt, Result, -}; +use crate::{public::RsaPublicKey, Error, MPInt, Result}; use core::fmt; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use zeroize::Zeroize; #[cfg(feature = "rsa")] use { - crate::Error, rand_core::{CryptoRng, RngCore}, rsa::{pkcs1v15, PublicKeyParts}, sha2::{digest::const_oid::AssociatedOid, Digest}, @@ -36,6 +33,8 @@ pub struct RsaPrivateKey { } impl Decode for RsaPrivateKey { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let d = MPInt::decode(reader)?; let iqmp = MPInt::decode(reader)?; @@ -46,21 +45,24 @@ impl Decode for RsaPrivateKey { } impl Encode for RsaPrivateKey { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.d.encoded_len()?, self.iqmp.encoded_len()?, self.p.encoded_len()?, self.q.encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { self.d.encode(writer)?; self.iqmp.encode(writer)?; self.p.encode(writer)?; - self.q.encode(writer) + self.q.encode(writer)?; + Ok(()) } } @@ -125,6 +127,8 @@ impl RsaKeypair { } impl Decode for RsaKeypair { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let n = MPInt::decode(reader)?; let e = MPInt::decode(reader)?; @@ -135,13 +139,15 @@ impl Decode for RsaKeypair { } impl Encode for RsaKeypair { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.public.n.encoded_len()?, self.public.e.encoded_len()?, self.private.encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { diff --git a/ssh-key/src/private/sk.rs b/ssh-key/src/private/sk.rs index 95016a7e..0b7e9910 100644 --- a/ssh-key/src/private/sk.rs +++ b/ssh-key/src/private/sk.rs @@ -2,11 +2,9 @@ //! //! [PROTOCOL.u2f]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, public, reader::Reader, writer::Writer, - Result, -}; +use crate::{public, Error, Result}; use alloc::vec::Vec; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; /// Security Key (FIDO/U2F) ECDSA/NIST P-256 private key as specified in /// [PROTOCOL.u2f](https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD). @@ -47,6 +45,8 @@ impl SkEcdsaSha2NistP256 { #[cfg(feature = "ecdsa")] impl Decode for SkEcdsaSha2NistP256 { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { Ok(Self { public: public::SkEcdsaSha2NistP256::decode(reader)?, @@ -59,21 +59,24 @@ impl Decode for SkEcdsaSha2NistP256 { #[cfg(feature = "ecdsa")] impl Encode for SkEcdsaSha2NistP256 { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.public.encoded_len()?, self.flags.encoded_len()?, self.key_handle.encoded_len()?, self.reserved.encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { self.public.encode(writer)?; self.flags.encode(writer)?; self.key_handle.encode(writer)?; - self.reserved.encode(writer) + self.reserved.encode(writer)?; + Ok(()) } } @@ -114,6 +117,8 @@ impl SkEd25519 { } impl Decode for SkEd25519 { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { Ok(Self { public: public::SkEd25519::decode(reader)?, @@ -125,20 +130,23 @@ impl Decode for SkEd25519 { } impl Encode for SkEd25519 { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.public.encoded_len()?, self.flags.encoded_len()?, self.key_handle.encoded_len()?, self.reserved.encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { self.public.encode(writer)?; self.flags.encode(writer)?; self.key_handle.encode(writer)?; - self.reserved.encode(writer) + self.reserved.encode(writer)?; + Ok(()) } } diff --git a/ssh-key/src/public.rs b/ssh-key/src/public.rs index 60446269..0f5df429 100644 --- a/ssh-key/src/public.rs +++ b/ssh-key/src/public.rs @@ -23,22 +23,19 @@ pub use self::{ecdsa::EcdsaPublicKey, sk::SkEcdsaSha2NistP256}; pub(crate) use self::openssh::Encapsulation; -use crate::{ - decode::Decode, - encode::Encode, - reader::{Base64Reader, Reader}, - Algorithm, Error, Fingerprint, HashAlg, Result, -}; +use crate::{Algorithm, Error, Fingerprint, HashAlg, Result}; use core::str::FromStr; +use encoding::{Base64Reader, Decode, Encode, Reader}; #[cfg(feature = "alloc")] use { - crate::{checked::CheckedSum, writer::base64_len, SshSig}, + crate::SshSig, alloc::{ borrow::ToOwned, string::{String, ToString}, vec::Vec, }, + encoding::{base64_len_approx, CheckedSum}, }; #[cfg(all(feature = "alloc", feature = "serde"))] @@ -121,14 +118,14 @@ impl PublicKey { comment: encapsulation.comment.to_owned(), }; - reader.finish(public_key) + Ok(reader.finish(public_key)?) } /// Parse a raw binary SSH public key. pub fn from_bytes(mut bytes: &[u8]) -> Result { let reader = &mut bytes; let key_data = KeyData::decode(reader)?; - reader.finish(key_data.into()) + Ok(reader.finish(key_data.into())?) } /// Encode OpenSSH-formatted public key. @@ -146,7 +143,7 @@ impl PublicKey { let encoded_len = [ 2, // interstitial spaces self.algorithm().as_str().len(), - base64_len(self.key_data.encoded_len()?), + base64_len_approx(self.key_data.encoded_len()?), self.comment.len(), ] .checked_sum()?; diff --git a/ssh-key/src/public/dsa.rs b/ssh-key/src/public/dsa.rs index c5563152..00aa5cf4 100644 --- a/ssh-key/src/public/dsa.rs +++ b/ssh-key/src/public/dsa.rs @@ -1,12 +1,7 @@ //! Digital Signature Algorithm (DSA) public keys. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, reader::Reader, writer::Writer, MPInt, - Result, -}; - -#[cfg(feature = "dsa")] -use crate::Error; +use crate::{Error, MPInt, Result}; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; /// Digital Signature Algorithm (DSA) public key. /// @@ -29,6 +24,8 @@ pub struct DsaPublicKey { } impl Decode for DsaPublicKey { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let p = MPInt::decode(reader)?; let q = MPInt::decode(reader)?; @@ -39,14 +36,16 @@ impl Decode for DsaPublicKey { } impl Encode for DsaPublicKey { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.p.encoded_len()?, self.q.encoded_len()?, self.g.encoded_len()?, self.y.encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { diff --git a/ssh-key/src/public/ecdsa.rs b/ssh-key/src/public/ecdsa.rs index 7990c2f6..9c8684b8 100644 --- a/ssh-key/src/public/ecdsa.rs +++ b/ssh-key/src/public/ecdsa.rs @@ -1,10 +1,8 @@ //! Elliptic Curve Digital Signature Algorithm (ECDSA) public keys. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, reader::Reader, writer::Writer, Algorithm, - EcdsaCurve, Error, Result, -}; +use crate::{Algorithm, EcdsaCurve, Error, Result}; use core::fmt; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use sec1::consts::{U32, U48, U66}; /// ECDSA/NIST P-256 public key. @@ -61,10 +59,10 @@ impl EcdsaPublicKey { 32 => Ok(Self::NistP256(EcdsaNistP256PublicKey::from_bytes(bytes)?)), 48 => Ok(Self::NistP384(EcdsaNistP384PublicKey::from_bytes(bytes)?)), 66 => Ok(Self::NistP521(EcdsaNistP521PublicKey::from_bytes(bytes)?)), - _ => Err(Error::Length), + _ => Err(encoding::Error::Length.into()), } } - _ => Err(Error::Length), + _ => Err(encoding::Error::Length.into()), } } @@ -101,6 +99,8 @@ impl AsRef<[u8]> for EcdsaPublicKey { } impl Decode for EcdsaPublicKey { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let curve = EcdsaCurve::decode(reader)?; @@ -116,18 +116,21 @@ impl Decode for EcdsaPublicKey { } impl Encode for EcdsaPublicKey { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.curve().encoded_len()?, 4, // uint32 length prefix self.as_ref().len(), ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { self.curve().encode(writer)?; - self.as_ref().encode(writer) + self.as_ref().encode(writer)?; + Ok(()) } } diff --git a/ssh-key/src/public/ed25519.rs b/ssh-key/src/public/ed25519.rs index 5197a426..cea82e9b 100644 --- a/ssh-key/src/public/ed25519.rs +++ b/ssh-key/src/public/ed25519.rs @@ -2,11 +2,9 @@ //! //! Edwards Digital Signature Algorithm (EdDSA) over Curve25519. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, reader::Reader, writer::Writer, Error, - Result, -}; +use crate::{Error, Result}; use core::fmt; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; /// Ed25519 public key. // TODO(tarcieri): use `ed25519::PublicKey`? (doesn't exist yet) @@ -25,6 +23,8 @@ impl AsRef<[u8; Self::BYTE_SIZE]> for Ed25519PublicKey { } impl Decode for Ed25519PublicKey { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let mut bytes = [0u8; Self::BYTE_SIZE]; reader.read_nested(|reader| reader.read(&mut bytes))?; @@ -33,12 +33,15 @@ impl Decode for Ed25519PublicKey { } impl Encode for Ed25519PublicKey { + type Error = Error; + fn encoded_len(&self) -> Result { - [4, Self::BYTE_SIZE].checked_sum() + Ok([4, Self::BYTE_SIZE].checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { - self.0.encode(writer) + self.0.encode(writer)?; + Ok(()) } } diff --git a/ssh-key/src/public/key_data.rs b/ssh-key/src/public/key_data.rs index 09cae902..2ecf94d9 100644 --- a/ssh-key/src/public/key_data.rs +++ b/ssh-key/src/public/key_data.rs @@ -1,10 +1,8 @@ //! Public key data. use super::{Ed25519PublicKey, SkEd25519}; -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, reader::Reader, writer::Writer, Algorithm, - Error, Fingerprint, HashAlg, Result, -}; +use crate::{Algorithm, Error, Fingerprint, HashAlg, Result}; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; #[cfg(feature = "alloc")] use super::{DsaPublicKey, RsaPublicKey}; @@ -224,6 +222,8 @@ impl KeyData { } impl Decode for KeyData { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let algorithm = Algorithm::decode(reader)?; Self::decode_as(reader, algorithm) @@ -231,12 +231,14 @@ impl Decode for KeyData { } impl Encode for KeyData { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.algorithm().encoded_len()?, self.encoded_key_data_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { diff --git a/ssh-key/src/public/openssh.rs b/ssh-key/src/public/openssh.rs index 96871f57..a55ba9c5 100644 --- a/ssh-key/src/public/openssh.rs +++ b/ssh-key/src/public/openssh.rs @@ -12,8 +12,9 @@ //! ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILM+rvN+ot98qgEN796jTiQfZfG1KaT0PtFDJ/XFSqti user@example.com //! ``` -use crate::{writer::Base64Writer, Error, Result}; +use crate::Result; use core::str; +use encoding::{Base64Writer, Error}; /// OpenSSH public key encapsulation parser. #[derive(Clone, Debug, Eq, PartialEq)] @@ -37,10 +38,12 @@ impl<'a> Encapsulation<'a> { let comment = str::from_utf8(bytes) .map_err(|_| Error::CharacterEncoding)? .trim_end(); + if algorithm_id.is_empty() || base64_data.is_empty() { // TODO(tarcieri): better errors for these cases? - return Err(Error::Length); + return Err(Error::Length.into()); } + Ok(Self { algorithm_id, base64_data, @@ -64,13 +67,16 @@ impl<'a> Encapsulation<'a> { let mut writer = Base64Writer::new(&mut out[offset..])?; f(&mut writer)?; + let base64_len = writer.finish()?.len(); offset = offset.checked_add(base64_len).ok_or(Error::Length)?; + if !comment.is_empty() { encode_str(out, &mut offset, " ")?; encode_str(out, &mut offset, comment)?; } + Ok(str::from_utf8(&out[..offset])?) } } @@ -91,15 +97,15 @@ fn decode_segment<'a>(bytes: &mut &'a [u8]) -> Result<&'a [u8]> { [b' ', rest @ ..] => { // Encountered space; we're done *bytes = rest; - return start.get(..len).ok_or(Error::Length); + return start.get(..len).ok_or_else(|| Error::Length.into()); } [_, ..] => { // Invalid character - return Err(Error::CharacterEncoding); + return Err(Error::CharacterEncoding.into()); } [] => { // End of input, could be truncated or could be no comment - return start.get(..len).ok_or(Error::Length); + return start.get(..len).ok_or_else(|| Error::Length.into()); } } } @@ -107,7 +113,7 @@ fn decode_segment<'a>(bytes: &mut &'a [u8]) -> Result<&'a [u8]> { /// Parse a segment of the public key as a `&str`. fn decode_segment_str<'a>(bytes: &mut &'a [u8]) -> Result<&'a str> { - str::from_utf8(decode_segment(bytes)?).map_err(|_| Error::CharacterEncoding) + str::from_utf8(decode_segment(bytes)?).map_err(|_| Error::CharacterEncoding.into()) } /// Encode a segment of the public key. @@ -115,11 +121,12 @@ fn encode_str(out: &mut [u8], offset: &mut usize, s: &str) -> Result<()> { let bytes = s.as_bytes(); if out.len() < offset.checked_add(bytes.len()).ok_or(Error::Length)? { - return Err(Error::Length); + return Err(Error::Length.into()); } out[*offset..][..bytes.len()].copy_from_slice(bytes); *offset = offset.checked_add(bytes.len()).ok_or(Error::Length)?; + Ok(()) } diff --git a/ssh-key/src/public/rsa.rs b/ssh-key/src/public/rsa.rs index e7d92068..f6e77e90 100644 --- a/ssh-key/src/public/rsa.rs +++ b/ssh-key/src/public/rsa.rs @@ -1,13 +1,11 @@ //! Rivest–Shamir–Adleman (RSA) public keys. -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, reader::Reader, writer::Writer, MPInt, - Result, -}; +use crate::{Error, MPInt, Result}; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; #[cfg(feature = "rsa")] use { - crate::{private::RsaKeypair, Error}, + crate::private::RsaKeypair, rsa::{pkcs1v15, PublicKeyParts}, sha2::{digest::const_oid::AssociatedOid, Digest}, }; @@ -32,6 +30,8 @@ impl RsaPublicKey { } impl Decode for RsaPublicKey { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let e = MPInt::decode(reader)?; let n = MPInt::decode(reader)?; @@ -40,8 +40,10 @@ impl Decode for RsaPublicKey { } impl Encode for RsaPublicKey { + type Error = Error; + fn encoded_len(&self) -> Result { - [self.e.encoded_len()?, self.n.encoded_len()?].checked_sum() + Ok([self.e.encoded_len()?, self.n.encoded_len()?].checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { diff --git a/ssh-key/src/public/sk.rs b/ssh-key/src/public/sk.rs index 12329ae0..d34b1e36 100644 --- a/ssh-key/src/public/sk.rs +++ b/ssh-key/src/public/sk.rs @@ -3,18 +3,14 @@ //! [PROTOCOL.u2f]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD use super::Ed25519PublicKey; -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, reader::Reader, writer::Writer, Result, -}; +use crate::{Error, Result}; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; #[cfg(feature = "alloc")] use alloc::{borrow::ToOwned, string::String}; #[cfg(feature = "ecdsa")] -use { - super::ecdsa::EcdsaNistP256PublicKey, - crate::{EcdsaCurve, Error}, -}; +use crate::{public::ecdsa::EcdsaNistP256PublicKey, EcdsaCurve}; /// Default FIDO/U2F Security Key application string. const DEFAULT_APPLICATION_STRING: &str = "ssh:"; @@ -55,6 +51,8 @@ impl SkEcdsaSha2NistP256 { #[cfg(feature = "ecdsa")] impl Decode for SkEcdsaSha2NistP256 { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { if EcdsaCurve::decode(reader)? != EcdsaCurve::NistP256 { return Err(Error::Crypto); @@ -78,19 +76,22 @@ impl Decode for SkEcdsaSha2NistP256 { #[cfg(feature = "ecdsa")] impl Encode for SkEcdsaSha2NistP256 { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ EcdsaCurve::NistP256.encoded_len()?, self.ec_point.as_bytes().encoded_len()?, self.application().encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { EcdsaCurve::NistP256.encode(writer)?; self.ec_point.as_bytes().encode(writer)?; - self.application().encode(writer) + self.application().encode(writer)?; + Ok(()) } } @@ -144,6 +145,8 @@ impl SkEd25519 { } impl Decode for SkEd25519 { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let public_key = Ed25519PublicKey::decode(reader)?; @@ -161,17 +164,20 @@ impl Decode for SkEd25519 { } impl Encode for SkEd25519 { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.public_key.encoded_len()?, self.application().encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { self.public_key.encode(writer)?; - self.application().encode(writer) + self.application().encode(writer)?; + Ok(()) } } diff --git a/ssh-key/src/signature.rs b/ssh-key/src/signature.rs index adec815f..e70457f0 100644 --- a/ssh-key/src/signature.rs +++ b/ssh-key/src/signature.rs @@ -1,11 +1,9 @@ //! Signatures (e.g. CA signatures over SSH certificates) -use crate::{ - checked::CheckedSum, decode::Decode, encode::Encode, private, public, reader::Reader, - writer::Writer, Algorithm, Error, MPInt, PrivateKey, PublicKey, Result, -}; +use crate::{private, public, Algorithm, Error, MPInt, PrivateKey, PublicKey, Result}; use alloc::vec::Vec; use core::fmt; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; use signature::{Signer, Verifier}; #[cfg(feature = "ed25519")] @@ -92,7 +90,7 @@ impl Signature { /// format the raw signature data for a given algorithm. /// /// # Returns - /// - [`Error::Length`] if the signature is not the correct length. + /// - [`Error::Encoding`] if the signature is not the correct length. pub fn new(algorithm: Algorithm, data: impl Into>) -> Result { let data = data.into(); @@ -108,17 +106,17 @@ impl Signature { if component.as_positive_bytes().ok_or(Error::Crypto)?.len() != curve.field_size() { - return Err(Error::Length); + return Err(encoding::Error::Length.into()); } } if !reader.is_finished() { - return Err(Error::Length); + return Err(encoding::Error::Length.into()); } } Algorithm::Ed25519 if data.len() == ED25519_SIGNATURE_SIZE => (), Algorithm::Rsa { hash: Some(_) } => (), - _ => return Err(Error::Length), + _ => return Err(encoding::Error::Length.into()), } Ok(Self { algorithm, data }) @@ -157,6 +155,8 @@ impl AsRef<[u8]> for Signature { } impl Decode for Signature { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let algorithm = Algorithm::decode(reader)?; let data = Vec::decode(reader)?; @@ -165,22 +165,25 @@ impl Decode for Signature { } impl Encode for Signature { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ self.algorithm().encoded_len()?, 4, // signature data length prefix (uint32) self.as_bytes().len(), ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { if self.is_placeholder() { - return Err(Error::Length); + return Err(encoding::Error::Length.into()); } self.algorithm().encode(writer)?; - self.as_bytes().encode(writer) + self.as_bytes().encode(writer)?; + Ok(()) } } @@ -602,8 +605,9 @@ impl Verifier for RsaPublicKey { #[cfg(test)] mod tests { use super::Signature; - use crate::{encode::Encode, Algorithm, EcdsaCurve, Error, HashAlg}; + use crate::{Algorithm, EcdsaCurve, HashAlg}; use alloc::vec::Vec; + use encoding::Encode; use hex_literal::hex; #[cfg(feature = "ed25519")] @@ -700,6 +704,9 @@ mod tests { assert!(placeholder.is_placeholder()); let mut writer = Vec::new(); - assert_eq!(placeholder.encode(&mut writer), Err(Error::Length)); + assert_eq!( + placeholder.encode(&mut writer), + Err(encoding::Error::Length.into()) + ); } } diff --git a/ssh-key/src/sshsig.rs b/ssh-key/src/sshsig.rs index 9024379e..d9c377b3 100644 --- a/ssh-key/src/sshsig.rs +++ b/ssh-key/src/sshsig.rs @@ -1,18 +1,12 @@ //! `sshsig` implementation. -use crate::{ - checked::CheckedSum, - decode::Decode, - encode::Encode, - pem::{self, PemLabel}, - public, - reader::Reader, - writer::Writer, - Algorithm, Error, HashAlg, Result, Signature, SigningKey, PEM_LINE_WIDTH, -}; +use crate::{public, Algorithm, Error, HashAlg, Result, Signature, SigningKey}; use alloc::{borrow::ToOwned, string::String, string::ToString, vec::Vec}; -use base64ct::LineEnding; use core::str::FromStr; +use encoding::{ + pem::{self, LineEnding, PemLabel}, + CheckedSum, Decode, Encode, Reader, Writer, PEM_LINE_WIDTH, +}; use signature::{Signer, Verifier}; type Version = u32; @@ -59,7 +53,7 @@ impl SshSig { let mut reader = pem::Decoder::new_wrapped(pem.as_ref(), PEM_LINE_WIDTH)?; Self::validate_pem_label(reader.type_label())?; let signature = Self::decode(&mut reader)?; - reader.finish(signature) + Ok(reader.finish(signature)?) } /// Encode signature as PEM which begins with the following: @@ -198,6 +192,8 @@ impl SshSig { } impl Decode for SshSig { + type Error = Error; + fn decode(reader: &mut impl Reader) -> Result { let mut magic_preamble = [0u8; Self::MAGIC_PREAMBLE.len()]; reader.read(&mut magic_preamble)?; @@ -235,8 +231,10 @@ impl Decode for SshSig { } impl Encode for SshSig { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ Self::MAGIC_PREAMBLE.len(), self.version.encoded_len()?, 4, // public key length prefix (uint32) @@ -247,7 +245,7 @@ impl Encode for SshSig { 4, // signature length prefix (uint32) self.signature.encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> { @@ -306,15 +304,17 @@ impl<'a> SignedData<'a> { } impl<'a> Encode for SignedData<'a> { + type Error = Error; + fn encoded_len(&self) -> Result { - [ + Ok([ SshSig::MAGIC_PREAMBLE.len(), self.namespace.encoded_len()?, self.reserved.encoded_len()?, self.hash_alg.encoded_len()?, self.hash.encoded_len()?, ] - .checked_sum() + .checked_sum()?) } fn encode(&self, writer: &mut impl Writer) -> Result<()> {